packages feed

cardano-coin-selection (empty) → 1.0.0

raw patch · 24 files changed

+6015/−0 lines, 24 filesdep +QuickCheckdep +basedep +bytestring

Dependencies added: QuickCheck, base, bytestring, cardano-coin-selection, containers, cryptonite, deepseq, fmt, hspec, memory, quiet, random, text, transformers, vector

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+## [1.0.0] - 2020-04-29++Initial release.
+ LICENSE view
@@ -0,0 +1,201 @@+                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright © 2019-2020 IOHK++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ cardano-coin-selection.cabal view
@@ -0,0 +1,103 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: fa91aa04303b2016d424d05014b688d5845de29176adad56b44a698e849d080a++name:           cardano-coin-selection+version:        1.0.0+synopsis:       Algorithms for coin selection and fee balancing.+description:    Please see the README on GitHub at <https://github.com/input-output-hk/cardano-coin-selection>+category:       Cardano+homepage:       https://github.com/input-output-hk/cardano-coin-selection#readme+bug-reports:    https://github.com/input-output-hk/cardano-coin-selection/issues+author:         IOHK+maintainer:     operations@iohk.io+copyright:      2020 IOHK+license:        Apache-2.0+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/input-output-hk/cardano-coin-selection++flag release+  description: Compile executables for a release.+  manual: True+  default: False++library+  exposed-modules:+      Cardano.CoinSelection+      Cardano.CoinSelection.Algorithm+      Cardano.CoinSelection.Algorithm.LargestFirst+      Cardano.CoinSelection.Algorithm.Migration+      Cardano.CoinSelection.Algorithm.RandomImprove+      Cardano.CoinSelection.Fee+      Internal+      Internal.Coin+      Internal.Invariant+      Internal.Rounding+  other-modules:+      Paths_cardano_coin_selection+  hs-source-dirs:+      src/library+      src/internal+  default-extensions: NoImplicitPrelude+  ghc-options: -Wall -Wcompat -fwarn-redundant-constraints+  build-depends:+      base >=4.7 && <5+    , bytestring+    , containers+    , cryptonite+    , deepseq+    , quiet+    , text+    , transformers+  if flag(release)+    ghc-options: -Werror+  default-language: Haskell2010++test-suite unit+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Cardano.CoinSelection.Algorithm.LargestFirstSpec+      Cardano.CoinSelection.Algorithm.MigrationSpec+      Cardano.CoinSelection.Algorithm.RandomImproveSpec+      Cardano.CoinSelection.FeeSpec+      Cardano.CoinSelection.TypesSpec+      Cardano.CoinSelectionSpec+      Cardano.Test.Utilities+      Internal.CoinSpec+      Test.Vector.Shuffle+      Test.Vector.ShuffleSpec+      Paths_cardano_coin_selection+  hs-source-dirs:+      src/test+  default-extensions: NoImplicitPrelude+  ghc-options: -Wall -Wcompat -fwarn-redundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      QuickCheck+    , base >=4.7 && <5+    , bytestring+    , cardano-coin-selection+    , containers+    , cryptonite+    , deepseq+    , fmt+    , hspec+    , memory+    , quiet+    , random+    , text+    , transformers+    , vector+  if flag(release)+    ghc-options: -Werror+  default-language: Haskell2010
+ src/internal/Internal.hs view
@@ -0,0 +1,20 @@+{-# OPTIONS_GHC -fno-warn-unused-imports #-}+{-# OPTIONS_HADDOCK hide #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- This module hierarchy provides types and functions that are not intended to+-- be part of the public API.+--+-- Types and functions defined herein are not guaranteed to be forwards or+-- backwards compatible across different versions of the library.+--+module Internal+    ( module Internal.Invariant+    , module Internal.Rounding+    ) where++import Internal.Invariant+import Internal.Rounding
+ src/internal/Internal/Coin.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleInstances #-}+{-# OPTIONS_HADDOCK hide #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+module Internal.Coin+    (+      -- * Types+      Coin++      -- Construction and Deconstruction+    , coinFromIntegral+    , coinFromNatural+    , coinToIntegral+    , coinToNatural++      -- * Unary Operations+    , pred+    , succ++      -- * Binary Operations+    , add+    , sub+    , mul+    , div+    , mod++      -- * Calculating Distances+    , distance++      -- * Value Tests+    , isZero++      -- * Special Values+    , zero+    , one++    ) where++import Prelude hiding+    ( div, fromIntegral, mod, pred, succ )++import GHC.Generics+    ( Generic )+import Numeric.Natural+    ( Natural )+import Quiet+    ( Quiet (Quiet) )++import qualified Prelude++-- | Represents a non-negative integral amount of currency.+--+-- Use 'coinFromNatural' to create a coin from a natural number.+--+-- Use 'coinToNatural' to convert a coin into a natural number.+--+-- @since 1.0.0+newtype Coin = Coin { unCoin :: Natural }+    deriving stock (Eq, Generic, Ord)+    deriving Show via (Quiet Coin)++-- | Creates a coin from an integral number.+--+-- Returns a coin if (and only if) the given input is not negative.+--+coinFromIntegral :: Integral i => i -> Maybe Coin+coinFromIntegral i+    | i >= 0    = Just $ Coin $ Prelude.fromIntegral i+    | otherwise = Nothing++-- | Creates a coin from a natural number.+--+-- @since 1.0.0+coinFromNatural :: Natural -> Coin+coinFromNatural = Coin++-- | Converts the given coin into an integral number.+--+coinToIntegral :: Integral i => Coin -> i+coinToIntegral (Coin i) = Prelude.fromIntegral i++-- | Converts the given coin into a natural number.+--+-- @since 1.0.0+coinToNatural :: Coin -> Natural+coinToNatural = unCoin++add :: Coin -> Coin -> Coin+add (Coin x) (Coin y) = Coin $ x + y++sub :: Coin -> Coin -> Maybe Coin+sub (Coin x) (Coin y) = coinFromIntegral $ toInteger x - toInteger y++mul :: Integral i => Coin -> i -> Maybe Coin+mul (Coin x) y = coinFromIntegral $ toInteger x * toInteger y++div :: Integral i => Coin -> i -> Maybe Coin+div (Coin x) y+    | y <= 0    = Nothing+    | otherwise = coinFromIntegral $ toInteger x `Prelude.div` toInteger y++mod :: Integral i => Coin -> i -> Maybe Coin+mod (Coin x) y+    | y <= 0    = Nothing+    | otherwise = coinFromIntegral $ toInteger x `Prelude.mod` toInteger y++distance :: Coin -> Coin -> Coin+distance (Coin x) (Coin y)+    | x >= y    = Coin $ x - y+    | otherwise = Coin $ y - x++pred :: Coin -> Maybe Coin+pred x = x `sub` one++succ :: Coin -> Coin+succ x = x `add` one++isZero :: Coin -> Bool+isZero = (== zero)++zero :: Coin+zero = Coin 0++one :: Coin+one = Coin 1++newtype Sum a = Sum { getSum :: a }+    deriving stock (Eq, Generic, Ord)+    deriving Show via (Quiet (Sum a))++instance Monoid Coin where+    mempty = zero++instance Semigroup Coin where+    (<>) = add
+ src/internal/Internal/Invariant.hs view
@@ -0,0 +1,35 @@+{-# OPTIONS_HADDOCK hide #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- Provides internal functions relating to verification of invariants.+--+module Internal.Invariant+    ( invariant+    ) where++import Prelude++-- | Checks whether or not an invariant holds, by applying the given predicate+--   to the given value.+--+-- If the invariant does not hold (indicated by the predicate function+-- returning 'False'), throws an error with the specified message.+--+-- >>> invariant "not empty" [1,2,3] (not . null)+-- [1, 2, 3]+--+-- >>> invariant "not empty" [] (not . null)+-- *** Exception: not empty+invariant+    :: String+        -- ^ The message+    -> a+        -- ^ The value to test+    -> (a -> Bool)+        -- ^ The predicate+    -> a+invariant msg a predicate =+    if predicate a then a else error msg
+ src/internal/Internal/Rounding.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE LambdaCase #-}++{-# OPTIONS_HADDOCK hide #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- Provides internal types and functions relating to rounding of fractional+-- numbers.+--+module Internal.Rounding+    ( RoundingDirection (..)+    , round+    ) where++import Prelude hiding+    ( round )++-- | Indicates a rounding direction to be used when converting from a+--   fractional value to an integral value.+--+-- See 'round'.+--+data RoundingDirection+    = RoundUp+      -- ^ Round up to the nearest integral value.+    | RoundDown+      -- ^ Round down to the nearest integral value.+    deriving (Eq, Show)++-- | Use the given rounding direction to round the given fractional value,+--   producing an integral result.+--+round :: (RealFrac a, Integral b) => RoundingDirection -> a -> b+round = \case+    RoundUp -> ceiling+    RoundDown -> floor
+ src/library/Cardano/CoinSelection.hs view
@@ -0,0 +1,399 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE RankNTypes #-}++{-# OPTIONS_HADDOCK prune #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- Provides general functions and types relating to coin selection.+--+-- The 'CoinSelection' type represents a __coin selection__, the basis for a+-- /transaction/ in a UTxO-based blockchain.+--+-- The 'CoinSelectionAlgorithm' type provides a __common interface__ to+-- algorithms that generate coin selections.+--+-- For a guide to __algorithms provided by this library__, see the+-- "Cardano.CoinSelection.Algorithm" module.+--+-- To adjust coin selections in order to __pay for transaction fees__, see+-- the "Cardano.CoinSelection.Fee" module.+--+module Cardano.CoinSelection+    (+      -- * Coin Selections+      CoinSelection (..)+    , sumInputs+    , sumOutputs+    , sumChange++      -- * Coin Selection Algorithms+    , CoinSelectionAlgorithm (..)+    , CoinSelectionParameters (..)+    , CoinSelectionResult (..)+    , CoinSelectionLimit (..)++      -- * Coins+    , Coin+    , coinFromNatural+    , coinToNatural++      -- * Coin Maps+    , CoinMap (..)+    , CoinMapEntry (..)+    , coinMapFromList+    , coinMapToList+    , coinMapValue++      -- * Coin Selection Errors+    , CoinSelectionError (..)+    , InputValueInsufficientError (..)+    , InputCountInsufficientError (..)+    , InputLimitExceededError (..)+    , InputsExhaustedError (..)++      -- # Internal Functions+    , coinMapRandomEntry++    ) where++import Prelude++import Control.Arrow+    ( (&&&) )+import Control.Monad.Trans.Except+    ( ExceptT (..) )+import Crypto.Number.Generate+    ( generateBetween )+import Crypto.Random.Types+    ( MonadRandom )+import Data.Map.Strict+    ( Map )+import Data.Word+    ( Word16 )+import GHC.Generics+    ( Generic )+import Internal.Coin+    ( Coin, coinFromNatural, coinToNatural )+import Numeric.Natural+    ( Natural )+import Quiet+    ( Quiet (Quiet) )++import qualified Data.Foldable as F+import qualified Data.Map.Strict as Map++--------------------------------------------------------------------------------+-- Coin Map+--------------------------------------------------------------------------------++-- | A mapping from unique keys to associated 'Coin' values.+--+-- A 'CoinMap' can be used to represent:+--+--   * a UTxO set, where each key within the map refers to an unspent output+--     from a previous transaction.+--+--   * a set of 'inputs' to a 'CoinSelection', where each input is an entry+--     selected from a UTxO set by a 'CoinSelectionAlgorithm'.+--+--   * a set of 'outputs' for a 'CoinSelection', where each key within the map+--     refers to the address of a payment recipient.+--+-- A 'CoinMap' can be constructed with the 'coinMapFromList' function.+--+-- The total value of a 'CoinMap' is given by the 'coinMapValue' function.+--+-- @since 1.0.0+newtype CoinMap a = CoinMap { unCoinMap :: Map a Coin }+    deriving (Eq, Generic)+    deriving Show via (Quiet (CoinMap a))++instance Foldable CoinMap where+    foldMap f = F.fold . fmap (f . entryKey) . coinMapToList++instance Ord a => Monoid (CoinMap a) where+    mempty = CoinMap mempty++instance Ord a => Semigroup (CoinMap a) where+    CoinMap a <> CoinMap b = CoinMap $ Map.unionWith (<>) a b++-- | An entry for a 'CoinMap'.+--+-- @since 1.0.0+data CoinMapEntry a = CoinMapEntry+    { entryKey+        :: a+        -- ^ The unique key associated with this entry.+    , entryValue+        :: Coin+        -- ^ The coin value associated with this entry.+    } deriving (Eq, Generic, Ord, Show)++-- | Constructs a 'CoinMap' from a list of entries.+--+-- See 'CoinMapEntry'.+--+-- @since 1.0.0+coinMapFromList :: Ord a => [CoinMapEntry a] -> CoinMap a+coinMapFromList = CoinMap+    . Map.fromListWith (<>)+    . fmap (entryKey &&& entryValue)++-- | Converts a 'CoinMap' to a list of entries.+--+-- See 'CoinMapEntry'.+--+-- @since 1.0.0+coinMapToList :: CoinMap a -> [CoinMapEntry a]+coinMapToList = fmap (uncurry CoinMapEntry) . Map.toList . unCoinMap++-- | Calculates the total coin value associated with a 'CoinMap'.+--+-- @since 1.0.0+coinMapValue :: CoinMap a -> Coin+coinMapValue = mconcat . fmap entryValue . coinMapToList++--------------------------------------------------------------------------------+-- Coin Selection+--------------------------------------------------------------------------------++-- | Provides a __common interface__ for coin selection algorithms.+--+-- The function 'selectCoins', when applied to the given+-- 'CoinSelectionParameters' object (with /available inputs/ and /requested/+-- /outputs/), will generate a 'CoinSelectionResult' (with /remaining inputs/+-- and a /coin selection/).+--+-- For implementations provided by this library, see+-- "Cardano.CoinSelection.Algorithm".+--+-- @since 1.0.0+newtype CoinSelectionAlgorithm i o m = CoinSelectionAlgorithm+    { selectCoins+        :: CoinSelectionParameters i o+        -> ExceptT CoinSelectionError m (CoinSelectionResult i o)+    }++-- | The complete set of parameters required for a 'CoinSelectionAlgorithm'.+--+-- The 'inputsAvailable' and 'outputsRequested' fields are both maps of unique+-- keys to associated 'Coin' values, where:+--+--   * Each key-value pair in the 'inputsAvailable' map corresponds to an+--     __unspent output__ from a previous transaction that is /available/+--     /for selection as an input/ by the coin selection algorithm. The /key/+--     is a unique reference to that output, and the /value/ is the amount of+--     unspent value associated with it.+--+--   * Each key-value pair in the 'outputsRequested' map corresponds to a+--     __payment__ whose value is /to be paid for/ by the coin selection+--     algorithm. The /key/ is a unique reference to a payment recipient,+--     and the /value/ is the amount of money to pay to that recipient.+--+-- A coin selection algorithm will select a __subset__ of inputs from+-- 'inputsAvailable' in order to pay for __all__ the outputs in+-- 'outputsRequested', where:+--+--   * Inputs __selected__ by the algorithm are included in the 'inputs'+--     set of the generated 'CoinSelection'.+--+--   * Inputs __not__ selected by the algorithm are included in the+--     'inputsRemaining' set of the 'CoinSelectionResult'.+--+-- The number of inputs that can selected is limited by 'limit'.+--+-- The total value of 'inputsAvailable' must be /greater than or equal to/+-- the total value of 'outputsRequested', as given by the 'coinMapValue'+-- function.+--+-- @since 1.0.0+data CoinSelectionParameters i o = CoinSelectionParameters+    { inputsAvailable :: CoinMap i+        -- ^ The set of inputs available for selection.+    , outputsRequested :: CoinMap o+        -- ^ The set of outputs requested for payment.+    , limit :: CoinSelectionLimit+        -- ^ A limit on the number of inputs that can be selected.+    }+    deriving Generic++-- | Represents the __result__ of running a coin selection algorithm.+--+-- See 'CoinSelectionAlgorithm'.+--+-- @since 1.0.0+data CoinSelectionResult i o = CoinSelectionResult+    { coinSelection :: CoinSelection i o+        -- ^ The generated coin selection.+    , inputsRemaining :: CoinMap i+        -- ^ The set of inputs that were __not__ selected.+    }++-- | A __coin selection__ is the basis for a /transaction/.+--+-- It consists of a selection of 'inputs', 'outputs', and 'change'.+--+-- The 'inputs' and 'outputs' fields are both maps of unique keys to associated+-- 'Coin' values, where:+--+--   * Each key-value pair in the 'inputs' map corresponds to an+--     __unspent output__ from a previous transaction (also known as a UTxO).+--     The /key/ is a unique reference to that output, and the /value/ is the+--     amount of unspent value associated with it.+--+--   * Each key-value pair in the 'outputs' map corresponds to a __payment__.+--     The /key/ is a unique reference to a payment recipient, and the /value/+--     is the amount of money to pay to that recipient.+--+-- The 'change' field is a set of coins to be returned to the originator of the+-- transaction.+--+-- The 'CoinSelectionAlgorithm' type provides a common interface for generating+-- coin selections.+--+-- @since 1.0.0+data CoinSelection i o = CoinSelection+    { inputs :: CoinMap i+      -- ^ The set of inputs.+    , outputs :: CoinMap o+      -- ^ The set of outputs.+    , change :: [Coin]+      -- ^ The set of change.+    }+    deriving (Generic, Show, Eq)++instance (Ord i, Ord o) => Semigroup (CoinSelection i o) where+    a <> b = CoinSelection+        { inputs = inputs a <> inputs b+        , outputs = outputs a <> outputs b+        , change = change a <> change b+        }++instance (Ord i, Ord o) => Monoid (CoinSelection i o) where+    mempty = CoinSelection mempty mempty mempty++-- | Calculate the total sum of all 'inputs' for the given 'CoinSelection'.+--+-- @since 1.0.0+sumInputs :: CoinSelection i o -> Coin+sumInputs = coinMapValue . inputs++-- | Calculate the total sum of all 'outputs' for the given 'CoinSelection'.+--+-- @since 1.0.0+sumOutputs :: CoinSelection i o -> Coin+sumOutputs =  coinMapValue . outputs++-- | Calculate the total sum of all 'change' for the given 'CoinSelection'.+--+-- @since 1.0.0+sumChange :: CoinSelection i o -> Coin+sumChange = mconcat . change++-- | Defines an __inclusive upper bound__ on the /number/ of inputs that+--   a 'CoinSelectionAlgorithm' is allowed to select.+--+-- @since 1.0.0+newtype CoinSelectionLimit = CoinSelectionLimit+    { calculateLimit+        :: Word16 -> Word16+            -- ^ Calculate the maximum number of inputs allowed for a given+            -- number of outputs.+    } deriving Generic++-- | Represents the set of possible failures that can occur when attempting+--   to produce a 'CoinSelection' with a 'CoinSelectionAlgorithm'.+--+-- See 'selectCoins'.+--+-- @since 1.0.0+data CoinSelectionError+    = InputValueInsufficient+        InputValueInsufficientError+    | InputCountInsufficient+        InputCountInsufficientError+    | InputLimitExceeded+        InputLimitExceededError+    | InputsExhausted+        InputsExhaustedError+    deriving (Eq, Show)++-- | Indicates that the total value of 'inputsAvailable' is less than the total+--   value of 'outputsRequested', making it /impossible/ to cover all payments,+--   /regardless/ of which algorithm is chosen.+--+-- @since 1.0.0+data InputValueInsufficientError =+    InputValueInsufficientError+    { inputValueAvailable :: Coin+        -- ^ The total value of 'inputsAvailable'.+    , inputValueRequired :: Coin+        -- ^ The total value of 'outputsRequested'.+    }+    deriving (Eq, Show)++-- | Indicates that the total count of entries in 'inputsAvailable' is /fewer/+--   /than/ required by the algorithm. The number required depends on the+--   particular algorithm implementation.+--+-- @since 1.0.0+data InputCountInsufficientError =+    InputCountInsufficientError+    { inputCountAvailable :: Natural+        -- ^ The number of entries in 'inputsAvailable'.+    , inputCountRequired :: Natural+        -- ^ The number of entries required.+    }+    deriving (Eq, Show)++-- | Indicates that all available entries in 'inputsAvailable' were depleted+--   /before/ all the payments in 'outputsRequested' could be paid for.+--+-- This condition can occur /even if/ the total value of 'inputsAvailable' is+-- greater than or equal to the total value of 'outputsRequested', due to+-- differences in the way that algorithms select inputs.+--+-- @since 1.0.0+data InputsExhaustedError =+    InputsExhaustedError+    deriving (Eq, Show)++-- | Indicates that the coin selection algorithm is unable to cover the total+--   value of 'outputsRequested' without exceeding the maximum number of inputs+--   defined by 'limit'.+--+-- See 'calculateLimit'.+--+-- @since 1.0.0+newtype InputLimitExceededError =+    InputLimitExceededError+    { calculatedInputLimit :: Word16 }+    deriving (Eq, Show)++--------------------------------------------------------------------------------+-- Internal Functions+--------------------------------------------------------------------------------++-- Selects an entry at random from a 'CoinMap', returning both the selected+-- entry and the map with the entry removed.+--+-- If the given map is empty, this function returns 'Nothing'.+--+coinMapRandomEntry+    :: MonadRandom m+    => CoinMap a+    -> m (Maybe (CoinMapEntry a), CoinMap a)+coinMapRandomEntry (CoinMap m)+    | Map.null m =+        return (Nothing, CoinMap m)+    | otherwise = do+        ix <- fromEnum <$> generateBetween 0 (toEnum (Map.size m - 1))+        let entry = uncurry CoinMapEntry $ Map.elemAt ix m+        let remainder = CoinMap $ Map.deleteAt ix m+        return (Just entry, remainder)
+ src/library/Cardano/CoinSelection/Algorithm.hs view
@@ -0,0 +1,57 @@+-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- __Submodules__ of this module provide implementations of+-- __coin selection algorithms__.+--+-- Algorithms can be divided into two categories:+--+--  * <#generalized-algorithms Generalized Algorithms>+--+--        Algorithms that implement the general+--        'Cardano.CoinSelection.CoinSelectionAlgorithm' interface.+--+--  * <#specialized-algorithms Specialized Algorithms>+--+--        Algorithms that provide functionality suited to specialized purposes.+--+-- = Generalized Algorithms #generalized-algorithms#+--+-- The following algorithms implement the general+-- 'Cardano.CoinSelection.CoinSelectionAlgorithm' interface:+--+--  * __"Cardano.CoinSelection.Algorithm.LargestFirst"__+--+--        Provides an implementation of the __Largest-First__ algorithm.+--+--        When selecting inputs from a given set of UTxO entries, this+--        algorithm always selects the /largest/ entries /first/.+--+--   * __"Cardano.CoinSelection.Algorithm.RandomImprove"__+--+--        Provides an implementation of the __Random-Improve__ algorithm.+--+--        When selecting inputs from a given set of UTxO entries, this+--        algorithm always selects entries at /random/.+--+--        Once selections have been made, a second phase attempts to /improve/+--        on each of the existing selections in order to optimize change+--        outputs.+--+-- For __guidance on choosing an algorithm__ that's appropriate for your+-- scenario, please consult the following article:+--+--        <https://iohk.io/blog/self-organisation-in-coin-selection/>+--+-- = Specialized Algorithms #specialized-algorithms#+--+-- The following algorithms provide functionality suited to specialized+-- purposes:+--+--   * __"Cardano.CoinSelection.Algorithm.Migration"__+--+--        Provides an algorithm for migrating all funds from one wallet to+--        another.+--+module Cardano.CoinSelection.Algorithm where
+ src/library/Cardano/CoinSelection/Algorithm/LargestFirst.hs view
@@ -0,0 +1,280 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- This module contains an implementation of the __Largest-First__ coin+-- selection algorithm.+--+module Cardano.CoinSelection.Algorithm.LargestFirst (+    largestFirst+  ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMapEntry (..)+    , CoinSelection (..)+    , CoinSelectionAlgorithm (..)+    , CoinSelectionError (..)+    , CoinSelectionLimit (..)+    , CoinSelectionParameters (..)+    , CoinSelectionResult (..)+    , InputCountInsufficientError (..)+    , InputLimitExceededError (..)+    , InputValueInsufficientError (..)+    , InputsExhaustedError (..)+    , coinMapFromList+    , coinMapToList+    , coinMapValue+    )+import Control.Monad+    ( foldM )+import Control.Monad.Trans.Except+    ( ExceptT (..), throwE )+import Data.Ord+    ( Down (..) )+import Internal.Coin+    ( Coin )++import qualified Data.Foldable as F+import qualified Data.List as L+import qualified Internal.Coin as C++-- | An implementation of the __Largest-First__ coin selection algorithm.+--+-- = Overview+--+-- The __Largest-First__ algorithm processes outputs in /descending order of/+-- /value/, from /largest/ to /smallest/.+--+-- For each output, it repeatedly selects the /largest/ remaining unspent UTxO+-- entry until the value of selected entries is greater than or equal to the+-- value of that output.+--+-- = State Maintained by the Algorithm+--+-- At all stages of processing, the algorithm maintains:+--+--  1.  A __/remaining UTxO list/__+--+--      This is initially equal to the given /initial UTxO set/ parameter,+--      sorted into /descending order of coin value/.+--+--      The /head/ of the list is always the remaining UTxO entry with the+--      /largest coin value/.+--+--      Entries are incrementally removed from the /head/ of the list as the+--      algorithm proceeds, until the list is empty.+--+--  2.  An __/unpaid output list/__+--+--      This is initially equal to the given /output list/ parameter, sorted+--      into /descending order of coin value/.+--+--      The /head/ of the list is always the unpaid output with the+--      /largest coin value/.+--+--      Entries are incrementally removed from the /head/ of the list as the+--      algorithm proceeds, until the list is empty.+--+--  3.  An __/accumulated coin selection/__+--+--      This is initially /empty/.+--+--      Entries are incrementally added as each output is paid for, until the+--      /unpaid output list/ is empty.+--+-- = Cardinality Rules+--+-- The algorithm requires that:+--+--  1.  Each output from the given /output list/ is paid for by /one or more/+--      entries from the /initial UTxO set/.+--+--  2.  Each entry from the /initial UTxO set/ is used to pay for /at most one/+--      output from the given /output list/.+--+--      (A single UTxO entry __cannot__ be used to pay for multiple outputs.)+--+-- = Order of Processing+--+-- The algorithm proceeds according to the following sequence of steps:+--+--  *   /Step 1/+--+--      Remove a single /unpaid output/ from the head of the+--      /unpaid output list/.+--+--  *   /Step 2/+--+--      Repeatedly remove UTxO entries from the head of the+--      /remaining UTxO list/ until the total value of entries removed is+--      /greater than or equal to/ the value of the /removed output/.+--+--  *   /Step 3/+--+--      Use the /removed UTxO entries/ to pay for the /removed output/.+--+--      This is achieved by:+--+--      *  adding the /removed UTxO entries/ to the 'inputs' field of the+--         /accumulated coin selection/.+--      *  adding the /removed output/ to the 'outputs' field of the+--         /accumulated coin selection/.+--+--  *   /Step 4/+--+--      If the /total value/ of the /removed UTxO entries/ is greater than the+--      value of the /removed output/, generate a coin whose value is equal to+--      the exact difference, and add it to the 'change' field of the+--      /accumulated coin selection/.+--+--  *   /Step 5/+--+--      If the /unpaid output list/ is empty, __terminate__ here.+--+--      Otherwise, return to /Step 1/.+--+-- = Termination+--+-- The algorithm terminates __successfully__ if the /remaining UTxO list/ is+-- not depleted before the /unpaid output list/ can be fully depleted (i.e., if+-- all the outputs have been paid for).+--+-- The /accumulated coin selection/ and /remaining UTxO list/ are returned to+-- the caller.+--+-- === Failure Modes+--+-- The algorithm terminates with an __error__ if:+--+--  1.  The /total value/ of the initial UTxO set (the amount of money+--      /available/) is /less than/ the total value of the output list (the+--      amount of money /required/).+--+--      See: __'InputValueInsufficientError'__.+--+--  2.  The /number/ of entries in the initial UTxO set is /smaller than/ the+--      number of requested outputs.+--+--      Due to the nature of the algorithm, /at least one/ UTxO entry is+--      required /for each/ output.+--+--      See: __'InputCountInsufficientError'__.+--+--  3.  Due to the particular /distribution/ of values within the initial UTxO+--      set, the algorithm depletes all entries from the UTxO set /before/ it+--      is able to pay for all requested outputs.+--+--      See: __'InputsExhaustedError'__.+--+--  4.  The /number/ of UTxO entries needed to pay for the requested outputs+--      would /exceed/ the upper limit specified by 'limit'.+--+--      See: __'InputLimitExceededError'__.+--+-- @since 1.0.0+largestFirst+    :: (Ord i, Ord o, Monad m)+    => CoinSelectionAlgorithm i o m+largestFirst = CoinSelectionAlgorithm payForOutputs++payForOutputs+    :: (Ord i, Ord o, Monad m)+    => CoinSelectionParameters i o+    -> ExceptT CoinSelectionError m (CoinSelectionResult i o)+payForOutputs params =+    case foldM payForOutput (utxoDescending, mempty) outputsDescending of+        Just (utxoRemaining, selection) ->+            pure $ CoinSelectionResult selection $ coinMapFromList utxoRemaining+        Nothing ->+            throwE errorCondition+  where+    errorCondition+      | amountAvailable < amountRequested =+          InputValueInsufficient $+              InputValueInsufficientError+                  amountAvailable amountRequested+      | utxoCount < outputCount =+          InputCountInsufficient $+              InputCountInsufficientError+                  utxoCount outputCount+      | utxoCount <= inputCountMax =+          InputsExhausted+              InputsExhaustedError+      | otherwise =+          InputLimitExceeded $+              InputLimitExceededError $+                  fromIntegral inputCountMax+    amountAvailable =+        coinMapValue $ inputsAvailable params+    amountRequested =+        coinMapValue $ outputsRequested params+    inputCountMax = fromIntegral+        $ calculateLimit (limit params)+        $ fromIntegral outputCount+    outputCount =+        fromIntegral $ length $ coinMapToList $ outputsRequested params+    outputsDescending =+        L.sortOn (Down . entryValue) $ coinMapToList $ outputsRequested params+    utxoCount =+        fromIntegral $ L.length $ coinMapToList $ inputsAvailable params+    utxoDescending =+        take (fromIntegral inputCountMax)+            $ L.sortOn (Down . entryValue)+            $ coinMapToList+            $ inputsAvailable params++-- | Attempts to pay for a /single transaction output/ by selecting the+--   /smallest possible/ number of entries from the /head/ of the given+--   UTxO list.+--+-- Returns a /reduced/ list of UTxO entries, and a coin selection that is+-- /updated/ to include the payment.+--+-- If the total value of entries in the given UTxO list is /less than/ the+-- required output amount, this function will return 'Nothing'.+--+payForOutput+    :: forall i o . (Ord i, Ord o)+    => ([CoinMapEntry i], CoinSelection i o)+    -> CoinMapEntry o+    -> Maybe ([CoinMapEntry i], CoinSelection i o)+payForOutput (utxoAvailable, currentSelection) out =+    coverTarget utxoAvailable mempty+  where+    coverTarget+        :: [CoinMapEntry i]+        -> [CoinMapEntry i]+        -> Maybe ([CoinMapEntry i], CoinSelection i o)+    coverTarget utxoRemaining utxoSelected+        | valueSelected >= valueTarget = Just+            -- We've selected enough to cover the target, so stop here.+            ( utxoRemaining+            , currentSelection <> CoinSelection+                { inputs  = coinMapFromList utxoSelected+                , outputs = coinMapFromList [out]+                , change  = filter (> C.zero)+                    (F.toList $ valueSelected `C.sub` valueTarget)+                }+            )+        | otherwise =+            -- We haven't yet selected enough to cover the target, so attempt+            -- to select a little more and then continue.+            case utxoRemaining of+                utxoEntry : utxoRemaining' ->+                    coverTarget utxoRemaining' (utxoEntry : utxoSelected)+                [] ->+                    -- The UTxO has been exhausted, so stop here.+                    Nothing+      where+        valueTarget+            = entryValue out+        valueSelected+            = sumEntries utxoSelected++sumEntries :: [CoinMapEntry a] -> Coin+sumEntries entries = mconcat $ entryValue <$> entries
+ src/library/Cardano/CoinSelection/Algorithm/Migration.hs view
@@ -0,0 +1,239 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_HADDOCK prune #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- This module contains an algorithm for migrating all funds from one wallet+-- to another.+--+-- See 'selectCoins'.+--+module Cardano.CoinSelection.Algorithm.Migration+    (+      -- * Coin Selection for Migration+      selectCoins+    , BatchSize (..)+    , idealBatchSize+    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMap+    , CoinMapEntry (..)+    , CoinSelection (..)+    , CoinSelectionLimit (..)+    , coinMapFromList+    , coinMapToList+    , sumChange+    , sumInputs+    )+import Cardano.CoinSelection.Fee+    ( DustThreshold (..)+    , Fee (..)+    , FeeBalancingPolicy (..)+    , FeeEstimator (..)+    , FeeOptions (..)+    )+import Control.Monad.Trans.State+    ( State, evalState, get, put )+import Data.List.NonEmpty+    ( NonEmpty ((:|)) )+import Data.Maybe+    ( fromMaybe, mapMaybe )+import Data.Word+    ( Word16 )+import GHC.Generics+    ( Generic )+import Internal.Coin+    ( Coin, coinFromIntegral, coinToIntegral )++import qualified Internal.Coin as C++--------------------------------------------------------------------------------+-- Coin Selection for Migration+--------------------------------------------------------------------------------++-- | Creates a __series__ of coin selections that, when published as+--   transactions, will have the effect of migrating all funds from one+--   wallet to another.+--+-- Since UTxO-based blockchains typically impose limits on the sizes of+-- individual transactions, and since individual UTxO sets can contain+-- /arbitrarily/ many entries, migrating all funds from one wallet to another+-- may require the creation of /several/ transactions.+--+-- This function therefore /partitions/ the given set of inputs into multiple+-- /batches/ of up to __/b/__ inputs, where __/b/__ is specified by the given+-- 'BatchSize' parameter. (See 'idealBatchSize' for an automatic way to+-- calculate a suitable batch size.)+--+-- For each batch of inputs, this function creates a separate 'CoinSelection'+-- with the given 'inputs' /and/ a generated 'change' set, where the 'change'+-- set represents the value to be transferred to the target wallet, carefully+-- adjusted to deduct a fee in accordance with the given 'FeeOptions'+-- parameter. The set of 'outputs' for each coin selection is /purposefully/+-- left empty, as /all/ value is captured in the 'change' set.+--+-- @since 1.0.0+selectCoins+    :: forall i o . (Ord i, Ord o)+    => FeeOptions i o+        -- ^ The fee options.+    -> BatchSize+        -- ^ The maximum number of inputs to include in each selection.+    -> CoinMap i+        -- ^ The UTxO set to migrate.+    -> [CoinSelection i o]+selectCoins options (BatchSize batchSize) utxo =+    evalState migrate (coinMapToList utxo)+  where+    FeeOptions {dustThreshold, feeEstimator, feeBalancingPolicy} = options++    migrate :: State [CoinMapEntry i] [CoinSelection i o]+    migrate = do+        batch <- getNextBatch+        if null batch then+            pure []+        else case adjustForFee (mkCoinSelection batch) of+            Nothing -> pure []+            Just coinSel -> do+                rest <- migrate+                pure (coinSel:rest)++    -- Construct a provisional 'CoinSelection' from the given selected inputs.+    -- Note that the selection may look a bit weird at first sight as it has+    -- no outputs (we are paying everything to ourselves!).+    mkCoinSelection :: [CoinMapEntry i] -> CoinSelection i o+    mkCoinSelection inps = CoinSelection+        { inputs = coinMapFromList inps+        , outputs = mempty+        , change =+            let chgs = mapMaybe (noDust . entryValue) inps+            in if null chgs then [threshold] else chgs+        }+      where+        threshold = unDustThreshold dustThreshold+        noDust :: Coin -> Maybe Coin+        noDust c+            | c < threshold = Nothing+            | otherwise = Just c++    -- | Attempt to balance the coin selection by reducing or increasing the+    -- change values based on the computed fees.+    adjustForFee :: CoinSelection i o -> Maybe (CoinSelection i o)+    adjustForFee !coinSel = case change coinSel of+        -- If there's no change, nothing to adjust+        [] -> Nothing++        -- No difference between required and computed, we're done+        (_ : _) | diff == 0 -> Just coinSel++        -- Otherwise, we have 2 cases:+        --+        -- 1/ diff < 0+        -- We aren't giving enough as fee, so we need to reduce one output.+        --+        -- 2/ diff > 0+        -- We have some surplus so we add it to an arbitrary output+        --+        -- If both cases we can simply modify one output by adding `diff`, the+        -- sign of `diff` making for the right modification.+        -- We then recursively call ourselves for this might reduce the number+        -- of outputs and change the fee.+        (c : cs) -> do+            let coinSel' = coinSel+                    { change = modifyFirst (c :| cs) (applyDiff diff) }+            let costOfSurplus+                    = fromIntegral+                    $ C.coinToNatural+                    $ C.distance+                        (unFee $ estimateFee feeEstimator coinSel')+                        (unFee $ estimateFee feeEstimator coinSel )+            if+                -- Adding the change costs less than not having it, so it's+                -- worth trying.+                | costOfSurplus < actualFee ->+                    adjustForFee coinSel'++                -- Adding the change costs more than not having it, If we don't+                -- require strict balancing, we can leave the selection as-is.+                | feeBalancingPolicy == RequireMinimalFee ->+                    pure coinSel++                -- Adding the change costs more than not having it. So,+                -- depending on our balancing policy, we may stop the balancing+                -- right here, or, if we must balance the selection discard the+                -- whole selection: it can't be balanced with this algorithm.+                --+                -- Note that this last extreme case is reached when using an+                -- unstable fee policy (where values of outputs can influence+                -- the policy) AND, require transactions to be 100% balanced.+                -- This is a silly thing to do.+                | otherwise ->+                    Nothing+      where+        applyDiff :: Integer -> Coin -> Coin+        applyDiff i c+            = fromMaybe C.zero+            $ coinFromIntegral (i + coinToIntegral c)++        diff :: Integer+        diff = actualFee - requiredFee+          where+            requiredFee+                = coinToIntegral $ unFee+                $ estimateFee feeEstimator coinSel++        actualFee :: Integer+        actualFee+            = coinToIntegral (sumInputs coinSel)+            - coinToIntegral (sumChange coinSel)++    -- | Apply the given function to the first coin of the list. If the+    -- operation makes the 'Coin' smaller than the dust threshold, the coin is+    -- discarded.+    modifyFirst :: NonEmpty Coin -> (Coin -> Coin) -> [Coin]+    modifyFirst (c :| cs) op+        | c' <= threshold = cs+        | otherwise = c' : cs+      where+        c' = op c+        threshold = unDustThreshold dustThreshold++    getNextBatch :: State [a] [a]+    getNextBatch = do+        xs <- get+        let (batch, rest) = splitAt (fromIntegral batchSize) xs+        put rest+        pure batch++-- | An upper limit for the number of 'inputs' to include in each coin selection+--   generated by 'selectCoins'.+--+-- @since 1.0.0+newtype BatchSize = BatchSize Word16+    deriving (Eq, Generic, Ord, Show)++-- | Calculate an ideal batch size based on the given coin selection limit.+--+-- @since 1.0.0+idealBatchSize :: CoinSelectionLimit -> BatchSize+idealBatchSize coinselOpts = BatchSize $ fixPoint 1+  where+    fixPoint :: Word16 -> Word16+    fixPoint !n+        | maxN n <= n = n+        | n == maxBound = n+        | otherwise = fixPoint (n + 1)+      where+        maxN :: Word16 -> Word16+        maxN = calculateLimit coinselOpts
+ src/library/Cardano/CoinSelection/Algorithm/RandomImprove.hs view
@@ -0,0 +1,421 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- This module contains an implementation of the __Random-Improve__ coin+-- selection algorithm.+--+module Cardano.CoinSelection.Algorithm.RandomImprove+    ( randomImprove+    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMap (..)+    , CoinMapEntry (..)+    , CoinSelection (..)+    , CoinSelectionAlgorithm (..)+    , CoinSelectionError (..)+    , CoinSelectionLimit (..)+    , CoinSelectionParameters (..)+    , CoinSelectionResult (..)+    , InputCountInsufficientError (..)+    , InputLimitExceededError (..)+    , InputValueInsufficientError (..)+    , InputsExhaustedError (..)+    , coinMapFromList+    , coinMapRandomEntry+    , coinMapToList+    , coinMapValue+    )+import Control.Monad+    ( foldM )+import Control.Monad.Trans.Class+    ( lift )+import Control.Monad.Trans.Except+    ( ExceptT (..), throwE )+import Control.Monad.Trans.Maybe+    ( MaybeT (..), runMaybeT )+import Crypto.Random.Types+    ( MonadRandom )+import Data.Ord+    ( Down (..) )+import Internal.Coin+    ( Coin )++import qualified Data.List as L+import qualified Internal.Coin as C++-- | An implementation of the __Random-Improve__ coin selection algorithm.+--+-- = Overview+--+-- The __Random-Improve__ coin selection algorithm works in __two phases__, by+-- /first/ selecting UTxO entries /at random/ to pay for each of the given+-- outputs, and /then/ attempting to /improve/ upon each of the selections.+--+-- === Phase 1: Random Selection+--+-- __In this phase, the algorithm randomly selects a minimal set of UTxO__+-- __entries to pay for each of the given outputs.__+--+-- During this phase, the algorithm:+--+--   *  processes outputs in /descending order of coin value/.+--+--   *  maintains a /remaining UTxO set/, initially equal to the given+--      /UTxO set/ parameter.+--+-- For each output of value __/v/__, the algorithm /randomly/ selects entries+-- from the /remaining UTxO set/, until the total value of selected entries is+-- greater than or equal to __/v/__. The selected entries are then associated+-- with that output, and removed from the /remaining UTxO set/.+--+-- This phase ends when every output has been associated with a selection of+-- UTxO entries.+--+-- However, if the remaining UTxO set is completely exhausted before all+-- outputs can be processed, the algorithm terminates with an error.+--+-- === Phase 2: Improvement+--+-- __In this phase, the algorithm attempts to improve upon each of the UTxO__+-- __selections made in the previous phase, by conservatively expanding the__+-- __selection made for each output.__+--+-- During this phase, the algorithm:+--+--   *  processes outputs in /ascending order of coin value/.+--+--   *  continues to maintain the /remaining UTxO set/ produced by the previous+--      phase.+--+--   *  maintains an /accumulated coin selection/, which is initially /empty/.+--+-- For each output of value __/v/__, the algorithm:+--+--  1.  __Calculates a /target range/__ for the total value of inputs used to+--      pay for that output, defined by the triplet:+--+--      (/minimum/, /ideal/, /maximum/) = (/v/, /2v/, /3v/)+--+--  2.  __Attempts to /improve/ upon the /existing UTxO selection/__ for that+--      output, by repeatedly selecting additional entries at random from the+--      /remaining UTxO set/, stopping when the selection can be improved upon+--      no further.+--+--      A selection with value /v1/ is considered to be an /improvement/ over a+--      selection with value /v0/ if __all__ of the following conditions are+--      satisfied:+--+--       * __Condition 1__: we have moved closer to the /ideal/ value:+--+--             abs (/ideal/ − /v1/) < abs (/ideal/ − /v0/)+--+--       * __Condition 2__: we have not exceeded the /maximum/ value:+--+--             /v1/ ≤ /maximum/+--+--       * __Condition 3__: when counting cumulatively across all outputs+--       considered so far, we have not selected more than the /maximum/ number+--       of UTxO entries specified by 'limit'.+--+--  3.  __Creates a /change value/__ for the output, equal to the total value+--      of the /final UTxO selection/ for that output minus the value /v/ of+--      that output.+--+--  4.  __Updates the /accumulated coin selection/__:+--+--       * Adds the /output/ to 'outputs'.+--       * Adds the /improved UTxO selection/ to 'inputs'.+--       * Adds the /change value/ to 'change'.+--+-- This phase ends when every output has been processed, __or__ when the+-- /remaining UTxO set/ has been exhausted, whichever occurs sooner.+--+-- = Termination+--+-- When both phases are complete, the algorithm terminates.+--+-- The /accumulated coin selection/ and /remaining UTxO set/ are returned to+-- the caller.+--+-- === Failure Modes+--+-- The algorithm terminates with an __error__ if:+--+--  1.  The /total value/ of the initial UTxO set (the amount of money+--      /available/) is /less than/ the total value of the output list (the+--      amount of money /required/).+--+--      See: __'InputValueInsufficientError'__.+--+--  2.  The /number/ of entries in the initial UTxO set is /smaller than/ the+--      number of requested outputs.+--+--      Due to the nature of the algorithm, /at least one/ UTxO entry is+--      required /for each/ output.+--+--      See: __'InputCountInsufficientError'__.+--+--  3.  Due to the particular /distribution/ of values within the initial UTxO+--      set, the algorithm depletes all entries from the UTxO set /before/ it+--      is able to pay for all requested outputs.+--+--      See: __'InputsExhaustedError'__.+--+--  4.  The /number/ of UTxO entries needed to pay for the requested outputs+--      would /exceed/ the upper limit specified by 'limit'.+--+--      See: __'InputLimitExceededError'__.+--+-- = Motivating Principles+--+-- There are several motivating principles behind the design of the algorithm.+--+-- === Principle 1: Dust Management+--+-- The probability that random selection will choose dust entries from a UTxO+-- set increases with the proportion of dust in the set.+--+-- Therefore, for a UTxO set with a large amount of dust, there's a high+-- probability that a random subset will include a large amount of dust.+--+-- === Principle 2: Change Management+--+-- Ideally, coin selection algorithms should, over time, create a UTxO set that+-- has /useful/ outputs: outputs that will allow us to process future payments+-- with a minimum number of inputs.+--+-- If for each payment request of value __/v/__ we create a change output of+-- /roughly/ the same value __/v/__, then we will end up with a distribution of+-- change values that matches the typical value distribution of payment+-- requests.+--+-- === Principle 3: Performance Management+--+-- Searching the UTxO set for additional entries to improve our change outputs+-- is /only/ useful if the UTxO set contains entries that are sufficiently+-- small enough. But it is precisely when the UTxO set contains many small+-- entries that it is less likely for a randomly-chosen UTxO entry to push the+-- total above the upper bound.+--+-- @since 1.0.0+randomImprove+    :: (Ord i, Ord o, MonadRandom m)+    => CoinSelectionAlgorithm i o m+randomImprove = CoinSelectionAlgorithm payForOutputs++payForOutputs+    :: (Ord i, Ord o, MonadRandom m)+    => CoinSelectionParameters i o+    -> ExceptT CoinSelectionError m (CoinSelectionResult i o)+payForOutputs params = do+    mRandomSelections <- lift $ runMaybeT $ foldM makeRandomSelection+        (inputCountMax, inputsAvailable params, []) outputsDescending+    case mRandomSelections of+        Just (inputCountRemaining, utxoRemaining, randomSelections) -> do+            (_, finalSelection, utxoRemaining') <- lift $ foldM+                improveSelection+                    (inputCountRemaining, mempty, utxoRemaining)+                    (reverse randomSelections)+            pure $ CoinSelectionResult finalSelection utxoRemaining'+        Nothing ->+            throwE errorCondition+  where+    errorCondition+      | amountAvailable < amountRequested =+          InputValueInsufficient $+              InputValueInsufficientError+                  amountAvailable amountRequested+      | utxoCount < outputCount =+          InputCountInsufficient $+              InputCountInsufficientError+                  utxoCount outputCount+      | utxoCount <= fromIntegral inputCountMax =+          InputsExhausted+              InputsExhaustedError+      | otherwise =+          InputLimitExceeded $+              InputLimitExceededError $+                  fromIntegral inputCountMax+    amountAvailable =+        coinMapValue $ inputsAvailable params+    amountRequested =+        coinMapValue $ outputsRequested params+    inputCountMax =+        fromIntegral $ calculateLimit (limit params) $ fromIntegral outputCount+    outputCount =+        fromIntegral $ length $ coinMapToList $ outputsRequested params+    outputsDescending =+        L.sortOn (Down . entryValue) $ coinMapToList $ outputsRequested params+    utxoCount =+        fromIntegral $ L.length $ coinMapToList $ inputsAvailable params++-- | Randomly select entries from the given UTxO set, until the total value of+--   selected entries is greater than or equal to the given output value.+--+-- Once a random selection has been made that meets the above criterion, this+-- function returns that selection as is, making no attempt to improve upon+-- the selection in any way.+--+makeRandomSelection+    :: forall i o m . MonadRandom m+    => (Integer, CoinMap i, [([CoinMapEntry i], CoinMapEntry o)])+    -> CoinMapEntry o+    -> MaybeT m (Integer, CoinMap i, [([CoinMapEntry i], CoinMapEntry o)])+makeRandomSelection+    (inputCountRemaining, utxoRemaining, existingSelections) txout = do+        (utxoSelected, utxoRemaining') <- coverRandomly ([], utxoRemaining)+        return+            ( inputCountRemaining - fromIntegral (L.length utxoSelected)+            , utxoRemaining'+            , (utxoSelected, txout) : existingSelections+            )+  where+    coverRandomly+        :: ([CoinMapEntry i], CoinMap i)+        -> MaybeT m ([CoinMapEntry i], CoinMap i)+    coverRandomly (selected, remaining)+        | L.length selected > fromIntegral inputCountRemaining =+            MaybeT $ return Nothing+        | sumEntries selected >= targetMin (mkTargetRange txout) =+            MaybeT $ return $ Just (selected, remaining)+        | otherwise =+            utxoPickRandomT remaining >>= \(picked, remaining') ->+                coverRandomly (picked : selected, remaining')++-- | Perform an improvement to random selection on a given output.+improveSelection+    :: forall i o m . (MonadRandom m, Ord i, Ord o)+    => (Integer, CoinSelection i o, CoinMap i)+    -> ([CoinMapEntry i], CoinMapEntry o)+    -> m (Integer, CoinSelection i o, CoinMap i)+improveSelection (maxN0, selection, utxo0) (inps0, txout) = do+    (maxN, inps, utxo) <- improve (maxN0, inps0, utxo0)+    return+        ( maxN+        , selection <> CoinSelection+            { inputs = coinMapFromList inps+            , outputs = coinMapFromList [txout]+            , change = mkChange txout inps+            }+        , utxo+        )+  where+    target = mkTargetRange txout++    improve+        :: (Integer, [CoinMapEntry i], CoinMap i)+        -> m (Integer, [CoinMapEntry i], CoinMap i)+    improve (maxN, inps, utxo)+        | maxN >= 1 && sumEntries inps < targetAim target = do+            runMaybeT (utxoPickRandomT utxo) >>= \case+                Nothing ->+                    return (maxN, inps, utxo)+                Just (io, utxo') | isImprovement io inps -> do+                    let inps' = io : inps+                    let maxN' = maxN - 1+                    improve (maxN', inps', utxo')+                Just _ ->+                    return (maxN, inps, utxo)+        | otherwise =+            return (maxN, inps, utxo)++    isImprovement :: CoinMapEntry i -> [CoinMapEntry i] -> Bool+    isImprovement io selected =+        let+            condA = -- (a) It doesn’t exceed a specified upper limit.+                sumEntries (io : selected) < targetMax target++            condB = -- (b) Addition gets us closer to the ideal change+                distanceA < distanceB+              where+                distanceA = C.distance+                    (targetAim target)+                    (sumEntries (io : selected))+                distanceB = C.distance+                    (targetAim target)+                    (sumEntries selected)++            -- (c) Doesn't exceed maximum number of inputs+            -- Guaranteed by the precondition on 'improve'.+        in+            condA && condB++--------------------------------------------------------------------------------+-- Internals+--------------------------------------------------------------------------------++-- | Represents a target range of /total input values/ for a given output.+--+-- In this context, /total input value/ refers to the total value of a set of+-- inputs selected to pay for a given output.+--+data TargetRange = TargetRange+    { targetMin :: Coin+        -- ^ The minimum value, corresponding to exactly the requested target+        -- amount, and a change amount of zero.+    , targetAim :: Coin+        -- ^ The ideal value, corresponding to exactly twice the requested+        -- target amount, and a change amount equal to the requested amount.+    , targetMax :: Coin+        -- ^ The maximum value, corresponding to exactly three times the+        -- requested amount, and a change amount equal to twice the requested+        -- amount.+    }++-- | Compute the target range of /total input values/ for a given output.+--+-- See 'TargetRange'.+--+mkTargetRange :: CoinMapEntry o -> TargetRange+mkTargetRange (CoinMapEntry _ c) = TargetRange+    { targetMin = c+    , targetAim = c `C.add` c+    , targetMax = c `C.add` c `C.add` c+    }++-- | Re-wrap 'utxoPickRandom' in a 'MaybeT' monad+utxoPickRandomT+    :: MonadRandom m+    => CoinMap i+    -> MaybeT m (CoinMapEntry i, CoinMap i)+utxoPickRandomT =+    MaybeT+        . fmap (\(mi, u) -> (, u) <$> mi)+        . coinMapRandomEntry++-- | Compute change outputs from a target output and a selection of inputs.+--+-- Pre-condition:+--+-- The output must be less than (or equal to) the sum of the inputs.+--+mkChange :: CoinMapEntry o -> [CoinMapEntry i] -> [Coin]+mkChange (CoinMapEntry _ out) inps =+    case difference of+        Nothing ->+            error $ mconcat+                [ "mkChange: "+                , "output must be less than or equal to sum of inputs"+                ]+        Just d | C.isZero d ->+            []+        Just d ->+            [d]+  where+    difference = sumEntries inps `C.sub` out++--------------------------------------------------------------------------------+-- Utilities+--------------------------------------------------------------------------------++sumEntries :: [CoinMapEntry i] -> Coin+sumEntries = mconcat . fmap entryValue
+ src/library/Cardano/CoinSelection/Fee.hs view
@@ -0,0 +1,713 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_HADDOCK prune #-}++-- |+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+-- Provides functionality for __adjusting__ coin selections in order to pay for+-- transaction __fees__.+--+module Cardano.CoinSelection.Fee+    (+      -- * Fundamental Types+      Fee (..)+    , FeeEstimator (..)++      -- * Fee Adjustment+    , adjustForFee+    , FeeOptions (..)+    , FeeBalancingPolicy (..)+    , FeeAdjustmentError (..)++      -- * Dust Processing+    , DustThreshold (..)+    , coalesceDust++      -- # Internal Functions+    , calculateFee+    , distributeFee+    , reduceChangeOutputs+    , splitCoin++    ) where++import Prelude hiding+    ( round )++import Cardano.CoinSelection+    ( CoinMap (..)+    , CoinMapEntry (..)+    , CoinSelection (..)+    , coinMapFromList+    , coinMapRandomEntry+    , sumChange+    , sumInputs+    , sumOutputs+    )+import Control.Monad.Trans.Class+    ( lift )+import Control.Monad.Trans.Except+    ( ExceptT (..), except, throwE )+import Control.Monad.Trans.State+    ( StateT (..), evalStateT )+import Crypto.Random.Types+    ( MonadRandom )+import Data.Function+    ( (&) )+import Data.List.NonEmpty+    ( NonEmpty ((:|)) )+import Data.Maybe+    ( fromMaybe )+import Data.Ord+    ( Down (..), comparing )+import Data.Ratio+    ( (%) )+import GHC.Generics+    ( Generic )+import Internal.Coin+    ( Coin )+import Internal.Invariant+    ( invariant )+import Internal.Rounding+    ( RoundingDirection (..), round )+import Quiet+    ( Quiet (Quiet) )++import qualified Data.Foldable as F+import qualified Data.List.NonEmpty as NE+import qualified Internal.Coin as C++--------------------------------------------------------------------------------+-- Fundamental Types+--------------------------------------------------------------------------------++-- | Represents a non-negative fee to be paid on a transaction.+--+-- @since 1.0.0+newtype Fee = Fee { unFee :: Coin }+    deriving newtype (Monoid, Semigroup)+    deriving stock (Eq, Generic, Ord)+    deriving Show via (Quiet Fee)++-- | Defines the /maximum/ size of a __dust coin__.+--+-- Functions that accept a 'DustThreshold' argument will generally exclude+-- values that are /less than or equal to/ this threshold from the 'change'+-- sets of generated selections, /coalescing/ such coins together into larger+-- coins that /exceed/ the threshold.+--+-- Specifying a dust threshold of __/n/__ causes all coins that are less than+-- or equal to __/n/__ to be treated as dust and coalesced together.+--+-- Specifying a dust threshold of __0__ completely /disables/ dust elimination+-- with the exception of zero-valued coins, which will always be eliminated.+--+-- See 'coalesceDust'.+--+-- @since 1.0.0+newtype DustThreshold = DustThreshold { unDustThreshold :: Coin }+    deriving stock (Eq, Generic, Ord)+    deriving Show via (Quiet DustThreshold)++-- | Provides a function capable of __estimating__ the transaction fee required+--   for a given coin selection, according to the rules of a particular+--   blockchain.+--+-- The fee estimate should be a function of the __current__ memberships of the+-- 'inputs', 'outputs', and 'change' sets.+--+-- Depending on the rules of the blockchain under consideration, the fee+-- estimate may take either (or both) of the following factors into account:+--+--   - the number of entries in each set;+--   - the coin value of each set member.+--+-- A fee estimate may differ from the final fee required for a selection, as+-- fees are generally paid for by /adjusting/ a given selection to make a /new/+-- selection. See 'adjustForFee' for more details of this process.+--+-- @since 1.0.0+newtype FeeEstimator i o = FeeEstimator+    { estimateFee :: CoinSelection i o -> Fee+    } deriving Generic++--------------------------------------------------------------------------------+-- Fee Adjustment+--------------------------------------------------------------------------------++-- | Provides options for fee adjustment.+--+-- @since 1.0.0+data FeeOptions i o = FeeOptions+    { feeEstimator+        :: FeeEstimator i o+        -- ^ Estimate fees based on selected inputs and requested outputs.++    , dustThreshold+        :: DustThreshold+        -- ^ The threshold to use for dust elimination. Specifying a threshold+        -- of zero will disable dust elimination. See 'DustThreshold' for more+        -- details.++    , feeBalancingPolicy+        :: FeeBalancingPolicy+        -- ^ Which fee balancing policy to use.+    } deriving Generic++-- | A choice of fee balancing policies for use when adjusting a coin selection.+--+-- == Background+--+-- A coin selection __'s'__ is said to have a /perfectly-balanced/ fee when it+-- satisfies the following property:+--+-- >>> sumInputs s = sumOutputs s + sumChange s + estimateFee s+--+-- Conversely, a selection is said to have an /unbalanced/ fee when it+-- satisfies the following property:+--+-- >>> sumInputs s > sumOutputs s + sumChange s + estimateFee s+--+-- In other words, if a coin selection has an /unbalanced/ fee, the /effective/+-- fee is greater than the minimum fee /actually required/ by the blockchain.+--+-- == Balanced Fees vs Minimal Fees+--+-- Some blockchains /require /that fees are always /perfectly-balanced/.+--+-- However, for blockchains that allow /unbalanced/ fees, it is sometimes+-- possible to /save money/ by generating a coin selection with an unbalanced+-- fee. This may seem counterintuitive at first, but consider an individual+-- change ouput __/c/__ of value __/v/__. If the /marginal fee/ __/f/__+-- associated with __/c/__ is greater than its value __/v/__, then we will+-- /save money/ by __not__ including __/c/__ within 'change'.+--+-- There are two policy choices available for handling change values with+-- marginal fees greater than their value:+--+--   - For blockchains that __allow__ transactions with /unbalanced/ fees,+--     specifying the 'RequireMinimalFee' policy will allow money to be saved by+--     /excluding/ change outputs that have a marginal fee greater than+--     their value.+--+--   - For blockchains that do __not__ allow transactions with /unbalanced/+--     fees, specifying the 'RequireBalancedFee' policy will always generate+--     selections with fees that are perfectly-balanced, even if the resulting+--     fees are higher than could be achieved by allowing unbalanced fees.+--+data FeeBalancingPolicy+    = RequireBalancedFee+        -- ^ Generate selections with fees that are perfectly balanced, with the+        -- trade-off of allowing slightly higher fees.+    | RequireMinimalFee+        -- ^ Generate selections with the lowest fees possible, with the+        -- trade-off of allowing slightly imbalanced fees.+    deriving (Generic, Show, Eq)++-- | Represents the set of possible failures that can occur when adjusting a+--   'CoinSelection' with the 'adjustForFee' function.+--+data FeeAdjustmentError i o+    = CannotCoverFee Fee+    -- ^ Indicates that the given map of additional inputs was exhausted while+    --   attempting to select extra inputs to cover the required fee.+    --+    -- Records the shortfall (__/f/__ − __/s/__) between the required fee+    -- __/f/__ and the total value __/s/__ of currently-selected inputs.++    | CoinSelectionUnderfunded (CoinSelection i o)+    -- ^ Indicates that the given coin selection is __underfunded__: the total+    -- value of 'inputs' is less than the total value of 'outputs', as+    -- calculated by the 'CoinSelection.coinMapValue' function.+    deriving (Show, Eq)++-- | Adjusts the given 'CoinSelection' in order to pay for a __transaction__+--   __fee__, required in order to publish the selection as a transaction on+--   a blockchain.+--+-- == Background+--+-- Implementations of 'Cardano.CoinSelection.CoinSelectionAlgorithm' generally+-- produce coin selections that are /exactly balanced/, satisfying the+-- following equality:+--+-- >>> sumInputs s = sumOutputs s + sumChange s+--+-- In order to pay for a transaction fee, the above equality must be+-- transformed into an /inequality/:+--+-- >>> sumInputs s > sumOutputs s + sumChange s+--+-- The difference between these two sides represents value to be paid /by the/+-- /originator/ of the transaction, in the form of a fee:+--+-- >>> sumInputs s = sumOutputs s + sumChange s + fee+--+-- == The Adjustment Process+--+-- In order to generate a fee that is acceptable to the network, this function+-- adjusts the 'change' and 'inputs' of the given 'CoinSelection', consulting+-- the 'FeeEstimator' as a guide for how much the current selection would cost+-- to publish as a transaction on the network.+--+-- == Methods of Adjustment+--+-- There are two methods of adjustment possible:+--+--  1. The __'change'__ set can be /reduced/, either by:+--+--      a. completely removing a change value from the set; or by+--+--      b. reducing a change value to a lower value.+--+--  2. The __'inputs'__ set can be /augmented/, by selecting additional inputs+--     from the specified 'CoinMap' argument.+--+-- == Dealing with Dust Values+--+-- If, at any point, a change value is generated that is less than or equal+-- to the 'DustThreshold', this function will eliminate that change value+-- from the 'change' set, redistributing the eliminated value over the remaining+-- change values, ensuring that the total value of all 'change' is preserved.+--+-- See 'coalesceDust' for more details.+--+-- == Termination+--+-- Since adjusting a selection can affect the fee estimate produced by+-- 'estimateFee', the process of adjustment is an /iterative/ process.+--+-- The termination post-condition depends on the choice of+-- 'FeeBalancingPolicy':+--+--   - If 'RequireBalancedFee' is specified, this function terminates+--     only when it has generated a 'CoinSelection' __'s'__ that satisfies the+--     following property:+--+--         >>> sumInputs s = sumOutputs s + sumChange s + estimateFee s+--+--   - If 'RequireMinimalFee' policy is specified, the above /equality/+--     is relaxed to the following /inequality/:+--+--         >>> sumInputs s ≥ sumOutputs s + sumChange s + estimateFee s+--+-- See 'FeeBalancingPolicy' for more details.+--+-- @since 1.0.0+adjustForFee+    :: (Ord i, MonadRandom m)+    => FeeOptions i o+    -> CoinMap i+    -> CoinSelection i o+    -> ExceptT (FeeAdjustmentError i o) m (CoinSelection i o)+adjustForFee unsafeOpt utxo coinSel = do+    let opt = invariant+            "adjustForFee: fee must be non-null" unsafeOpt (not . nullFee)+    senderPaysFee opt utxo coinSel+  where+    nullFee opt = estimateFee (feeEstimator opt) coinSel == Fee C.zero++--------------------------------------------------------------------------------+-- Internal Functions+--------------------------------------------------------------------------------++-- Calculates the current fee associated with a given 'CoinSelection'.+--+-- If the result is less than zero, returns 'Nothing'.+--+calculateFee :: CoinSelection i o -> Maybe Fee+calculateFee s = Fee <$> sumInputs s `C.sub` (sumOutputs s `C.add` sumChange s)++-- The sender pays fee in this scenario, so fees are removed from the change+-- outputs, and new inputs are selected if necessary.+--+senderPaysFee+    :: forall i o m . (Ord i, MonadRandom m)+    => FeeOptions i o+    -> CoinMap i+    -> CoinSelection i o+    -> ExceptT (FeeAdjustmentError i o) m (CoinSelection i o)+senderPaysFee opts utxo sel =+    evalStateT (go sel) utxo+  where+    go+        :: CoinSelection i o+        -> StateT+            (CoinMap i)+            (ExceptT (FeeAdjustmentError i o) m)+            (CoinSelection i o)+    go coinSel@(CoinSelection inps outs chgs) = do+        -- Substract fee from change outputs, proportionally to their value.+        (coinSel', remFee) <- lift $ except $ reduceChangeOutputs opts coinSel++        -- Should the change cover the fee, we're (almost) good. By removing+        -- change outputs, we make them smaller and may reduce the size of the+        -- transaction, and the fee. Thus, we end up paying slightly more than+        -- the upper bound. We could do some binary search and try to+        -- re-distribute excess across changes until fee becomes bigger.+        if remFee == Fee C.zero+        then pure coinSel'+        else do+            -- Otherwise, we need an extra entries from the available utxo to+            -- cover what's left. Note that this entry may increase our change+            -- because we may not consume it entirely. So we will just split+            -- the extra change across all changes possibly increasing the+            -- number of change outputs (if there was none, or if increasing a+            -- change value causes an overflow).+            --+            -- Because selecting a new input increases the fee, we need to+            -- re-run the algorithm with this new elements and using the initial+            -- change plus the extra change brought up by this entry and see if+            -- we can now correctly cover fee.+            inps' <- coverRemainingFee remFee+            let extraChange = splitCoin (sumEntries inps') chgs+            go $ CoinSelection (inps <> coinMapFromList inps') outs extraChange++-- A short and simple version of the 'random' fee policy to cover for the fee+-- in the case where existing set of change is not enough.+--+coverRemainingFee+    :: MonadRandom m+    => Fee+    -> StateT (CoinMap i) (ExceptT (FeeAdjustmentError i o) m) [CoinMapEntry i]+coverRemainingFee (Fee fee) = go [] where+    go acc+        | sumEntries acc >= fee =+            return acc+        | otherwise = do+            -- We ignore the size of the fee, and just pick randomly+            StateT (lift . coinMapRandomEntry) >>= \case+                Just entry ->+                    go (entry : acc)+                Nothing ->+                    lift $ throwE $ CannotCoverFee $ Fee $+                        fee `C.distance` (sumEntries acc)++-- Pays for the given fee by subtracting it from the given list of change+-- outputs, so that each change output is reduced by a portion of the fee+-- that's in proportion to its relative size.+--+-- == Basic Examples+--+-- >>> reduceChangeOutputs (DustThreshold 0) (Fee 4) (Coin <$> [2, 2, 2, 2])+-- [Coin 1, Coin 1, Coin 1, Coin 1]+--+-- >>> reduceChangeOutputs (DustThreshold 0) (Fee 15) (Coin <$> [2, 4, 8, 16])+-- [Coin 1, Coin 2, Coin 4, Coin 8]+--+-- == Handling Dust+--+-- Any dust outputs in the resulting list are coalesced according to the given+-- dust threshold: (See 'coalesceDust'.)+--+-- >>> reduceChangeOutputs (DustThreshold 1) (Fee 4) (Coin <$> [2, 2, 2, 2])+-- [Coin 4]+--+-- == Handling Insufficient Change+--+-- If there's not enough change to pay for the fee, or if there's only just+-- enough to pay for it exactly, this function returns the /empty list/:+--+-- >>> reduceChangeOutputs (DustThreshold 0) (Fee 15) (Coin <$> [10])+-- []+--+-- >>> reduceChangeOutputs (DustThreshold 0) (Fee 15) (Coin <$> [1, 2, 4, 8])+-- []+--+reduceChangeOutputs+    :: FeeOptions i o+    -> CoinSelection i o+    -> Either (FeeAdjustmentError i o) (CoinSelection i o, Fee)+reduceChangeOutputs opts s = do+    -- The original requested fee amount+    let Fee φ_original = estimateFee (feeEstimator opts) s+    -- The initial amount left for fee (i.e. inputs - outputs)+    let mδ_original = sumInputs s `C.sub` (sumOutputs s `C.add` sumChange s)+    case mδ_original of+        -- selection is now balanced, nothing to do.+        Just δ_original | φ_original == δ_original -> do+            pure (s, Fee C.zero)++        -- some fee left to pay, but we've depleted all change outputs+        Just δ_original | φ_original > δ_original && null (change s) -> do+            let remainder = φ_original `C.distance` δ_original+            pure (s, Fee remainder)++        -- some fee left to pay, and we've haven't depleted all change yet+        Just δ_original | φ_original > δ_original && not (null (change s)) -> do+            let remainder = φ_original `C.distance` δ_original+            let chgs' = distributeFee (Fee remainder) (NE.fromList (change s))+                      & fmap payFee+                      & coalesceDust (dustThreshold opts)+            reduceChangeOutputs opts (s { change = chgs' })++        -- The current selection has a higher fee than necessary. This typically+        -- occurs if, after reducing an output to pay for the predicted fee, the+        -- required fee turns out to be less than originally predicted.+        -- The outcome depends on whether or not the node allows transactions+        -- to be unbalanced.+        Just δ_original | δ_original > φ_original -> do+            let extraChg = δ_original `C.distance` φ_original+            let sDangling = s { change = splitCoin extraChg (change s) }+            let Fee φ_dangling = estimateFee (feeEstimator opts) sDangling+            -- We have `δ_dangling = φ_original` by construction of sDangling.+            --+            -- Proof:+            --+            -- δ_dangling = Σi_dangling - (Σo_dangling + Σc_dangling)+            --            = Σi_original - (Σo_original + Σc_original + extraChg)+            --            = Σi_original - (Σo_original + Σc_original) - extraChg+            --            = δ_original - extraChg+            --            = δ_original - (δ_original - φ_original)+            --            = φ_original+            let δ_dangling = φ_original+            case φ_dangling `C.sub` δ_dangling of+                -- we've left too much, but adding a change output would be more+                -- expensive than not having it. Here we have two choices:+                --+                -- a) If the node allows unbalanced transaction, we can stop+                --    here and do nothing. We'll leave slightly more than what's+                --    needed for fees, but having an extra change output isn't+                --    worth it anyway.+                --+                -- b) If we __must__ balance the transaction, then we can choose+                --    to pay the extra cost by adding the change output and+                --    continue trying to balance the transaction (likely, by+                --    selecting another input).+                Just remainder | φ_dangling >= δ_original ->+                    case feeBalancingPolicy opts of+                        RequireMinimalFee ->+                            pure (s, Fee C.zero)+                        RequireBalancedFee ->+                            pure (sDangling, Fee remainder)++                -- If however, adding the dangling change doesn't cost more than+                -- not having it, we might as well add it to get the money and+                -- continue balancing!+                _otherwise ->+                    reduceChangeOutputs opts sDangling++        -- The only way to end-up here is if the user has provided an invalid+        -- selection where outputs are trying to spend more than inputs. This is+        -- simply forbidden.+        _Nothing ->+            Left (CoinSelectionUnderfunded s)++-- Distribute the given fee over the given list of coins, so that each coin+-- is allocated a __fraction__ of the fee in proportion to its relative size.+--+-- == Pre-condition+--+-- Every coin in the given list must be __non-zero__ in value.+--+-- == Examples+--+-- >>> distributeFee (Fee 2) [(Coin 1), (Coin 1)]+-- [(Fee 1, Coin 1), (Fee 1, Coin 1)]+--+-- >>> distributeFee (Fee 4) [(Coin 1), (Coin 1)]+-- [(Fee 2, Coin 1), (Fee 2, Coin 1)]+--+-- >>> distributeFee (Fee 7) [(Coin 1), (Coin 2), (Coin 4)]+-- [(Fee 1, Coin 1), (Fee 2, Coin 2), (Fee 4, Coin 4)]+--+-- >>> distributeFee (Fee 14) [(Coin 1), (Coin 2), (Coin 4)]+-- [(Fee 2, Coin 1), (Fee 4, Coin 2), (Fee 8, Coin 4)]+--+distributeFee :: Fee -> NonEmpty Coin -> NonEmpty (Fee, Coin)+distributeFee (Fee feeTotal) coinsUnsafe =+    NE.zip feesRounded coins+  where+    -- A list of coins that are non-zero in value.+    coins :: NonEmpty Coin+    coins =+        invariant "distributeFee: all coins must be non-zero in value."+        coinsUnsafe (C.zero `F.notElem`)++    -- A list of rounded fee portions, where each fee portion deviates from the+    -- ideal unrounded portion by as small an amount as possible.+    feesRounded :: NonEmpty Fee+    feesRounded+        -- 1. Start with the list of ideal unrounded fee portions for each coin:+        = feesUnrounded+        -- 2. Attach an index to each fee portion, so that we can remember the+        --    original order:+        & NE.zip indices+        -- 3. Sort the fees into descending order of their fractional parts:+        & NE.sortBy (comparing (Down . fractionalPart . snd))+        -- 4. Apply pre-computed roundings to each fee portion:+        --    * portions with the greatest fractional parts are rounded up;+        --    * portions with the smallest fractional parts are rounded down.+        & NE.zipWith (\roundDir (i, f) -> (i, round roundDir f)) feeRoundings+        -- 5. Restore the original order:+        & NE.sortBy (comparing fst)+        -- 6. Strip away the indices:+        & fmap snd+        -- 7. Transform results into fees:+        & fmap (Fee . fromMaybe C.zero . C.coinFromIntegral @Integer)+      where+        indices :: NonEmpty Int+        indices = 0 :| [1 ..]++    -- A list of rounding directions, one per fee portion.+    --+    -- Since the ideal fee portion for each coin is a rational value, we must+    -- therefore round each rational value either /up/ or /down/ to produce a+    -- final integer result.+    --+    -- However, we can't take the simple approach of either rounding /all/ fee+    -- portions down or rounding /all/ fee portions up, as this could cause the+    -- sum of fee portions to either undershoot or overshoot the original fee.+    --+    -- So in order to hit the fee exactly, we must round /some/ of the portions+    -- up, and /some/ of the portions down.+    --+    -- Fortunately, we can calculate exactly how many fee portions must be+    -- rounded up, by first rounding /all/ portions down, and then computing+    -- the /shortfall/ between the sum of the rounded-down portions and the+    -- original fee.+    --+    -- We return a list where all values of 'RoundUp' occur in a contiguous+    -- section at the start of the list, of the following form:+    --+    --     [RoundUp, RoundUp, ..., RoundDown, RoundDown, ...]+    --+    feeRoundings :: NonEmpty RoundingDirection+    feeRoundings =+        applyN feeShortfall (NE.cons RoundUp) (NE.repeat RoundDown)+      where+         -- The part of the total fee that we'd lose if we were to take the+         -- simple approach of rounding all ideal fee portions /down/.+        feeShortfall+            = C.coinToIntegral feeTotal+            - fromIntegral @Integer (F.sum $ round RoundDown <$> feesUnrounded)++    -- A list of ideal unrounded fee portions, with one fee portion per coin.+    --+    -- A coin's ideal fee portion is the rational portion of the total fee that+    -- corresponds to that coin's relative size when compared to other coins.+    feesUnrounded :: NonEmpty Rational+    feesUnrounded = calculateIdealFee <$> coins+      where+        calculateIdealFee c+            = C.coinToIntegral c+            * C.coinToIntegral feeTotal+            % C.coinToIntegral totalCoinValue++    -- The total value of all coins.+    totalCoinValue :: Coin+    totalCoinValue = F.fold coins++-- | From the given list of coins, remove dust coins with a value less than or+--   equal to the given threshold value, redistributing their total value over+--   the coins that remain.+--+-- This function satisfies the following properties:+--+-- >>> sum coins = sum (coalesceDust threshold coins)+-- >>> all (/= Coin 0) (coalesceDust threshold coins)+--+coalesceDust :: DustThreshold -> NonEmpty Coin -> [Coin]+coalesceDust (DustThreshold threshold) coins =+    splitCoin valueToDistribute coinsToKeep+  where+    (coinsToKeep, coinsToRemove) = NE.partition (> threshold) coins+    valueToDistribute = F.fold coinsToRemove++-- Splits up the given coin of value __@v@__, distributing its value over the+-- given coin list of length __@n@__, so that each coin value is increased by+-- an integral amount within unity of __@v/n@__, producing a new list of coin+-- values where the overall total is preserved.+--+-- == Basic Examples+--+-- When it's possible to divide a coin evenly, each coin value is increased by+-- the same integer amount:+--+-- >>> splitCoin (Coin 40) (Coin <$> [1, 1, 1, 1])+-- [Coin 11, Coin 11, Coin 11, Coin 11]+--+-- >>> splitCoin (Coin 40) (Coin <$> [1, 2, 3, 4])+-- [Coin 11, Coin 12, Coin 13, Coin 14]+--+-- == Handling Non-Uniform Increases+--+-- When it's not possible to divide a coin evenly, each integral coin value in+-- the resulting list is always within unity of the ideal unrounded result:+--+-- >>> splitCoin (Coin 2) (Coin <$> [1, 1, 1, 1])+-- [Coin 1, Coin 1, Coin 2, Coin 2]+--+-- >>> splitCoin (Coin 10) (Coin <$> [1, 1, 1, 1])+-- [Coin 3, Coin 3, Coin 4, Coin 4]+--+-- == Handling Empty Lists+--+-- If the given list is empty, this function returns a list with the original+-- given coin as its sole element:+--+-- >>> splitCoin (Coin 10) []+-- [Coin 10]+--+-- == Properties+--+-- The total value is always preserved:+--+-- >>> sum (splitCoin x ys) == x + sum ys+--+splitCoin :: Coin -> [Coin] -> [Coin]+splitCoin coinToSplit coinsToIncrease =+    case (mIncrement, mShortfall) of+        (Just increment, Just shortfall) ->+            zipWith C.add coinsToIncrease increments+          where+            increments = zipWith C.add majorIncrements minorIncrements+            majorIncrements = repeat increment+            minorIncrements = replicate (C.coinToIntegral shortfall) C.one+                <> repeat C.zero+        _ | coinToSplit > C.zero ->+            [coinToSplit]+        _ ->+            []+  where+    mCoinCount = length coinsToIncrease+    mIncrement = coinToSplit `C.div` mCoinCount+    mShortfall = coinToSplit `C.mod` mCoinCount++-- Extract the fractional part of a rational number.+--+-- Examples:+--+-- >>> fractionalPart (3 % 2)+-- 1 % 2+--+-- >>> fractionalPart (11 % 10)+-- 1 % 10+--+fractionalPart :: Rational -> Rational+fractionalPart = snd . properFraction @_ @Integer++-- Apply the same function multiple times to a value.+--+applyN :: Int -> (a -> a) -> a -> a+applyN n f = F.foldr (.) id (replicate n f)++-- Find the sum of a list of entries.+--+sumEntries :: [CoinMapEntry i] -> Coin+sumEntries = F.fold . fmap entryValue++-- | Reduce a coin value by a given fee amount. If fees are too big for+-- a single coin, returns a `Coin 0`.+payFee :: (Fee, Coin) -> Coin+payFee (Fee f, c) = fromMaybe C.zero (c `C.sub` f)
+ src/test/Cardano/CoinSelection/Algorithm/LargestFirstSpec.hs view
@@ -0,0 +1,247 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Cardano.CoinSelection.Algorithm.LargestFirstSpec+    ( spec+    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMap (..)+    , CoinMapEntry (..)+    , CoinSelection (..)+    , CoinSelectionAlgorithm (..)+    , CoinSelectionError (..)+    , CoinSelectionLimit (..)+    , CoinSelectionParameters (..)+    , CoinSelectionResult (..)+    , InputCountInsufficientError (..)+    , InputLimitExceededError (..)+    , InputValueInsufficientError (..)+    , InputsExhaustedError (..)+    , coinMapToList+    )+import Cardano.CoinSelection.Algorithm.LargestFirst+    ( largestFirst )+import Cardano.CoinSelectionSpec+    ( CoinSelProp (..)+    , CoinSelectionFixture (..)+    , CoinSelectionTestResult (..)+    , coinSelectionUnitTest+    )+import Cardano.Test.Utilities+    ( Address, TxIn, excluding, unsafeCoin )+import Control.Monad+    ( unless )+import Control.Monad.Trans.Except+    ( runExceptT )+import Data.Either+    ( isRight )+import Data.Functor.Identity+    ( Identity (runIdentity) )+import Test.Hspec+    ( Spec, describe, it, shouldSatisfy )+import Test.QuickCheck+    ( Property, property, (==>) )++import qualified Data.List as L+import qualified Data.Map.Strict as Map+import qualified Data.Set as Set++spec :: Spec+spec = do+    describe "Coin selection: largest-first algorithm: unit tests" $ do++        coinSelectionUnitTest largestFirst ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [17]+                , rsChange = []+                , rsOutputs = [17]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [10,10,17]+                , txOutputs = [17]+                })++        coinSelectionUnitTest largestFirst ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [17]+                , rsChange = [16]+                , rsOutputs = [1]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [1]+                })++        coinSelectionUnitTest largestFirst ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [12, 17]+                , rsChange = [11]+                , rsOutputs = [18]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [18]+                })++        coinSelectionUnitTest largestFirst ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [10, 12, 17]+                , rsChange = [9]+                , rsOutputs = [30]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [30]+                })++        coinSelectionUnitTest largestFirst ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [6,10,5]+                , rsChange = [5,4]+                , rsOutputs = [11,1]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 3+                , utxoInputs = [1,2,10,6,5]+                , txOutputs = [11, 1]+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance not sufficient"+            (Left $ InputValueInsufficient $ InputValueInsufficientError+                (unsafeCoin @Int 39) (unsafeCoin @Int 40))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [40]+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance not sufficient, and not fragmented enough"+            (Left $ InputValueInsufficient $ InputValueInsufficientError+                (unsafeCoin @Int 39) (unsafeCoin @Int 43))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [40,1,1,1]+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance sufficient, but not fragmented enough"+            (Left $ InputCountInsufficient $ InputCountInsufficientError 3 4)+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,20,17]+                , txOutputs = [40,1,1,1]+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance sufficient, fragmented enough, but single output \+            \depletes all UTxO entries"+            (Left (InputsExhausted InputsExhaustedError))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,20,17]+                , txOutputs = [40, 1]+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance sufficient, fragmented enough, but single output \+            \depletes all UTxO entries"+            (Left (InputsExhausted InputsExhaustedError))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [20,20,10,5]+                , txOutputs = [41, 6]+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance sufficient, fragmented enough, but maximum input \+            \count exceeded"+            (Left $ InputLimitExceeded $ InputLimitExceededError 9)+            (CoinSelectionFixture+                { maxNumOfInputs = 9+                , utxoInputs = replicate 100 1+                , txOutputs = replicate 100 1+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance sufficient, fragmented enough, but maximum input \+            \count exceeded"+            (Left $ InputLimitExceeded $ InputLimitExceededError 9)+            (CoinSelectionFixture+                { maxNumOfInputs = 9+                , utxoInputs = replicate 100 1+                , txOutputs = replicate 10 10+                })++        coinSelectionUnitTest largestFirst+            "UTxO balance sufficient, fragmented enough, but maximum input \+            \count exceeded"+            (Left $ InputLimitExceeded $ InputLimitExceededError 2)+            (CoinSelectionFixture+                { maxNumOfInputs = 2+                , utxoInputs = [1,2,10,6,5]+                , txOutputs = [11, 1]+                })++    describe "Coin selection: largest-first algorithm: properties" $ do++        it "forall (UTxO, NonEmpty TxOut), there's at least as many selected \+            \inputs as there are requested outputs"+            (property $ propAtLeast @TxIn @Address)+        it "forall (UTxO, NonEmpty TxOut), for all selected input, there's no \+            \bigger input in the UTxO that is not already in the selected \+            \inputs"+            (property $ propInputDecreasingOrder @TxIn @Address)++--------------------------------------------------------------------------------+-- Properties+--------------------------------------------------------------------------------++propAtLeast+    :: (Ord i, Ord o)+    => CoinSelProp i o+    -> Property+propAtLeast (CoinSelProp utxo txOuts) =+    isRight selection ==>+        let Right (CoinSelectionResult s _) = selection in+        prop s+  where+    prop (CoinSelection inps _ _) =+        length inps `shouldSatisfy` (>= length txOuts)+    selection = runIdentity $ runExceptT $ selectCoins largestFirst+        $ CoinSelectionParameters utxo txOuts selectionLimit+    selectionLimit = CoinSelectionLimit $ const 100++propInputDecreasingOrder+    :: (Ord i, Ord o)+    => CoinSelProp i o+    -> Property+propInputDecreasingOrder (CoinSelProp utxo txOuts) =+    isRight selection ==>+        let Right (CoinSelectionResult s _) = selection in+        prop s+  where+    prop (CoinSelection inps _ _) =+        let+            utxo' = (Map.toList . unCoinMap) $ utxo `excluding`+                Set.fromList (entryKey <$> coinMapToList inps)+        in unless (L.null utxo') $+            (L.minimum (entryValue <$> coinMapToList inps))+            `shouldSatisfy`+            (>= (L.maximum (snd <$> utxo')))+    selection = runIdentity+        $ runExceptT+        $ selectCoins largestFirst+        $ CoinSelectionParameters utxo txOuts selectionLimit+    selectionLimit = CoinSelectionLimit $ const 100
+ src/test/Cardano/CoinSelection/Algorithm/MigrationSpec.hs view
@@ -0,0 +1,339 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Cardano.CoinSelection.Algorithm.MigrationSpec+    ( spec+    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMap (..)+    , CoinMapEntry (..)+    , CoinSelection (..)+    , coinMapToList+    , coinMapValue+    , sumChange+    , sumInputs+    )+import Cardano.CoinSelection.Algorithm.Migration+    ( BatchSize (..), idealBatchSize, selectCoins )+import Cardano.CoinSelection.Fee+    ( DustThreshold (..)+    , Fee (..)+    , FeeBalancingPolicy (..)+    , FeeEstimator (..)+    , FeeOptions (..)+    )+import Cardano.CoinSelection.FeeSpec+    ( FeeParameters, stableEstimator )+import Cardano.CoinSelectionSpec+    ()+import Cardano.Test.Utilities+    ( Address+    , Hash (..)+    , TxIn (..)+    , unsafeCoin+    , unsafeDustThreshold+    , unsafeFee+    )+import Data.ByteString+    ( ByteString )+import Data.Function+    ( (&) )+import Internal.Coin+    ( Coin, coinToIntegral )+import Numeric.Natural+    ( Natural )+import Test.Hspec+    ( Spec, SpecWith, describe, it, shouldSatisfy )+import Test.QuickCheck+    ( Arbitrary (..)+    , Gen+    , Property+    , arbitrarySizedIntegral+    , choose+    , conjoin+    , counterexample+    , frequency+    , label+    , property+    , scale+    , vectorOf+    , withMaxSuccess+    , (===)+    )+import Test.QuickCheck.Monadic+    ( monadicIO, monitor, pick )++import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as B8+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Internal.Coin as C++spec :: Spec+spec = do+    describe "idealBatchSize" $ do+        it "Eventually converge for decreasing functions" $ do+            property $ \coinselOpts -> do+                let batchSize = idealBatchSize coinselOpts+                label (show batchSize) True++    describe "accuracy of selectCoins" $ do+        let testAccuracy :: Double -> SpecWith ()+            testAccuracy r = it title $ withMaxSuccess 1000 $ monadicIO $ do+                let dust = unsafeCoin @Int 100+                utxo <- pick (genUTxO r dust)+                batchSize <- pick genBatchSize+                feeOpts <- pick (genFeeOptions dust)+                let selections = selectCoins feeOpts batchSize utxo+                monitor $ label $ accuracy dust+                    (coinToIntegral $ coinMapValue utxo)+                    (sum $ coinToIntegral . sumInputs <$> selections)+              where+                title :: String+                title = "dust=" <> show (round (100 * r) :: Int) <> "%"++                accuracy :: Coin -> Natural -> Natural -> String+                accuracy dust sup real+                    | a >= 1.0 =+                        "PERFECT  (== 100%)"+                    | a > 0.99 || (sup - real) < coinToIntegral dust =+                        "OKAY     (>   99%)"+                    | otherwise =+                        "MEDIOCRE (<=  99%)"+                  where+                    a = double real / double sup+                    double = fromRational @Double . fromIntegral++        mapM_ testAccuracy [ 0.01 , 0.05 , 0.10 , 0.25 , 0.50 ]++    describe "selectCoins properties" $ do+        it "No coin selection has outputs" $+            property $ withMaxSuccess 10000 $ prop_onlyChangeOutputs+                @(Wrapped TxIn) @Address++        it "Every coin in the selection change >= minimum threshold coin" $+            property $ withMaxSuccess 10000 $ prop_noLessThanThreshold+                @(Wrapped TxIn) @Address++        it "Total input UTxO value >= sum of selection change coins" $+            property $ withMaxSuccess 10000 $ prop_inputsGreaterThanOutputs+                @(Wrapped TxIn) @Address++        it "Every selection input is unique" $+            property $ withMaxSuccess 10000 $ prop_inputsAreUnique+                @(Wrapped TxIn) @Address++        it "Every selection input is a member of the UTxO" $+            property $ withMaxSuccess 10000 $ prop_inputsStillInUTxO+                @(Wrapped TxIn) @Address++        it "Every coin selection is well-balanced" $+            property $ withMaxSuccess 10000 $ prop_wellBalanced+                @(Wrapped TxIn) @Address++    describe "selectCoins regressions" $ do+        it "regression #1" $ do+            let feeOpts = FeeOptions+                    { dustThreshold = unsafeDustThreshold @Int 9+                    , feeEstimator = FeeEstimator $ \s -> unsafeFee @Int+                        $ fromIntegral+                        $ 5 * (length (inputs s) + length (outputs s))+                    , feeBalancingPolicy = RequireBalancedFee+                    }+            let batchSize = BatchSize 1+            let utxo = CoinMap $ Map.fromList+                    [ ( TxIn+                        { txinId = Hash "|\243^\SUBg\242\231\&1\213\203"+                        , txinIx = 2+                        }+                      , unsafeCoin @Int 2+                      )+                    ]+            property $ prop_inputsGreaterThanOutputs+                @TxIn @Address feeOpts batchSize utxo++--------------------------------------------------------------------------------+-- Properties+--------------------------------------------------------------------------------++-- | No coin selection has outputs+prop_onlyChangeOutputs+    :: forall i o . (Ord i, Ord o, Show o)+    => FeeOptions i o+    -> BatchSize+    -> CoinMap i+    -> Property+prop_onlyChangeOutputs feeOpts batchSize utxo = do+    let allOutputs =+            coinMapToList . outputs =<< selectCoins feeOpts batchSize utxo+    property (allOutputs `shouldSatisfy` null)++-- | Every coin in the selection change >= minimum threshold coin+prop_noLessThanThreshold+    :: forall i o . (Ord i, Ord o)+    => FeeOptions i o+    -> BatchSize+    -> CoinMap i+    -> Property+prop_noLessThanThreshold feeOpts batchSize utxo = do+    let allChange = change+            =<< selectCoins feeOpts batchSize utxo+    let undersizedCoins =+            filter (< threshold) allChange+    property (undersizedCoins `shouldSatisfy` null)+  where+    threshold = unDustThreshold $ dustThreshold feeOpts++-- | Total input UTxO value >= sum of selection change coins+prop_inputsGreaterThanOutputs+    :: forall i o . (Ord i, Ord o, Show i, Show o)+    => FeeOptions i o+    -> BatchSize+    -> CoinMap i+    -> Property+prop_inputsGreaterThanOutputs feeOpts batchSize utxo = do+    let selections  = selectCoins feeOpts batchSize utxo+    let totalChange = mconcat (sumChange <$> selections)+    let balanceUTxO = coinMapValue utxo+    property (balanceUTxO >= totalChange)+        & counterexample ("Total change balance: " <> show totalChange)+        & counterexample ("Total UTxO balance: " <> show balanceUTxO)+        & counterexample ("Selections: " <> show selections)++-- | Every selected input is unique, i.e. selected only once+prop_inputsAreUnique+    :: forall i o . (Ord i, Ord o)+    => FeeOptions i o+    -> BatchSize+    -> CoinMap i+    -> Property+prop_inputsAreUnique feeOpts batchSize utxo = do+    let selectionInputList =+            coinMapToList . inputs =<< selectCoins feeOpts batchSize utxo+    let selectionInputSet =+            Set.fromList selectionInputList+    Set.size selectionInputSet === length selectionInputSet++-- | Every selection input is still a member of the UTxO" $+prop_inputsStillInUTxO+    :: forall i o . (Ord i, Ord o)+    => FeeOptions i o+    -> BatchSize+    -> CoinMap i+    -> Property+prop_inputsStillInUTxO feeOpts batchSize utxo = do+    let selectionInputSet = Set.fromList $+            coinMapToList . inputs =<< selectCoins feeOpts batchSize utxo+    let utxoSet = Set.fromList $+            fmap (uncurry CoinMapEntry) $ Map.toList $ unCoinMap utxo+    property (selectionInputSet `Set.isSubsetOf` utxoSet)++-- | Every coin selection is well-balanced (i.e. actual fees are exactly the+-- expected fees)+prop_wellBalanced+    :: forall i o . (Ord i, Ord o, Show i, Show o)+    => FeeParameters i o+    -> BatchSize+    -> CoinMap i+    -> Property+prop_wellBalanced feeParams batchSize utxo = do+    let feeOpts = FeeOptions+            { dustThreshold = DustThreshold mempty+            , feeEstimator = stableEstimator feeParams+            , feeBalancingPolicy = RequireBalancedFee+            }+    let selections = selectCoins feeOpts batchSize utxo+    conjoin+        [ counterexample example (actualFee === expectedFee)+        | s <- selections+        , let actualFee+                = coinToIntegral (sumInputs s)+                - coinToIntegral (sumChange s)+        , let expectedFee+                = coinToIntegral @Integer+                $ unFee $ estimateFee (feeEstimator feeOpts) s+        , let example = unlines+                [ "Coin Selection: " <> show s+                , "Actual fee: " <> show actualFee+                , "Expected fee: " <> show expectedFee+                ]+        ]++--------------------------------------------------------------------------------+-- Arbitrary Instances+--------------------------------------------------------------------------------++-- A wrapper to avoid overlapping instances imported from other modules.+newtype Wrapped a = Wrapped { unwrap :: a }+    deriving (Eq, Ord, Show)++-- TODO: Move similar Arbitrary instances to a shared module for better reuse.+instance Arbitrary (Wrapped TxIn) where+    arbitrary = fmap Wrapped . TxIn+        <$> fmap unwrap arbitrary+        <*> scale (`mod` 3) arbitrary++-- TODO: Move similar Arbitrary instances to a shared module for better reuse.+instance Arbitrary (Wrapped (Hash "Tx")) where+    arbitrary = Wrapped . Hash <$> (BS.pack <$> vectorOf 32 arbitrary)++instance Arbitrary BatchSize where+    arbitrary = BatchSize <$> arbitrarySizedIntegral+    shrink (BatchSize s) = BatchSize <$> shrink s++--------------------------------------------------------------------------------+-- Generators+--------------------------------------------------------------------------------++genBatchSize :: Gen BatchSize+genBatchSize = BatchSize <$> choose (50, 150)++genFeeOptions :: Coin -> Gen (FeeOptions TxIn Address)+genFeeOptions dust = do+    pure $ FeeOptions+        { feeEstimator = FeeEstimator $ \s ->+            let x = fromIntegral @_ @Integer+                    (length (inputs s) + length (outputs s))+            in unsafeFee $+                  (C.coinToIntegral dust `div` 100) * x + C.coinToIntegral dust+        , dustThreshold = DustThreshold dust+        , feeBalancingPolicy = RequireBalancedFee+        }++-- | Generate a given UTxO with a particular percentage of dust+genUTxO :: Double -> Coin -> Gen (CoinMap TxIn)+genUTxO r dust = do+    n <- choose (10, 1000)+    inps <- genTxIn n+    coins <- vectorOf n genCoin+    pure $ CoinMap $ Map.fromList $ zip inps coins+  where+    genTxIn :: Int -> Gen [TxIn]+    genTxIn n = do+        ids <- vectorOf n (Hash <$> genBytes 8)+        ixs <- vectorOf n arbitrary+        pure $ zipWith TxIn ids ixs++    genBytes :: Int -> Gen ByteString+    genBytes n = B8.pack <$> vectorOf n arbitrary++    genCoin :: Gen Coin+    genCoin = unsafeCoin @Int <$> frequency+        [ (round (100*r), choose (1, integralDust))+        , (round (100*(1-r)), choose (integralDust, 1000 * integralDust))+        ]+      where+        integralDust = C.coinToIntegral dust++--------------------------------------------------------------------------------+-- Utility Functions+--------------------------------------------------------------------------------
+ src/test/Cardano/CoinSelection/Algorithm/RandomImproveSpec.hs view
@@ -0,0 +1,264 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Cardano.CoinSelection.Algorithm.RandomImproveSpec+    ( spec+    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinSelection (..)+    , CoinSelectionAlgorithm (..)+    , CoinSelectionError (..)+    , CoinSelectionLimit (..)+    , CoinSelectionParameters (..)+    , CoinSelectionResult (..)+    , InputCountInsufficientError (..)+    , InputLimitExceededError (..)+    , InputValueInsufficientError (..)+    , InputsExhaustedError (..)+    )+import Cardano.CoinSelection.Algorithm.LargestFirst+    ( largestFirst )+import Cardano.CoinSelection.Algorithm.RandomImprove+    ( randomImprove )+import Cardano.CoinSelectionSpec+    ( CoinSelProp (..)+    , CoinSelectionFixture (..)+    , CoinSelectionTestResult (..)+    , coinSelectionUnitTest+    )+import Cardano.Test.Utilities+    ( Address, TxIn, unsafeCoin )+import Control.Monad.Trans.Except+    ( runExceptT )+import Crypto.Random+    ( SystemDRG, getSystemDRG )+import Crypto.Random.Types+    ( withDRG )+import Data.Either+    ( isLeft, isRight )+import Data.Functor.Identity+    ( Identity (..) )+import Test.Hspec+    ( Spec, before, describe, it, shouldSatisfy )+import Test.QuickCheck+    ( Property, property, (===), (==>) )++import qualified Data.List as L++spec :: Spec+spec = do+    describe "Coin selection : random algorithm unit tests" $ do+        let oneAda = 1000000++        coinSelectionUnitTest randomImprove ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [1,1,1,1]+                , rsChange = [2]+                , rsOutputs = [2]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [1,1,1,1,1,1]+                , txOutputs = [2]+                })++        coinSelectionUnitTest randomImprove ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [1,1,1,1,1,1]+                , rsChange = [2,1]+                , rsOutputs = [2,1]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [1,1,1,1,1,1]+                , txOutputs = [2,1]+                })++        coinSelectionUnitTest randomImprove ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [1,1,1,1,1]+                , rsChange = [2]+                , rsOutputs = [2,1]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [1,1,1,1,1]+                , txOutputs = [2,1]+                })++        coinSelectionUnitTest randomImprove ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [1,1,1,1]+                , rsChange = [1]+                , rsOutputs = [2,1]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [1,1,1,1]+                , txOutputs = [2,1]+                })++        coinSelectionUnitTest randomImprove ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [5]+                , rsChange = [3]+                , rsOutputs = [2]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [5,5,5]+                , txOutputs = [2]+                })++        coinSelectionUnitTest randomImprove ""+            (Right $ CoinSelectionTestResult+                { rsInputs = [10,10]+                , rsChange = [8,8]+                , rsOutputs = [2,2]+                }+            )+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [10,10,10]+                , txOutputs = [2,2]+                })++        coinSelectionUnitTest randomImprove "cannot cover aim, but only min"+            (Right $ CoinSelectionTestResult+                { rsInputs = [1,1,1,1]+                , rsChange = [1]+                , rsOutputs = [3]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 4+                , utxoInputs = [1,1,1,1,1,1]+                , txOutputs = [3]+                })++        coinSelectionUnitTest randomImprove "REG CO-450: no fallback"+            (Right $ CoinSelectionTestResult+                { rsInputs = [oneAda, oneAda, oneAda, oneAda]+                , rsChange = [oneAda, oneAda `div` 2]+                , rsOutputs = [2*oneAda,oneAda `div` 2]+                })+            (CoinSelectionFixture+                { maxNumOfInputs = 4+                , utxoInputs = [oneAda, oneAda, oneAda, oneAda]+                , txOutputs = [2*oneAda, oneAda `div` 2]+                })++        coinSelectionUnitTest randomImprove+            "enough funds, proper fragmentation, inputs depleted"+            (Left (InputsExhausted InputsExhaustedError))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [10,10,10,10]+                , txOutputs = [38,1]+                })++        coinSelectionUnitTest randomImprove ""+            (Left $ InputLimitExceeded $ InputLimitExceededError 2)+            (CoinSelectionFixture+                { maxNumOfInputs = 2+                , utxoInputs = [1,1,1,1,1,1]+                , txOutputs = [3]+                })++        coinSelectionUnitTest randomImprove "each output needs <maxNumOfInputs"+            (Left $ InputLimitExceeded $ InputLimitExceededError 9)+            (CoinSelectionFixture+                { maxNumOfInputs = 9+                , utxoInputs = replicate 100 1+                , txOutputs = replicate 100 1+                })++        coinSelectionUnitTest randomImprove "each output needs >maxNumInputs"+            (Left $ InputLimitExceeded $ InputLimitExceededError 9)+            (CoinSelectionFixture+                { maxNumOfInputs = 9+                , utxoInputs = replicate 100 1+                , txOutputs = replicate 10 10+                })++        coinSelectionUnitTest randomImprove ""+            (Left $ InputValueInsufficient $ InputValueInsufficientError+                (unsafeCoin @Int 39) (unsafeCoin @Int 40))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [40]+                })++        coinSelectionUnitTest randomImprove ""+            (Left $ InputValueInsufficient $ InputValueInsufficientError+                (unsafeCoin @Int 39) (unsafeCoin @Int 43))+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,10,17]+                , txOutputs = [40,1,1,1]+                })++        coinSelectionUnitTest randomImprove ""+            (Left $ InputCountInsufficient $ InputCountInsufficientError 3 4)+            (CoinSelectionFixture+                { maxNumOfInputs = 100+                , utxoInputs = [12,20,17]+                , txOutputs = [40,1,1,1]+                })++    before getSystemDRG $+        describe "Coin selection properties : random algorithm" $ do+            it "forall (UTxO, NonEmpty TxOut), running algorithm gives not \+                \less UTxO fragmentation than LargestFirst algorithm"+                (property . propFragmentation @TxIn @Address)+            it "forall (UTxO, NonEmpty TxOut), running algorithm gives the \+                \same errors as LargestFirst algorithm"+                (property . propErrors @TxIn @Address)++--------------------------------------------------------------------------------+-- Properties+--------------------------------------------------------------------------------++propFragmentation+    :: (Ord i, Ord o)+    => SystemDRG+    -> CoinSelProp i o+    -> Property+propFragmentation drg (CoinSelProp utxo txOuts) = do+    isRight selection1 && isRight selection2 ==>+        let Right (CoinSelectionResult s1 _) = selection1 in+        let Right (CoinSelectionResult s2 _) = selection2 in+        prop (s1, s2)+  where+    prop (CoinSelection inps1 _ _, CoinSelection inps2 _ _) =+        L.length inps1 `shouldSatisfy` (>= L.length inps2)+    (selection1,_) = withDRG drg $+        runExceptT $ selectCoins randomImprove params+    selection2 = runIdentity $ runExceptT $+        selectCoins largestFirst params+    selectionLimit = CoinSelectionLimit $ const 100+    params = CoinSelectionParameters txOuts utxo selectionLimit++propErrors+    :: (Ord i, Ord o)+    => SystemDRG+    -> CoinSelProp i o+    -> Property+propErrors drg (CoinSelProp utxo txOuts) = do+    isLeft selection1 && isLeft selection2 ==>+        let (Left s1, Left s2) = (selection1, selection2)+        in prop (s1, s2)+  where+    prop (err1, err2) =+        err1 === err2+    (selection1,_) = withDRG drg $+        runExceptT $ selectCoins randomImprove params+    selection2 = runIdentity $ runExceptT $+        selectCoins largestFirst params+    selectionLimit = CoinSelectionLimit $ const 1+    params = CoinSelectionParameters txOuts utxo selectionLimit
+ src/test/Cardano/CoinSelection/FeeSpec.hs view
@@ -0,0 +1,1103 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Cardano.CoinSelection.FeeSpec+    ( spec++    -- Internals+    , FeeParameters+    , stableEstimator+    , valueDependentEstimator+    ) where++import Prelude hiding+    ( round )++import Cardano.CoinSelection+    ( CoinMap (..)+    , CoinMapEntry (..)+    , CoinSelection (..)+    , CoinSelectionAlgorithm (..)+    , CoinSelectionResult (..)+    , coinMapFromList+    , coinMapToList+    , sumChange+    , sumInputs+    , sumOutputs+    )+import Cardano.CoinSelection.Algorithm.LargestFirst+    ( largestFirst )+import Cardano.CoinSelection.Fee+    ( DustThreshold (..)+    , Fee (..)+    , FeeAdjustmentError (..)+    , FeeBalancingPolicy (..)+    , FeeEstimator (..)+    , FeeOptions (..)+    , adjustForFee+    , coalesceDust+    , distributeFee+    , reduceChangeOutputs+    , splitCoin+    )+import Cardano.Test.Utilities+    ( Address (..)+    , Hash (..)+    , ShowFmt (..)+    , TxIn (..)+    , unsafeCoin+    , unsafeDustThreshold+    , unsafeFee+    )+import Control.Arrow+    ( left )+import Control.Monad+    ( replicateM )+import Control.Monad.IO.Class+    ( liftIO )+import Control.Monad.Trans.Except+    ( runExceptT )+import Crypto.Random+    ( SystemDRG, getSystemDRG )+import Crypto.Random.Types+    ( withDRG )+import Data.Either+    ( isRight )+import Data.Function+    ( (&) )+import Data.Functor.Identity+    ( Identity (runIdentity) )+import Data.List.NonEmpty+    ( NonEmpty (..) )+import Data.Monoid+    ( All (..) )+import Data.Ratio+    ( (%) )+import Fmt+    ( Buildable (..), nameF, tupleF )+import GHC.Generics+    ( Generic )+import Internal.Coin+    ( Coin, coinToIntegral )+import Internal.Rounding+    ( RoundingDirection (..), round )+import Test.Hspec+    ( Spec, SpecWith, before, describe, it, shouldBe, shouldSatisfy )+import Test.QuickCheck+    ( Arbitrary (..)+    , Gen+    , Positive (getPositive)+    , Property+    , arbitraryBoundedIntegral+    , checkCoverage+    , choose+    , counterexample+    , cover+    , coverTable+    , disjoin+    , elements+    , expectFailure+    , generate+    , genericShrink+    , oneof+    , property+    , scale+    , tabulate+    , vectorOf+    , withMaxSuccess+    , (.&&.)+    , (===)+    , (==>)+    )+import Test.QuickCheck.Monadic+    ( monadicIO )++import qualified Cardano.CoinSelection as CS+import qualified Data.ByteString as BS+import qualified Data.Foldable as F+import qualified Data.List as L+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as Map+import qualified Internal.Coin as C++spec :: Spec+spec = do+    describe "Fee calculation : unit tests" $ do+        -- Change covers fee exactly, single change output+        feeUnitTest (FeeFixture+            { fInps = [20]+            , fOuts = [17]+            , fChngs = [3]+            , fUtxo = []+            , fFee = 3+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [20]+            , csOuts = [17]+            , csChngs = []+            })++        -- Total change covers fee, multiple change outputs+        feeUnitTest (FeeFixture+            { fInps = [20,20]+            , fOuts = [16,18]+            , fChngs = [4,2]+            , fUtxo = []+            , fFee = 6+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [20,20]+            , csOuts = [16,18]+            , csChngs = []+            })++        -- Fee split evenly across change outputs+        feeUnitTest (FeeFixture+            { fInps = [20,20]+            , fOuts = [18,18]+            , fChngs = [2,2]+            , fUtxo = []+            , fFee = 2+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [20,20]+            , csOuts = [18,18]+            , csChngs = [1,1]+            })++        -- Fee split evenly across change outputs+        feeUnitTest (FeeFixture+            { fInps = [20,20]+            , fOuts = [17,18]+            , fChngs = [3,2]+            , fUtxo = []+            , fFee = 2+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [20,20]+            , csOuts = [17,18]+            , csChngs = [2,1]+            })++        -- Fee divvied, dust removed (dust = 0)+        feeUnitTest (FeeFixture+            { fInps = [20,20,20]+            , fOuts = [14,18,19]+            , fChngs = [6,2,1]+            , fUtxo = []+            , fFee = 3+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [20,20,20]+            , csOuts = [14,18,19]+            , csChngs = [4,1,1]+            })++        -- Fee divvied, dust removed (dust = 1)+        feeUnitTest (FeeFixture+            { fInps = [20,20,20]+            , fOuts = [14,18,19]+            , fChngs = [6,2,1]+            , fUtxo = []+            , fFee = 3+            , fDust = 1+            }) (Right $ FeeOutput+            { csInps = [20,20,20]+            , csOuts = [14,18,19]+            , csChngs = [6]+            })++        -- Cannot cover fee, no extra inputs+        feeUnitTest (FeeFixture+            { fInps = [20]+            , fOuts = [17]+            , fChngs = [3]+            , fUtxo = []+            , fFee = 4+            , fDust = 0+            }) (Left $ CannotCoverFee $ unsafeFee @Int 1)++        -- Cannot cover fee even with an extra (too small) inputs+        feeUnitTest (FeeFixture+            { fInps = [10]+            , fOuts = [7]+            , fChngs = [3]+            , fUtxo = [1]+            , fFee = 5+            , fDust = 0+            }) (Left $ CannotCoverFee $ unsafeFee @Int 1)++        -- Can select extra inputs to exactly cover fee, no change back+        feeUnitTest (FeeFixture+            { fInps = [10]+            , fOuts = [7]+            , fChngs = [3]+            , fUtxo = [1,1]+            , fFee = 5+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [10,1,1]+            , csOuts = [7]+            , csChngs = []+            })++        -- Can select extra inputs to cover for fee, and leave a change back+        feeUnitTest (FeeFixture+            { fInps = [10]+            , fOuts = [7]+            , fChngs = [3]+            , fUtxo = [3]+            , fFee = 5+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [10,3]+            , csOuts = [7]+            , csChngs = [1]+            })++        -- Multiple change output, can select extra inputs to cover fee, no+        -- change+        feeUnitTest (FeeFixture+            { fInps = [10,10]+            , fOuts = [7,7]+            , fChngs = [3,3]+            , fUtxo = [2,2]+            , fFee = 10+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [10,10,2,2]+            , csOuts = [7,7]+            , csChngs = []+            })++        -- Multiple outputs, extra inputs selected, resulting change+        feeUnitTest (FeeFixture+            { fInps = [10,10]+            , fOuts = [7,7]+            , fChngs = [3,3]+            , fUtxo = [3,3]+            , fFee = 10+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [10,10,3,3]+            , csOuts = [7,7]+            , csChngs = [1,1]+            })++        -- Multiple change outputs, some bigger than actual Dust+        feeUnitTest (FeeFixture+            { fInps = [20,20]+            , fOuts = [16,18]+            , fChngs = [4,2]+            , fUtxo = []+            , fFee = 6+            , fDust = 2+            }) (Right $ FeeOutput+            { csInps = [20,20]+            , csOuts = [16,18]+            , csChngs = []+            })++        -- Change created when there was no change before+        feeUnitTest (FeeFixture+            { fInps = [1]+            , fOuts = [1]+            , fChngs = []+            , fUtxo = [2]+            , fFee = 1+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [1,2]+            , csOuts = [1]+            , csChngs = [1]+            })++        feeUnitTest (FeeFixture+            { fInps = []+            , fOuts = []+            , fChngs = []+            , fUtxo = [3]+            , fFee = 3+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [3]+            , csOuts = []+            , csChngs = []+            })++        feeUnitTest (FeeFixture+            { fInps = []+            , fOuts = []+            , fChngs = []+            , fUtxo = [2,2]+            , fFee = 3+            , fDust = 0+            }) (Right $ FeeOutput+            { csInps = [2,2]+            , csOuts = []+            , csChngs = [1]+            })++        feeUnitTest (FeeFixture+            { fInps = []+            , fOuts = []+            , fChngs = []+            , fUtxo = [2,2]+            , fFee = 3+            , fDust = 1+            }) (Right $ FeeOutput+            { csInps = [2,2]+            , csOuts = []+            , csChngs = [1]+            })++    describe "Fee Calculation: Generators" $ do+        it "Arbitrary CoinSelection" $ property $ \(ShowFmt cs) ->+            property $ isValidSelection @TxIn @Address cs++    before getSystemDRG $ describe "Fee Adjustment properties" $ do+        it "Fee adjustment is deterministic when there's no extra inputs"+            (\_ -> property $ propDeterministic @TxIn @Address)+        it "Adjusting for fee (/= 0) reduces the change outputs or increase \+            \inputs"+            (property . propReducedChanges @TxIn @Address)++    describe "distributeFee" $ do+        it "fee portions are all within unity of ideal unrounded portions"+            (checkCoverage propDistributeFeeFair)+        it "fee portions are allocated optimally"+            (checkCoverage propDistributeFeeOptimal)+        it "Σ fst (distributeFee fee outs) == fee"+            (checkCoverage propDistributeFeeSame)+        it "snd (distributeFee fee outs) == outs"+            (checkCoverage propDistributeFeeOuts)+        it "expectFailure: not (any null (fst <$> distributeFee fee outs))"+            (expectFailure propDistributeFeeNoNullFee)++    describe "coalesceDust" $ do+        it "sum coins = sum (coalesceDust threshold coins)"+            (checkCoverage propCoalesceDustPreservesSum)+        it "all (/= Coin 0) (coalesceDust threshold coins)"+            (checkCoverage propCoalesceDustLeavesNoZeroCoins)+        it "leaves at most one dust coin"+            (checkCoverage propCoalesceDustLeavesAtMostOneDustCoin)+        it "length coins >= (coalesceDust threshold coins)"+            (checkCoverage propCoalesceDustNeverLengthensList)++    describe "reduceChangeOutputs" $ do+        it "data coverage is adequate"+            (checkCoverage propReduceChangeOutputsDataCoverage)+        it "the fee balancing algorithm converges for any coin selection"+            ( withMaxSuccess 100_000+            $ propReduceChangeOutputsConverge @TxIn @Address+            )++    describe "splitCoin" $ do+        it "data coverage is adequate"+            (checkCoverage propSplitCoinDataCoverage)+        it "preserves the total sum"+            (checkCoverage propSplitCoinPreservesSum)+        it "results are all within unity of ideal unrounded results"+            (checkCoverage propSplitCoinFair)++--------------------------------------------------------------------------------+-- Fee Adjustment - Properties+--------------------------------------------------------------------------------++-- Check whether a selection is valid+isValidSelection :: CoinSelection i o -> Bool+isValidSelection s =+    sumInputs s >= sumOutputs s `C.add` sumChange s++-- | Data for running fee calculation properties+data FeeProp i o = FeeProp+    { selection :: CoinSelection i o+     -- ^ inputs from wich largestFirst can be calculated+    , availableUtxo :: CoinMap i+     -- ^ additional UTxO from which fee calculation will pick needed coins+    , feeDust :: (Integer, Integer)+     -- ^ constant fee and dust threshold+    } deriving Show++instance (Buildable i, Buildable o) =>+    Buildable (FeeProp i o) where+        build (FeeProp cc utxo opt) = mempty+            <> nameF "selection" (build cc)+            <> nameF "utxo" (build (coinMapToList utxo))+            <> nameF "options" (tupleF opt)++propDeterministic+    :: forall i o . (Ord i, Ord o, Show i, Show o)+    => ShowFmt (FeeProp i o)+    -> Property+propDeterministic (ShowFmt (FeeProp coinSel _ (fee, dust))) =+    monadicIO $ liftIO $ do+        let feeOpt = feeOptions fee dust+        let utxo = mempty+        resultOne <- runExceptT $ adjustForFee feeOpt utxo coinSel+        resultTwo <- runExceptT $ adjustForFee feeOpt utxo coinSel+        resultOne `shouldBe` resultTwo++propReducedChanges+    :: forall i o . (Ord i, Show i, Show o)+    => SystemDRG+    -> ShowFmt (FeeProp i o)+    -> Property+propReducedChanges drg (ShowFmt (FeeProp coinSel utxo (fee, dust))) = do+    isRight coinSel' ==> let Right s = coinSel' in prop s+  where+    prop s = do+        let chgs' = F.fold $ change s+        let chgs = F.fold $ change coinSel+        let inps' = CS.inputs s+        let inps = CS.inputs coinSel+        disjoin+            [ chgs' `shouldSatisfy` (<= chgs)+            , length inps' `shouldSatisfy` (>= length inps)+            ]+    feeOpt = feeOptions fee dust+    coinSel' = left show $ fst $ withDRG drg $ runExceptT $+        adjustForFee feeOpt utxo coinSel++--------------------------------------------------------------------------------+-- distributeFee - Properties+--------------------------------------------------------------------------------++-- | Helper to re-apply the pre-conditions for distributeFee+propDistributeFee+    :: ((Fee, NonEmpty Coin) -> Property)+    -> (Fee, NonEmpty Coin)+    -> Property+propDistributeFee prop (fee, outs) =+    coverTable "properties"+        [ ("fee > 0", 50)+        , ("nOuts=1", 1)+        , ("nOuts=2", 1)+        , ("nOuts=2+", 10)+        ]+        $ tabulate "properties"+            [ if fee > Fee C.zero then "fee > 0" else "fee == 0"+            , "nOuts=" <> case length outs of+                n | n <= 2 -> show n+                _ -> "2+"+            ]+        $ prop (fee, outs)++-- | Verify that fees are distributed fairly across outputs, so that every+--   rounded fee portion is always within unity of the ideal unrounded fee+--   portion.+propDistributeFeeFair+    :: (Fee, NonEmpty Coin)+    -> Property+propDistributeFeeFair (fee, coins) = (.&&.)+    (F.all (uncurry (<=)) (NE.zip fees feeUpperBounds))+    (F.all (uncurry (>=)) (NE.zip fees feeLowerBounds))+  where+    fees = fst <$> distributeFee fee coins++    feeUpperBounds = unsafeFee @Integer . ceiling . computeIdealFee <$> coins+    feeLowerBounds = unsafeFee @Integer . floor   . computeIdealFee <$> coins++    computeIdealFee :: Coin -> Rational+    computeIdealFee c+        = coinToIntegral c+        * coinToIntegral (unFee fee)+        % coinToIntegral (F.fold coins)++-- | Verify that fees are distributed optimally across coins, such that the+-- absolute deviation from the ideal (unrounded) fee distribution is minimal.+propDistributeFeeOptimal+    :: Fee+    -> NonEmpty Coin+    -> Property+propDistributeFeeOptimal fee coins = property $+    computeDeviation (fst <$> distributeFee fee coins)+        `shouldBe` minimumPossibleDeviation+  where+    -- Compute the deviation of a fee portion distribution from the ideal+    -- unrounded fee portion distribution.+    computeDeviation :: NonEmpty Fee -> Rational+    computeDeviation feesRounded = F.sum $+        NE.zipWith zipper+            feesRounded+            idealUnroundedDistribution+      where+        zipper :: Fee -> Rational -> Rational+        zipper (Fee roundedFee) unroundedFee = abs $+            fromIntegral (coinToIntegral @Integer roundedFee) - unroundedFee++    -- The minimum deviation across all possible distributions for the given+    -- fee and set of coins.+    minimumPossibleDeviation :: Rational+    minimumPossibleDeviation =+        F.minimum $ computeDeviation <$> allPossibleDistributions++    -- The set of all possible fee distributions for the given fee and coins.+    allPossibleDistributions :: [NonEmpty Fee]+    allPossibleDistributions = filter isValidDistribution $ NE.zipWith+        (\f roundDir ->+            unsafeFee @Integer $ fromIntegral @Integer $ round roundDir f)+        idealUnroundedDistribution <$> allPossibleRoundings+      where+        -- Indicates whether the given distribution has the correct total fee.+        isValidDistribution :: NonEmpty Fee -> Bool+        isValidDistribution r = F.fold r == fee++        -- All possible ways to round an unrounded fee distribution.+        allPossibleRoundings :: [NonEmpty RoundingDirection]+        allPossibleRoundings = traverse (const [RoundUp, RoundDown]) coins++    -- The ideal unrounded fee distribution.+    idealUnroundedDistribution :: NonEmpty Rational+    idealUnroundedDistribution = computeIdealFee <$> coins+      where+        computeIdealFee :: Coin -> Rational+        computeIdealFee c+            = coinToIntegral c+            * coinToIntegral (unFee fee)+            % coinToIntegral (F.fold coins)++-- | Sum of the fees divvied over each output is the same as the initial total+-- fee.+propDistributeFeeSame+    :: (Fee, NonEmpty Coin)+    -> Property+propDistributeFeeSame = propDistributeFee $ \(fee, outs) ->+    F.fold (fst <$> distributeFee fee outs) === fee++-- | distributeFee doesn't change any of the outputs+propDistributeFeeOuts+    :: (Fee, NonEmpty Coin)+    -> Property+propDistributeFeeOuts = propDistributeFee $ \(fee, outs) ->+    (snd <$> distributeFee fee outs) === outs++-- | distributeFee never generates null fees for a given output.+--+-- This is NOT a property. It is here to illustrate that this can happen in+-- practice, and is known as a possible outcome for the distributeFee function+-- (it is fine for one of the output to be assigned no fee). The only reason+-- this would happen is because there would be less outputs than the fee amount+-- which is probably never going to happen in practice...+propDistributeFeeNoNullFee+    :: (Fee, NonEmpty Coin)+    -> Property+propDistributeFeeNoNullFee (fee, outs) =+    not (null outs) ==> withMaxSuccess 100000 prop+  where+    prop = property $ Fee C.zero `F.notElem` (fst <$> distributeFee fee outs)++--------------------------------------------------------------------------------+-- coalesceDust - Properties+--------------------------------------------------------------------------------++data CoalesceDustData = CoalesceDustData+    { cddThreshold :: DustThreshold+    , cddCoins :: NonEmpty Coin+    } deriving (Eq, Generic, Show)++instance Arbitrary CoalesceDustData where+    arbitrary = do+        coinCount <- genCoinCount+        coins <- (:|) <$> genCoin <*> replicateM coinCount genCoin+        threshold <- DustThreshold <$> oneof+            [ -- Two possibilities:+              genCoin+              -- ^ A completely fresh coin.+            , elements (F.toList coins)+              -- ^ A coin picked from the existing coin set.+            ]+        pure $ CoalesceDustData threshold coins+      where+        genCoin = unsafeCoin @Int <$> oneof [pure 0, choose (1, 100)]+        genCoinCount = choose (0, 10)+    shrink = genericShrink++propCoalesceDustPreservesSum :: CoalesceDustData -> Property+propCoalesceDustPreservesSum (CoalesceDustData threshold coins) =+    property $+    let total = F.fold coins in+    cover 8 (total == C.zero) "sum coins = 0" $+    cover 8 (total /= C.zero) "sum coins ≠ 0" $+    total == F.fold (coalesceDust threshold coins)++propCoalesceDustLeavesNoZeroCoins :: CoalesceDustData -> Property+propCoalesceDustLeavesNoZeroCoins (CoalesceDustData threshold coins) =+    property $+    cover 4 (F.all  (== C.zero) coins) "∀ coin ∈ coins . coin = 0" $+    cover 4 (F.elem    (C.zero) coins) "∃ coin ∈ coins . coin = 0" $+    cover 8 (F.notElem (C.zero) coins) "∀ coin ∈ coins . coin > 0" $+    F.notElem C.zero $ coalesceDust threshold coins++propCoalesceDustLeavesAtMostOneDustCoin :: CoalesceDustData -> Property+propCoalesceDustLeavesAtMostOneDustCoin (CoalesceDustData threshold coins) =+    property $+    let result = coalesceDust threshold coins in+    -- Check that we cover different kinds of extreme threshold conditions:+    cover 2 (F.all (<  threshold') coins) "∀ coin ∈ coins . coin < threshold" $+    cover 2 (F.all (>  threshold') coins) "∀ coin ∈ coins . coin > threshold" $+    cover 2 (F.all (== threshold') coins) "∀ coin ∈ coins . coin = threshold" $+    cover 2 (F.all (/= threshold') coins) "∀ coin ∈ coins . coin ≠ threshold" $+    -- Check that we cover typical threshold conditions:+    let haveMixture = getAll $ mconcat $ All <$>+          [ F.any (<  threshold') coins+          , F.any (== threshold') coins+          , F.any (>  threshold') coins+          ] in+    cover 8 haveMixture "have mixture of coin values in relation to threshold" $+    -- Check that we cover different result lengths:+    cover 8 (null result)        "length result = 0" $+    cover 8 (length result == 1) "length result = 1" $+    cover 8 (length result >= 2) "length result ≥ 2" $+    case result of+        [ ] -> F.fold coins == C.zero+        [x] -> F.fold coins == x+        cxs -> all (> threshold') cxs+  where+    threshold' = unDustThreshold threshold++propCoalesceDustNeverLengthensList :: CoalesceDustData -> Property+propCoalesceDustNeverLengthensList (CoalesceDustData threshold coins) =+    property $ length coins >= length (coalesceDust threshold coins)++--------------------------------------------------------------------------------+-- reduceChangeOutputs - Properties+--------------------------------------------------------------------------------++data ReduceChangeOutputsData = ReduceChangeOutputsData+    { rcodFee :: Fee+    , rcodThreshold :: DustThreshold+    , rcodCoins :: [Coin]+    } deriving (Eq, Generic, Show)++instance Arbitrary ReduceChangeOutputsData where+    arbitrary = do+        coalesceDustData <- arbitrary+        let threshold = cddThreshold coalesceDustData+        let coins = F.toList $ cddCoins coalesceDustData+        let coinSum = sum $ coinToIntegral <$> coins+        fee <- unsafeFee <$> oneof+            [ pure 0+            , choose (1, safePred coinSum)+            , pure coinSum+            , choose (coinSum + 1, coinSum * 2)+            ]+        pure $ ReduceChangeOutputsData fee threshold coins+      where+        safePred :: Integer -> Integer+        safePred x+            | x > 0 = x - 1+            | otherwise = 0+    shrink = genericShrink++propReduceChangeOutputsDataCoverage :: ReduceChangeOutputsData -> Property+propReduceChangeOutputsDataCoverage+    (ReduceChangeOutputsData (Fee fee) _ coins) =+        let coinSum = F.fold coins in+        property+            -- Test coverage of fee amount, relative to sum of coins:+            $ cover 100 (fee >= C.zero)+                "fee >= 0"+            $ cover 8 (fee == C.zero)+                "fee = 0"+            $ cover 8 (length (filter (> C.zero) coins) == 1)+                "one non-empty coin"+            $ cover 8 (length (filter (> C.zero) coins) == 2)+                "two non-empty coins"+            $ cover 8 (length (filter (> C.zero) coins) >= 3)+                "several non-empty coins"+            $ cover 8 (any (> C.zero) coins && C.zero < fee && fee < coinSum)+                "0 < fee < sum coins"+            $ cover 8 (any (> C.zero) coins && fee == coinSum)+                "fee = sum coins"+            $ cover 8 (any (> C.zero) coins && fee > coinSum)+                "fee > sum coins"+            True++propReduceChangeOutputsConverge+    :: (Show i, Show o)+    => CoinSelection i o+    -> FeeOptions i o+    -> Property+propReduceChangeOutputsConverge sel opts = do+    let Right (sel', remainder) = reduceChangeOutputs opts sel+    let fee = estimateFee (feeEstimator opts) sel'+    let prop = case feeBalancingPolicy opts of+            -- If fees are null and we require balanced selections, then the+            -- selection must be exactly balanced.+            RequireBalancedFee | remainder == Fee C.zero ->+                (Fee <$> delta sel') == Just fee++            -- Otherwise, either the change outputs are null, or the delta is+            -- positive because of the dangling output and we'll have to pay for+            -- it.+            RequireBalancedFee ->+                null (change sel') || (delta sel' >= Just C.zero)++            -- If fees are null and we do not require balanced selections, then+            -- the selection must leave __at least__ the fee amount.+            RequireMinimalFee | remainder == Fee C.zero ->+                (Fee <$> delta sel') >= Just fee++            -- Otherwise, the change outputs are necessarily null.+            RequireMinimalFee ->+                null (change sel')+    property prop+        & counterexample (unlines+            [ "new selection:  " <> show sel'+            , "delta (before): " <> show (delta sel)+            , "delta (after):  " <> show (delta sel')+            , "remainder:      " <> show remainder+            ])+  where+    delta s = sumInputs s `C.sub` (sumOutputs s `C.add` sumChange s)++--------------------------------------------------------------------------------+-- splitCoin - Properties+--------------------------------------------------------------------------------++data SplitCoinData = SplitCoinData+    { scdCoinToSplit :: Coin+    , scdCoinsToIncrease :: [Coin]+    } deriving (Eq, Generic, Show)++instance Arbitrary SplitCoinData where+    arbitrary = do+        coinToSplit <- genCoin+        n <- oneof+            [ pure 0+            , pure 1+            , choose (2, 10)+            ]+        coinsToIncrease <- replicateM n genCoin+        pure $ SplitCoinData coinToSplit coinsToIncrease+      where+        genCoin :: Gen Coin+        genCoin = oneof+            [ pure $ unsafeCoin @Integer 0+            , pure $ unsafeCoin @Integer 1+            , unsafeCoin @Int . (+ 1) . getPositive <$> arbitrary+            ]+    shrink = genericShrink++propSplitCoinDataCoverage :: SplitCoinData -> Property+propSplitCoinDataCoverage (SplitCoinData coinToSplit coinsToIncrease) =+    property+        $ cover 8 (null coinsToIncrease)+            "list of coins is empty"+        $ cover 8 (length coinsToIncrease == 1)+            "list of coins is singleton"+        $ cover 8 (length coinsToIncrease > 1)+            "list of coins has multiple entries"+        $ cover 8 (C.zero `elem` coinsToIncrease)+            "list of coins has at least one zero coin"+        $ cover 8 (any (> C.zero) coinsToIncrease)+            "list of coins has at least one non-zero coin"+        $ cover 8 (coinToSplit == C.zero)+            "coin to split is zero"+        $ cover 8 (coinToSplit > C.zero)+            "coin to split is non-zero"+        $ cover 8 (length coinsToIncrease > coinToIntegral coinToSplit)+            "coin to split is smaller than number of coins to increase"+        True++propSplitCoinPreservesSum :: SplitCoinData -> Property+propSplitCoinPreservesSum (SplitCoinData coinToSplit coinsToIncrease) =+    property $ totalBefore `shouldBe` totalAfter+  where+    totalAfter = F.fold (splitCoin coinToSplit coinsToIncrease)+    totalBefore = coinToSplit `C.add` F.fold coinsToIncrease++propSplitCoinFair :: (Coin, NonEmpty Coin) -> Property+propSplitCoinFair (coinToSplit, coinsToIncrease) = (.&&.)+    (F.all (uncurry (<=)) (NE.zip results upperBounds))+    (F.all (uncurry (>=)) (NE.zip results lowerBounds))+  where+    results = NE.fromList $ splitCoin coinToSplit $ NE.toList coinsToIncrease++    upperBounds = unsafeCoin @Integer . ceiling+        . computeIdealResult <$> coinsToIncrease+    lowerBounds = unsafeCoin @Integer . floor+        . computeIdealResult <$> coinsToIncrease++    computeIdealResult :: Coin -> Rational+    computeIdealResult c+        = fromIntegral (coinToIntegral @Integer c)+        + coinToIntegral @Integer coinToSplit+        % fromIntegral (length coinsToIncrease)++--------------------------------------------------------------------------------+-- Fee Adjustment - Unit Tests+--------------------------------------------------------------------------------++feeOptions+    :: Integer+    -> Integer+    -> FeeOptions i o+feeOptions fee dust = FeeOptions+    { feeEstimator = FeeEstimator $+        \_ -> unsafeFee fee+    , dustThreshold =+        unsafeDustThreshold dust+    , feeBalancingPolicy =+        RequireBalancedFee+    }++feeUnitTest+    :: FeeFixture+    -> Either (FeeAdjustmentError TxIn Address) FeeOutput+    -> SpecWith ()+feeUnitTest (FeeFixture inpsF outsF chngsF utxoF feeF dustF) expected =+    it title $ do+        (utxo, sel) <- setup @TxIn @Address+        result <- runExceptT $ do+            (CoinSelection inps outs chngs) <-+                adjustForFee (feeOptions feeF dustF) utxo sel+            return $ FeeOutput+                { csInps = coinToIntegral . entryValue <$> coinMapToList inps+                , csOuts = coinToIntegral . entryValue <$> coinMapToList outs+                , csChngs = coinToIntegral <$> chngs+                }+        fmap sortFeeOutput result `shouldBe` fmap sortFeeOutput expected+  where+    setup+        :: forall i o . (Arbitrary i, Arbitrary o, Ord i, Ord o)+        => IO (CoinMap i, CoinSelection i o)+    setup = do+        utxo <- generate (genInputs $ unsafeCoin <$> utxoF)+        inps <- (fmap (uncurry CoinMapEntry) . Map.toList . unCoinMap) <$>+            generate (genInputs $ unsafeCoin <$> inpsF)+        outs <- generate (genOutputs $ unsafeCoin <$> outsF)+        let chngs = map unsafeCoin chngsF+        pure (utxo, CoinSelection (coinMapFromList inps) outs chngs)++    title :: String+    title = mempty+        <> "CoinSelection (inps=" <> show inpsF+        <> "outs=" <> show outsF+        <> "chngs=" <> show chngsF+        <> "), UTxO=" <> show utxoF+        <> "), fee=" <> show feeF+        <> " --> " <> show expected++-- | A fixture for testing the fee calculation+data FeeFixture = FeeFixture+    { fInps :: [Integer]+        -- ^ Value (in Lovelace) & number of coins in inputs+    , fOuts :: [Integer]+        -- ^ Value (in Lovelace) & number of requested outputs+    , fChngs :: [Integer]+        -- ^ Value (in Lovelace) & number of changes+    , fUtxo :: [Integer]+        -- ^ Value (in Lovelace) & number of available coins in the UTxO+    , fFee :: Integer+        -- ^ Value (in Lovelace) of rigid fee+    , fDust :: Integer+        -- ^ Value (in Lovelace) of dust+    } deriving Show++-- | A fee calculation output+data FeeOutput = FeeOutput+    { csInps :: [Integer]+        -- ^ Value (in Lovelace) & number of available coins in the UTxO+    , csOuts :: [Integer]+        -- ^ Value (in Lovelace) & number of requested outputs+    , csChngs :: [Integer]+        -- ^ Value (in Lovelace) & number of changes+    } deriving (Show, Eq)++sortFeeOutput :: FeeOutput -> FeeOutput+sortFeeOutput (FeeOutput is os cs) =+    FeeOutput (L.sort is) (L.sort os) (L.sort cs)++--------------------------------------------------------------------------------+-- Arbitrary Instances+--------------------------------------------------------------------------------++deriving newtype instance Arbitrary a => Arbitrary (ShowFmt a)++genInputs :: (Arbitrary i, Ord i) => [Coin] -> Gen (CoinMap i)+genInputs coins = do+    let n = length coins+    inps <- vectorOf n arbitrary+    return $ CoinMap $ Map.fromList $ zip inps coins++genOutputs :: (Arbitrary o, Ord o) => [Coin] -> Gen (CoinMap o)+genOutputs coins = do+    let n = length coins+    outs <- vectorOf n arbitrary+    return $ coinMapFromList $ zipWith CoinMapEntry outs coins++genSelection+    :: (Arbitrary i, Ord i, Ord o)+    => CoinMap o+    -> Gen (CoinSelection i o)+genSelection outs = do+    utxo <- vectorOf (length outs * 3) arbitrary >>= genInputs+    let selectionLimit = CS.CoinSelectionLimit $ const 100+    let params = CS.CoinSelectionParameters utxo outs selectionLimit+    case runIdentity $ runExceptT $ selectCoins largestFirst params of+        Left _ -> genSelection outs+        Right (CoinSelectionResult s _) -> return s++instance Arbitrary TxIn where+    shrink _ = []+    arbitrary = TxIn+        <$> arbitrary+        <*> scale (`mod` 3) arbitrary -- No need for a high indexes++instance Arbitrary Coin where+    arbitrary = unsafeCoin @Int <$> choose (1, 100_000)+    shrink = fmap unsafeCoin . filter (> 0) . shrink . coinToIntegral @Int++instance Arbitrary DustThreshold where+    arbitrary = unsafeDustThreshold @Int <$> choose (0, 100)+    shrink = fmap DustThreshold . shrink . unDustThreshold++instance Arbitrary Fee where+    arbitrary = unsafeFee @Int <$> choose (1, 100_000)+    shrink = fmap Fee . shrink . unFee++instance (Arbitrary i, Arbitrary o, Ord i, Ord o) =>+    Arbitrary (FeeProp i o)+  where+    shrink (FeeProp cs utxo opts) =+        case Map.toList $ unCoinMap utxo of+            [] ->+                map (\cs' -> FeeProp cs' utxo opts) (shrink cs)+            us ->+                concatMap (\cs' ->+                    [ FeeProp cs' mempty opts+                    , FeeProp cs' (CoinMap $ Map.fromList (drop 1 us)) opts+                    ]+                ) (shrink cs)+    arbitrary = do+        cs <- arbitrary+        utxo <- choose (0, 50)+            >>= \n -> vectorOf n arbitrary+            >>= genInputs+        fee <- choose (100000, 500000)+        dust <- choose (0, 10000)+        return $ FeeProp cs utxo (fee, dust)++instance Arbitrary (Hash "Tx") where+    shrink _ = []+    arbitrary = do+        bytes <- BS.pack <$> vectorOf 8 arbitraryBoundedIntegral+        pure $ Hash bytes++instance Arbitrary Address where+    shrink _ = []+    arbitrary = do+        bytes <- BS.pack <$> vectorOf 8 arbitraryBoundedIntegral+        pure $ Address bytes++instance (Arbitrary i, Arbitrary o, Ord i, Ord o) =>+    Arbitrary (CoinSelection i o)+  where+    shrink sel = case unCoinSelection sel of+        ([_], [_], []) ->+            []+        (inps, outs, chgs) ->+            let+                inps' = if length inps > 1 then drop 1 inps else inps+                outs' = if length outs > 1 then drop 1 outs else outs+                chgs' = if not (null chgs) then drop 1 chgs else chgs+            in+            filter (\s -> s /= sel && isValidSelection s) $+                mkCoinSelection <$>+                    [ (inps', outs', chgs')+                    , (inps', outs , chgs )+                    , (inps , outs', chgs )+                    , (inps , outs , chgs')+                    ]+      where+        unCoinSelection s =+            (coinMapToList $ inputs s, coinMapToList $ outputs s, change s)+        mkCoinSelection (is, os, cs) =+            CoinSelection (coinMapFromList is) (coinMapFromList os) cs+    arbitrary = do+        outs <- choose (1, 10)+            >>= \n -> vectorOf n arbitrary+            >>= genOutputs+        genSelection outs++data FeeParameters i o = FeeParameters+    { feePerTransaction+        :: Fee+        -- ^ Base fee for a transaction.+    , feePerTransactionEntry+        :: Fee+        -- ^ Incremental fee for each input, output, and change output.+    } deriving (Eq, Generic, Show)++instance Arbitrary (FeeParameters i o) where+    arbitrary = do+        feePerTransaction <- unsafeFee @Int <$> choose (0, 10)+        feePerTransactionEntry <- unsafeFee @Int <$> choose (0, 10)+        pure $ FeeParameters {feePerTransaction, feePerTransactionEntry}+    shrink = genericShrink++-- An estimator that solely depends on the number of inputs and outputs.+stableEstimator :: FeeParameters i o -> FeeEstimator i o+stableEstimator+    FeeParameters {feePerTransaction, feePerTransactionEntry} =+        FeeEstimator $ \s -> Fee $ maybe C.zero+            (C.add (unFee feePerTransaction))+            (C.mul+                (unFee feePerTransactionEntry)+                (length (inputs s) + length (outputs s)))++-- An estimator which depends on the size of inputs and outputs, where the size+-- is a function of their number and their value. The bigger the coins, the+-- bigger the fee.+valueDependentEstimator :: FeeParameters i o -> FeeEstimator i o+valueDependentEstimator+    FeeParameters {feePerTransaction, feePerTransactionEntry} =+        FeeEstimator $ \s -> Fee $ maybe C.zero+            (C.add (unFee feePerTransaction))+            (C.mul+                (unFee feePerTransactionEntry)+                (length (inputs s) + length (outputs s) + size (change s)))+  where+    size :: [Coin] -> Int+    size = sum . fmap sizeOfOne+      where+        sizeOfOne :: Coin -> Int+        sizeOfOne coin+            | coin < C.coinFromNatural 1_000   = 1+            | coin < C.coinFromNatural 10_000  = 2+            | coin < C.coinFromNatural 100_000 = 3+            | otherwise                        = 4++instance Arbitrary (FeeOptions i o) where+    arbitrary = do+        dustThreshold <- unsafeDustThreshold @Int <$> choose (0, 10)+        feeEstimator <- oneof+            [ stableEstimator <$> arbitrary+            , valueDependentEstimator <$> arbitrary+            ]+        feeBalancingPolicy <- elements [RequireBalancedFee, RequireMinimalFee]+        return $ FeeOptions {dustThreshold, feeEstimator, feeBalancingPolicy}++instance Arbitrary a => Arbitrary (NonEmpty a) where+    arbitrary = do+        tailLength <- choose (0, 10)+        (:|) <$> arbitrary <*> replicateM tailLength arbitrary+    shrink = genericShrink++instance Show (FeeOptions i o) where+    show (FeeOptions _ dust policy) = show (dust, policy)
+ src/test/Cardano/CoinSelection/TypesSpec.hs view
@@ -0,0 +1,257 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Cardano.CoinSelection.TypesSpec+    ( spec+    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMap (..), coinMapValue )+import Cardano.Test.Utilities+    ( Address (..)+    , Hash (..)+    , ShowFmt (..)+    , TxIn (..)+    , TxOut (..)+    , excluding+    , isSubsetOf+    , restrictedBy+    , restrictedTo+    , unsafeCoin+    , unsafeFromHex+    )+import Data.Set+    ( Set, (\\) )+import Internal.Coin+    ( Coin )+import Test.Hspec+    ( Spec, describe, it )+import Test.QuickCheck+    ( Arbitrary (..)+    , Property+    , checkCoverage+    , choose+    , cover+    , oneof+    , property+    , scale+    , vectorOf+    , (===)+    )++import qualified Data.Map.Strict as Map+import qualified Data.Set as Set+import qualified Internal.Coin as C++spec :: Spec+spec = do++    describe "Lemma 2.1 - Properties of UTxO operations" $ do+        it "2.1.1) ins⊲ u ⊆ u"+            (checkCoverage $ prop_2_1_1 @TxIn)+        it "2.1.2) ins⋪ u ⊆ u"+            (checkCoverage $ prop_2_1_2 @TxIn)+        it "2.1.3) u ⊳ outs ⊆ u"+            (checkCoverage $ prop_2_1_3 @TxIn)+        it "2.1.4) ins⊲ (u ⋃ v) = (ins⊲ u) ⋃ (ins⊲ v)"+            (checkCoverage $ prop_2_1_4 @TxIn)+        it "2.1.5) ins⋪ (u ⋃ v) = (ins⋪ u) ⋃ (ins⋪ v)"+            (checkCoverage $ prop_2_1_5 @TxIn)+        it "2.1.6) (dom u ⋂ ins) ⊲ u = ins⊲ u"+            (checkCoverage $ prop_2_1_6 @TxIn)+        it "2.1.7) (dom u ⋂ ins) ⋪ u = ins⋪ u"+            (checkCoverage $ prop_2_1_7 @TxIn)+        it "2.1.8) (dom u ⋃ ins) ⋪ (u ⋃ v) = (ins ⋃ dom u) ⋪ v"+            (checkCoverage $ prop_2_1_8 @TxIn)+        it "2.1.9) ins⋪ u = (dom u \\ ins)⊲ u"+            (checkCoverage $ prop_2_1_9 @TxIn)++    describe "Lemma 2.6 - Properties of balance" $ do+        it "2.6.1) dom u ⋂ dom v ==> balance (u ⋃ v) = balance u + balance v"+            (checkCoverage $ prop_2_6_1 @TxIn)+        it "2.6.2) balance (ins⋪ u) = balance u - balance (ins⊲ u)"+            (checkCoverage $ prop_2_6_2 @TxIn)++--------------------------------------------------------------------------------+-- Wallet Specification - Lemma 2.1 - Properties of UTxO operations+--------------------------------------------------------------------------------++prop_2_1_1 :: Ord u => (Set u, CoinMap u) -> Property+prop_2_1_1 (ins, u) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop = (u `restrictedBy` ins) `isSubsetOf` u++prop_2_1_2 :: Ord u => (Set u, CoinMap u) -> Property+prop_2_1_2 (ins, u) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop = (u `excluding` ins) `isSubsetOf` u++prop_2_1_3 :: Ord u => (Set Coin, CoinMap u) -> Property+prop_2_1_3 (outs, u) =+    cover 50 cond "u ⋂ outs ≠ ∅" (property prop)+  where+    cond = not $ Set.null $+        Set.fromList (Map.elems (unCoinMap u)) `Set.intersection` outs+    prop = (u `restrictedTo` outs) `isSubsetOf` u++prop_2_1_4 :: (Ord u, Show u) => (Set u, CoinMap u, CoinMap u) -> Property+prop_2_1_4 (ins, u, v) =+    cover 50 cond "(dom u ⋃ dom v) ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ Set.union (dom u) (dom v) `Set.intersection` ins+    prop =+        ((u <> v) `restrictedBy` ins)+            ===+        (u `restrictedBy` ins) <> (v `restrictedBy` ins)++prop_2_1_5 :: (Ord u, Show u) => (Set u, CoinMap u, CoinMap u) -> Property+prop_2_1_5 (ins, u, v) =+    cover 50 cond "(dom u ⋃ dom v) ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ Set.union (dom u) (dom v) `Set.intersection` ins+    prop =+        ((u <> v) `excluding` ins)+            ===+        (u `excluding` ins) <> (v `excluding` ins)++prop_2_1_6 :: (Ord u, Show u) => (Set u, CoinMap u) -> Property+prop_2_1_6 (ins, u) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop =+        (u `restrictedBy` (dom u `Set.intersection` ins))+            ===+        (u `restrictedBy` ins)++prop_2_1_7 :: (Ord u, Show u) => (Set u, CoinMap u) -> Property+prop_2_1_7 (ins, u) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop =+        (u `excluding` (dom u `Set.intersection` ins))+            ===+        (u `excluding` ins)++prop_2_1_8 :: (Ord u, Show u) => (Set u, CoinMap u, CoinMap u) -> Property+prop_2_1_8 (ins, u, v) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop =+        ((u <> v) `excluding` (dom u <> ins))+            ===+        v `excluding` (ins <> dom u)++prop_2_1_9 :: (Ord u, Show u) => (Set u, CoinMap u) -> Property+prop_2_1_9 (ins, u) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop = (u `excluding` ins) === u `restrictedBy` (dom u \\ ins)++--------------------------------------------------------------------------------+-- Wallet Specification - Lemma 2.6 - Properties of Balance+--------------------------------------------------------------------------------++prop_2_6_1 :: Ord u => (CoinMap u, CoinMap u) -> Property+prop_2_6_1 (u, v) =+    cover 50 cond "u ≠ ∅ , v ≠ ∅" (property prop)+  where+    -- NOTE:+    -- A precondition (u ⋂ v = ∅ ) is hard to satisfy because our generators+    -- are built in order to not be 'too entropic'. So, we better just create+    -- a v' that has no overlap with u.+    v' = v `excluding` dom u+    cond = not (u `isSubsetOf` mempty || v' `isSubsetOf` mempty)+    prop = coinMapValue (u <> v')+        === coinMapValue u `C.add` coinMapValue v'++prop_2_6_2 :: Ord u => (Set u, CoinMap u) -> Property+prop_2_6_2 (ins, u) =+    cover 50 cond "dom u ⋂ ins ≠ ∅" (property prop)+  where+    cond = not $ Set.null $ dom u `Set.intersection` ins+    prop =+        Just (coinMapValue (u `excluding` ins))+            ===+        coinMapValue u `C.sub`+            coinMapValue (u `restrictedBy` ins)++--------------------------------------------------------------------------------+-- UTxO Utilities+--------------------------------------------------------------------------------++-- | Extracts the domain of a UTxO: the set of references to unspent transaction+--   ouputs.+dom :: CoinMap u -> Set u+dom (CoinMap utxo) = Map.keysSet utxo++--------------------------------------------------------------------------------+-- Arbitrary Instances+--+-- Arbitrary instances define here aren't necessarily reflecting on real-life+-- scenario, but they help test the property above by constructing data+-- structures that don't have much entropy and therefore, allow us to even test+-- something when checking for intersections and set restrictions!+--------------------------------------------------------------------------------++deriving instance Arbitrary a => Arbitrary (ShowFmt a)++instance Arbitrary (Hash "Tx") where+    -- No Shrinking+    arbitrary = oneof+        [ pure $ Hash $ unsafeFromHex+            "0000000000000000000000000000000000000000000000000000000000000001"+        , pure $ Hash $ unsafeFromHex+            "0000000000000000000000000000000000000000000000000000000000000002"+        , pure $ Hash $ unsafeFromHex+            "0000000000000000000000000000000000000000000000000000000000000003"+        ]++instance Arbitrary Address where+    -- No Shrinking+    arbitrary = oneof+        [ pure $ Address "ADDR01"+        , pure $ Address "ADDR02"+        , pure $ Address "ADDR03"+        ]++instance Arbitrary Coin where+    -- No Shrinking+    arbitrary = unsafeCoin @Int <$> choose (0, 3)++instance Arbitrary TxOut where+    -- No Shrinking+    arbitrary = TxOut+        <$> arbitrary+        <*> arbitrary++instance Arbitrary TxIn where+    -- No Shrinking+    arbitrary = TxIn+        <$> arbitrary+        <*> scale (`mod` 3) arbitrary -- No need for a crazy high indexes++instance (Arbitrary u, Ord u) => Arbitrary (CoinMap u) where+    shrink (CoinMap utxo) = CoinMap <$> shrink utxo+    arbitrary = do+        n <- choose (0, 10)+        utxo <- zip+            <$> vectorOf n arbitrary+            <*> vectorOf n arbitrary+        return $ CoinMap $ Map.fromList utxo
+ src/test/Cardano/CoinSelectionSpec.hs view
@@ -0,0 +1,460 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Cardano.CoinSelectionSpec+    ( spec++    -- * Export used to test various coin selection implementations+    , CoinSelectionFixture (..)+    , CoinSelectionTestResult (..)+    , CoinSelProp (..)+    , coinSelectionUnitTest+    ) where++-- | This module contains shared functionality for coin selection tests.+--+-- Coin selection algorithms share a common interface, and therefore it makes+-- sense for them to also share arbitrary instances and tests.++import Prelude++import Cardano.CoinSelection+    ( CoinMap (..)+    , CoinMapEntry (..)+    , CoinSelection (..)+    , CoinSelectionAlgorithm (..)+    , CoinSelectionError (..)+    , CoinSelectionLimit (..)+    , CoinSelectionParameters (..)+    , CoinSelectionResult (..)+    , coinMapFromList+    , coinMapToList+    , coinMapValue+    , sumChange+    , sumInputs+    , sumOutputs+    )+import Cardano.Test.Utilities+    ( Address (..), Hash (..), ShowFmt (..), TxIn (..), unsafeCoin )+import Control.Arrow+    ( (&&&) )+import Control.Monad.Trans.Except+    ( runExceptT )+import Data.Function+    ( (&) )+import Data.List.NonEmpty+    ( NonEmpty (..) )+import Data.Maybe+    ( catMaybes )+import Data.Set+    ( Set )+import Data.Word+    ( Word16, Word8 )+import Fmt+    ( Buildable (..), blockListF, nameF )+import Internal.Coin+    ( Coin, coinToIntegral )+import Test.Hspec+    ( Spec, SpecWith, describe, it, shouldBe )+import Test.QuickCheck+    ( Arbitrary (..)+    , Confidence (..)+    , Gen+    , NonEmptyList (..)+    , Property+    , arbitraryBoundedIntegral+    , checkCoverage+    , checkCoverageWith+    , choose+    , cover+    , elements+    , generate+    , genericShrink+    , oneof+    , property+    , scale+    , vectorOf+    )+import Test.QuickCheck.Monadic+    ( monadicIO )+import Test.Vector.Shuffle+    ( shuffle )++import qualified Data.ByteString as BS+import qualified Data.List as L+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as Map+import qualified Data.Set as Set+import qualified Internal.Coin as C+import qualified Test.QuickCheck.Monadic as QC++spec :: Spec+spec = do+    describe "CoinMap properties" $ do+        it "CoinMap coverage is adequate" $+            checkCoverage $ prop_CoinMap_coverage @Int+        it "CoinMapEntry coverage is adequate" $+            checkCoverage $ prop_CoinMapEntry_coverage @Int+        it "coinMapFromList preserves total value for each unique key" $+            checkCoverage $+                prop_coinMapFromList_preservesTotalValueForEachUniqueKey @Int+        it "coinMapFromList preserves total value" $+            checkCoverage $ prop_coinMapFromList_preservesTotalValue @Int+        it "coinMapToList preserves total value" $+            checkCoverage $ prop_coinMapToList_preservesTotalValue @Int+        it "coinMapFromList . coinMapToList = id" $+            checkCoverage $ prop_coinMapToList_coinMapFromList @Int+        it "coinMapToList . coinMapFromList = id (when no keys duplicated)" $+            checkCoverage $ prop_coinMapFromList_coinMapToList @Int+        it "coinMapToList order deterministic" $+            checkCoverageWith lowerConfidence $+                prop_coinMapToList_orderDeterministic @Int++    describe "CoinSelection properties" $ do+        it "monoidal append preserves keys" $+            checkCoverage $ prop_coinSelection_mappendPreservesKeys @Int @Int+        it "monoidal append preserves value" $+            checkCoverage $+                prop_coinSelection_mappendPreservesTotalValue @Int @Int++  where+    lowerConfidence :: Confidence+    lowerConfidence = Confidence (10^(6 :: Integer)) 0.75++--------------------------------------------------------------------------------+-- Properties+--------------------------------------------------------------------------------++prop_CoinMap_coverage+    :: CoinMap u+    -> Property+prop_CoinMap_coverage m = property+    $ cover 10 (null m)+        "coin map is empty"+    $ cover 10 (length m == 1)+        "coin map has one entry"+    $ cover 10 (length m >= 2)+        "coin map has multiple entries"+    $ cover 10 (1 == length (Set.fromList $ entryValue <$> coinMapToList m))+        "coin map has one unique value"+    $ cover 10 (1 < length (Set.fromList $ entryValue <$> coinMapToList m))+        "coin map has several unique values"+    True++prop_CoinMapEntry_coverage :: forall u . Ord u => [CoinMapEntry u] -> Property+prop_CoinMapEntry_coverage entries = property+    $ cover 2 (null entries)+        "coin map entry list is empty"+    $ cover 2 (length entries == 1)+        "coin map entry list has one entry"+    $ cover 2 (length entries == 2)+        "coin map entry list has two entries"+    $ cover 2 (length entries >= 3)+        "coin map entry list has multiple entries"+    $ cover 2 (not (null entries) && length uniqueKeys == 1)+        "coin map entry list has one unique key"+    $ cover 2 (not (null entries) && length uniqueKeys == 2)+        "coin map entry list has two unique keys"+    $ cover 2 (not (null entries) && length uniqueKeys >= 3)+        "coin map entry list has multiple unique keys"+    $ cover 2 (not (null entries) && null duplicateKeys)+        "coin map entry list has no duplicate keys"+    $ cover 2 (not (null entries) && length duplicateKeys == 1)+        "coin map entry list has one duplicate key"+    $ cover 2 (not (null entries) && length duplicateKeys == 2)+        "coin map entry list has two duplicate keys"+    $ cover 2 (not (null entries) && length duplicateKeys >= 3)+        "coin map entry list has multiple duplicate keys"+    True+  where+    uniqueKeys :: Set u+    uniqueKeys = entries+        & fmap entryKey+        & Set.fromList+    duplicateKeys :: Set u+    duplicateKeys = entries+        & fmap (entryKey &&& const (1 :: Int))+        & Map.fromListWith (+)+        & Map.filter (> 1)+        & Map.keysSet++prop_coinMapFromList_preservesTotalValueForEachUniqueKey+    :: (Ord u, Show u)+    => [CoinMapEntry u]+    -> Property+prop_coinMapFromList_preservesTotalValueForEachUniqueKey entries = property $+    mkEntryMap entries+        `shouldBe`+        mkEntryMap (coinMapToList (coinMapFromList entries))+  where+    mkEntryMap+        = Map.fromListWith C.add+        . fmap (entryKey &&& entryValue)++prop_coinMapFromList_preservesTotalValue+    :: Ord u+    => [CoinMapEntry u]+    -> Property+prop_coinMapFromList_preservesTotalValue entries = property $+    mconcat (entryValue <$> entries)+        `shouldBe` coinMapValue (coinMapFromList entries)++prop_coinMapToList_preservesTotalValue+    :: CoinMap u+    -> Property+prop_coinMapToList_preservesTotalValue m = property $+    mconcat (entryValue <$> coinMapToList m)+        `shouldBe` coinMapValue m++prop_coinMapToList_coinMapFromList+    :: (Ord u, Show u)+    => CoinMap u+    -> Property+prop_coinMapToList_coinMapFromList m = property $+    coinMapFromList (coinMapToList m)+        `shouldBe` m++prop_coinMapFromList_coinMapToList+    :: (Ord u, Show u)+    => [CoinMapEntry u]+    -> Property+prop_coinMapFromList_coinMapToList entries+    | duplicateKeyCount == 0 =+        property $ x `shouldBe` y+    | otherwise =+        property $ length x > length y+  where+    x = L.sort entries+    y = L.sort $ coinMapToList $ coinMapFromList entries++    duplicateKeyCount :: Int+    duplicateKeyCount = entries+        & fmap (entryKey &&& const (1 :: Int))+        & Map.fromListWith (+)+        & Map.filter (> 1)+        & Map.keysSet+        & length++prop_coinMapToList_orderDeterministic+    :: Ord u => CoinMap u -> Property+prop_coinMapToList_orderDeterministic u = monadicIO $ QC.run $ do+    let list0 = coinMapToList u+    list1 <- shuffle list0+    return $+        cover 10 (list0 /= list1) "shuffled" $+        list0 == coinMapToList (coinMapFromList list1)++prop_coinSelection_mappendPreservesKeys+    :: (Ord i, Ord o, Show i, Show o)+    => CoinSelection i o+    -> CoinSelection i o+    -> Property+prop_coinSelection_mappendPreservesKeys s1 s2 = property $ do+    Map.keysSet (unCoinMap $ inputs $ s1 <> s2)+        `shouldBe`+        Map.keysSet (unCoinMap $ inputs s1)+        `Set.union`+        Map.keysSet (unCoinMap $ inputs s2)+    Map.keysSet (unCoinMap $ outputs $ s1 <> s2)+        `shouldBe`+        Map.keysSet (unCoinMap $ outputs s1)+        `Set.union`+        Map.keysSet (unCoinMap $ outputs s2)++prop_coinSelection_mappendPreservesTotalValue+    :: (Ord i, Ord o)+    => CoinSelection i o+    -> CoinSelection i o+    -> Property+prop_coinSelection_mappendPreservesTotalValue s1 s2 = property $ do+    sumInputs  s1 <> sumInputs  s2 `shouldBe` sumInputs  (s1 <> s2)+    sumOutputs s1 <> sumOutputs s2 `shouldBe` sumOutputs (s1 <> s2)+    sumChange  s1 <> sumChange  s2 `shouldBe` sumChange  (s1 <> s2)+    change     s1 <> change     s2 `shouldBe` change     (s1 <> s2)++--------------------------------------------------------------------------------+-- Coin Selection - Unit Tests+--------------------------------------------------------------------------------++-- | Data for running+data CoinSelProp i o = CoinSelProp+    { csUtxO :: CoinMap i+        -- ^ Available UTxO for the selection+    , csOuts :: CoinMap o+        -- ^ Requested outputs for the payment+    } deriving Show++instance (Buildable i, Buildable o) => Buildable (CoinSelProp i o) where+    build (CoinSelProp utxo outs) = mempty+        <> nameF "utxo" (blockListF $ coinMapToList utxo)+        <> nameF "outs" (blockListF $ coinMapToList outs)++-- | A fixture for testing the coin selection+data CoinSelectionFixture i o = CoinSelectionFixture+    { maxNumOfInputs :: Word16+        -- ^ Maximum number of inputs that can be selected+    , utxoInputs :: [Integer]+        -- ^ Value (in Lovelace) & number of available coins in the UTxO+    , txOutputs :: [Integer]+        -- ^ Value (in Lovelace) & number of requested outputs+    }++-- | Testing-friendly format for 'CoinSelection' results of unit tests.+data CoinSelectionTestResult = CoinSelectionTestResult+    { rsInputs :: [Integer]+    , rsChange :: [Integer]+    , rsOutputs :: [Integer]+    } deriving (Eq, Show)++sortCoinSelectionTestResult+    :: CoinSelectionTestResult -> CoinSelectionTestResult+sortCoinSelectionTestResult (CoinSelectionTestResult is cs os) =+    CoinSelectionTestResult (L.sort is) (L.sort cs) (L.sort os)++-- | Generate a 'UTxO' and 'TxOut' matching the given 'Fixture', and perform+-- the given coin selection on it.+coinSelectionUnitTest+    :: CoinSelectionAlgorithm TxIn Address IO+    -> String+    -> Either CoinSelectionError CoinSelectionTestResult+    -> CoinSelectionFixture TxIn Address+    -> SpecWith ()+coinSelectionUnitTest alg lbl expected (CoinSelectionFixture n utxoF outsF) =+    it title $ do+        (utxo,txOuts) <- setup+        result <- runExceptT $ do+            CoinSelectionResult (CoinSelection inps outs chngs) _ <-+                selectCoins alg $+                    CoinSelectionParameters utxo txOuts selectionLimit+            return $ CoinSelectionTestResult+                { rsInputs = coinToIntegral . entryValue <$> coinMapToList inps+                , rsChange = coinToIntegral <$> chngs+                , rsOutputs = coinToIntegral . entryValue <$> coinMapToList outs+                }+        fmap sortCoinSelectionTestResult result+            `shouldBe` fmap sortCoinSelectionTestResult expected+  where+    selectionLimit = CoinSelectionLimit $ const n++    title :: String+    title = mempty+        <> if null lbl then "" else lbl <> ":\n\t"+        <> "max=" <> show n+        <> ", UTxO=" <> show utxoF+        <> ", Output=" <> show outsF+        <> " --> " <> show (rsInputs <$> expected)++    setup :: IO (CoinMap TxIn, CoinMap Address)+    setup = do+        utxo <- generate (genUTxO utxoF)+        outs <- generate (genOutputs outsF)+        pure (utxo, outs)++--------------------------------------------------------------------------------+-- Arbitrary Instances+--------------------------------------------------------------------------------++deriving instance Arbitrary a => Arbitrary (ShowFmt a)++instance (Arbitrary i, Arbitrary o, Ord i, Ord o) =>+    Arbitrary (CoinSelection i o)+  where+    arbitrary = CoinSelection+        <$> arbitrary+        <*> arbitrary+        <*> arbitrary+    shrink = genericShrink++instance Arbitrary (CoinSelectionLimit) where+    arbitrary = do+        -- NOTE Functions have to be decreasing functions+        fn <- elements+            [ (maxBound -)+            , \x ->+                if x > maxBound `div` 2+                    then maxBound+                    else maxBound - (2 * x)+            , const 42+            ]+        pure $ CoinSelectionLimit fn++instance Show (CoinSelectionLimit) where+    show _ = "CoinSelectionLimit"++instance Arbitrary a => Arbitrary (NonEmpty a) where+    shrink xs = catMaybes (NE.nonEmpty <$> shrink (NE.toList xs))+    arbitrary = do+        n <- choose (1, 10)+        NE.fromList <$> vectorOf n arbitrary++instance (Arbitrary i, Arbitrary o, Ord i, Ord o) =>+    Arbitrary (CoinSelProp i o)+  where+    shrink (CoinSelProp utxo outs) = uncurry CoinSelProp <$> zip+        (shrink utxo)+        (coinMapFromList <$> filter (not . null) (shrink (coinMapToList outs)))+    arbitrary = CoinSelProp+        <$> (coinMapFromList . getNonEmpty <$> arbitrary)+        <*> (coinMapFromList . getNonEmpty <$> arbitrary)++instance Arbitrary Address where+    shrink _ = []+    arbitrary = do+        bytes <- BS.pack <$> vectorOf 8 arbitraryBoundedIntegral+        pure $ Address bytes++instance Arbitrary Coin where+    -- No Shrinking+    arbitrary = unsafeCoin @Int <$> choose (1, 100000)++instance Arbitrary TxIn where+    -- No Shrinking+    arbitrary = TxIn+        <$> arbitrary+        <*> scale (`mod` 3) arbitrary -- No need for a high indexes++instance Arbitrary (Hash "Tx") where+    -- No Shrinking+    arbitrary = do+        wds <- vectorOf 10 arbitrary :: Gen [Word8]+        let bs = BS.pack wds+        pure $ Hash bs++instance Arbitrary a => Arbitrary (CoinMapEntry a) where+    -- No Shrinking+    arbitrary = CoinMapEntry+        <$> arbitrary+        <*> arbitrary++instance (Arbitrary a, Ord a) => Arbitrary (CoinMap a) where+    shrink (CoinMap m) = CoinMap <$> shrink m+    arbitrary = do+        n <- oneof+            [ pure 0+            , pure 1+            , choose (2, 100)+            ]+        entries <- zip+            <$> vectorOf n arbitrary+            <*> vectorOf n arbitrary+        return $ CoinMap $ Map.fromList entries++genUTxO :: [Integer] -> Gen (CoinMap TxIn)+genUTxO coins = do+    let n = length coins+    inps <- vectorOf n arbitrary+    return $ CoinMap $ Map.fromList $ zip inps (unsafeCoin <$> coins)++genOutputs :: [Integer] -> Gen (CoinMap Address)+genOutputs coins = do+    let n = length coins+    outs <- vectorOf n arbitrary+    return $ coinMapFromList $ zipWith CoinMapEntry outs (map unsafeCoin coins)
+ src/test/Cardano/Test/Utilities.hs view
@@ -0,0 +1,264 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++-- | Utility functions, types, and type class instances used purely for testing.+--+-- Copyright: © 2018-2020 IOHK+-- License: Apache-2.0+--+module Cardano.Test.Utilities+    (+    -- * Addresses+      Address (..)++    -- * Hashes+    , Hash (..)++    -- * Formatting+    , ShowFmt (..)++    -- * Transactions+    , TxIn (..)+    , TxOut (..)++    -- * UTxO Operations+    , excluding+    , isSubsetOf+    , restrictedBy+    , restrictedTo++    -- * Unsafe Operations+    , unsafeCoin+    , unsafeDustThreshold+    , unsafeFee+    , unsafeFromHex++    ) where++import Prelude++import Cardano.CoinSelection+    ( CoinMap (..), CoinMapEntry (..), CoinSelection (..), coinMapToList )+import Cardano.CoinSelection.Fee+    ( DustThreshold (..), Fee (..) )+import Control.DeepSeq+    ( NFData (..) )+import Data.ByteArray+    ( ByteArrayAccess )+import Data.ByteArray.Encoding+    ( Base (Base16), convertFromBase, convertToBase )+import Data.ByteString+    ( ByteString )+import Data.Maybe+    ( fromMaybe )+import Data.Set+    ( Set )+import Data.Word+    ( Word32 )+import Fmt+    ( Buildable (..)+    , blockListF+    , fmt+    , listF+    , nameF+    , ordinalF+    , prefixF+    , suffixF+    )+import GHC.Generics+    ( Generic )+import GHC.Stack+    ( HasCallStack )+import GHC.TypeLits+    ( Symbol )+import Internal.Coin+    ( Coin, coinFromIntegral )+import Numeric.Natural+    ( Natural )+import Quiet+    ( Quiet (Quiet) )++import qualified Data.Map.Strict as Map+import qualified Data.Set as Set+import qualified Data.Text.Encoding as T+import qualified Internal.Coin as C++--------------------------------------------------------------------------------+-- Addresses+--------------------------------------------------------------------------------++newtype Address = Address+    { unAddress :: ByteString }+    deriving stock (Eq, Generic, Ord)+    deriving Show via (Quiet Address)++instance NFData Address++instance Buildable Address where+    build addr = mempty+        <> prefixF 8 addrF+        <> "..."+        <> suffixF 8 addrF+      where+        addrF = build (toText addr)+        toText = T.decodeUtf8+            . convertToBase Base16+            . unAddress++--------------------------------------------------------------------------------+-- Unsafe Operations+--------------------------------------------------------------------------------++unsafeCoin :: (Integral i, Show i) => i -> Coin+unsafeCoin i = fromMaybe die $ coinFromIntegral i+  where+    die = error $ mconcat+        [ "Test suite attempted to create a coin with negative value: "+        , show i+        ]++unsafeDustThreshold :: (Integral i, Show i) => i -> DustThreshold+unsafeDustThreshold i = DustThreshold $ fromMaybe die $ coinFromIntegral i+  where+    die = error $ mconcat+        [ "Test suite attempted to create a dust theshold with negative value: "+        , show i+        ]++unsafeFee :: (Integral i, Show i) => i -> Fee+unsafeFee i = Fee $ fromMaybe die $ coinFromIntegral i+  where+    die = error $ mconcat+        [ "Test suite attempted to create a fee with negative value: "+        , show i+        ]++-- | Decode an hex-encoded 'ByteString' into raw bytes, or fail.+unsafeFromHex :: HasCallStack => ByteString -> ByteString+unsafeFromHex =+    either (error . show) id . convertFromBase @ByteString @ByteString Base16++--------------------------------------------------------------------------------+-- Hashes+--------------------------------------------------------------------------------++newtype Hash (tag :: Symbol) = Hash { getHash :: ByteString }+    deriving stock (Eq, Generic, Ord)+    deriving newtype (ByteArrayAccess)+    deriving Show via (Quiet (Hash tag))++instance NFData (Hash tag)++instance Buildable (Hash tag) where+    build h = mempty+        <> prefixF 8 builder+      where+        builder = build . toText $ h+        toText = T.decodeUtf8 . convertToBase Base16 . getHash++--------------------------------------------------------------------------------+-- Formatting+--------------------------------------------------------------------------------++-- | A polymorphic wrapper type with a custom 'Show' instance to display data+--   through 'Buildable' instances.+newtype ShowFmt a = ShowFmt { unShowFmt :: a }+    deriving (Generic, Eq, Ord)++instance NFData a => NFData (ShowFmt a)++instance Buildable a => Show (ShowFmt a) where+    show (ShowFmt a) = fmt (build a)++--------------------------------------------------------------------------------+-- Transactions+--------------------------------------------------------------------------------++data TxIn = TxIn+    { txinId+        :: !(Hash "Tx")+    , txinIx+        :: !Word32+    } deriving (Show, Generic, Eq, Ord)++instance NFData TxIn++instance Buildable TxIn where+    build txin = mempty+        <> ordinalF (txinIx txin + 1)+        <> " "+        <> build (txinId txin)++data TxOut = TxOut+    { txoutAddress+        :: !Address+    , txoutCoin+        :: !Coin+    } deriving (Show, Generic, Eq, Ord)++instance Buildable TxOut where+    build txout = mempty+        <> build (txoutCoin txout)+        <> " @ "+        <> prefixF 8 addrF+        <> "..."+        <> suffixF 8 addrF+      where+        addrF = build $ txoutAddress txout++--------------------------------------------------------------------------------+-- UTxO Operations+--------------------------------------------------------------------------------++-- | ins⋪ u+excluding :: Ord u => CoinMap u -> Set u -> CoinMap u+excluding (CoinMap utxo) =+    CoinMap . Map.withoutKeys utxo++-- | a ⊆ b+isSubsetOf :: Ord u => CoinMap u -> CoinMap u -> Bool+isSubsetOf (CoinMap a) (CoinMap b) =+    a `Map.isSubmapOf` b++-- | ins⊲ u+restrictedBy :: Ord u => CoinMap u -> Set u -> CoinMap u+restrictedBy (CoinMap utxo) =+    CoinMap . Map.restrictKeys utxo++-- | u ⊳ outs+restrictedTo :: CoinMap u -> Set Coin -> CoinMap u+restrictedTo (CoinMap utxo) outs =+    CoinMap $ Map.filter (`Set.member` outs) utxo++--------------------------------------------------------------------------------+-- Buildable Instances+--------------------------------------------------------------------------------++instance Buildable Coin where+    build = build . fromIntegral @Natural @Integer . C.coinToIntegral++instance Buildable a => Buildable (CoinMapEntry a) where+    build a = mempty+        <> build (entryKey a)+        <> ":"+        <> build (entryValue a)++instance (Buildable i, Buildable o) => Buildable (CoinSelection i o) where+    build s = mempty+        <> nameF "inputs"+            (blockListF $ coinMapToList $ inputs s)+        <> nameF "outputs"+            (blockListF $ coinMapToList $ outputs s)+        <> nameF "change"+            (listF $ change s)
+ src/test/Internal/CoinSpec.hs view
@@ -0,0 +1,164 @@+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Internal.CoinSpec+    ( spec+    ) where++import Prelude++import Data.Maybe+    ( catMaybes, fromMaybe )+import Internal.Coin+    ( Coin )+import Test.Hspec+    ( Spec, describe, it, shouldBe, shouldSatisfy )+import Test.QuickCheck+    ( Arbitrary (..)+    , NonNegative (..)+    , Property+    , checkCoverage+    , cover+    , oneof+    , property+    )++import qualified Internal.Coin as C++spec :: Spec+spec = do++    describe "Coin properties" $ do+        it "Only construction of non-negative values is possible." $+            checkCoverage prop_construction+        it "Coverage of generated values is acceptable." $+            checkCoverage prop_generation+        it "Addition" $+            checkCoverage prop_add+        it "Multiplication" $+            checkCoverage prop_mul+        it "Subtraction" $+            checkCoverage prop_sub+        it "Division" $+            checkCoverage prop_div+        it "Modulus" $+            checkCoverage prop_mod+        it "Distance" $+            checkCoverage prop_distance++prop_construction+    :: Integer+    -> Property+prop_construction i = property+    $ cover 10 (i < 0)+        "input is negative"+    $ cover 10 (i > 0)+        "input is positive"+    $ cover 2 (i == 0)+        "input is zero"+    $ if i < 0+        then C.coinFromIntegral i `shouldBe` Nothing+        else (C.coinToIntegral <$> C.coinFromIntegral i) `shouldBe` Just i++prop_generation+    :: Coin+    -> Property+prop_generation n = property+    $ cover 2 (n == C.zero)+        "value is zero"+    $ cover 2 (n == C.one)+        "value is one"+    $ cover 10 (n > C.one)+        "value is more than one"+    True++prop_add :: Coin -> Coin -> Property+prop_add x y = property $+    C.coinToIntegral @Integer (x `C.add` y)+        `shouldBe`+        (C.coinToIntegral x + C.coinToIntegral y)++prop_mul :: Coin -> Integer -> Property+prop_mul x y = property+    $ cover 2 (y == 0)+        "scaling factor is zero"+    $ cover 8 (y < 0)+        "scaling factor is negative"+    $ cover 8 (y > 0)+        "scaling factor is positive"+    $ case (x `C.mul` y) of+        Nothing ->+            y `shouldSatisfy` (< 0)+        Just r ->+            (C.coinToIntegral x * y) `shouldBe` C.coinToIntegral @Integer r++prop_sub :: Coin -> Coin -> Property+prop_sub x y = property+    $ cover 4 (x == y)+        "values are equal"+    $ cover 8 (x < y)+        "x < y"+    $ cover 8 (x > y)+        "x > y"+    $ case x `C.sub` y of+        Nothing ->+            x `shouldSatisfy` (< y)+        Just r ->+            C.coinToIntegral x - C.coinToIntegral y+                `shouldBe` C.coinToIntegral @Integer r++prop_div :: Coin -> Integer -> Property+prop_div x y = property+    $ cover 2 (y == 0)+        "denominator is zero"+    $ cover 8 (y < 0)+        "denominator is negative"+    $ cover 8 (y > 0)+        "denominator is positive"+    $ case (x `C.div` y) of+        Nothing ->+            y `shouldSatisfy` (<= 0)+        Just r ->+            (C.coinToIntegral x `div` y)+                `shouldBe` C.coinToIntegral @Integer r++prop_mod :: Coin -> Integer -> Property+prop_mod x y = property+    $ cover 2 (y == 0)+        "denominator is zero"+    $ cover 8 (y < 0)+        "denominator is negative"+    $ cover 8 (y > 0)+        "denominator is positive"+    $ case (x `C.mod` y) of+        Nothing ->+            y `shouldSatisfy` (<= 0)+        Just r ->+            (C.coinToIntegral x `mod` y)+                `shouldBe` C.coinToIntegral @Integer r++prop_distance :: Coin -> Coin -> Property+prop_distance x y = property+    $ cover 4 (x == y)+        "values are equal"+    $ cover 8 (x < y)+        "x < y"+    $ cover 8 (x > y)+        "x > y"+    $ C.coinToIntegral @Integer (x `C.distance` y)+        `shouldBe`+        abs (C.coinToIntegral x - C.coinToIntegral y)++instance Arbitrary Coin where+    arbitrary = oneof+        [ pure C.zero+        , pure C.one+        , somethingElse+        ]+      where+        somethingElse =+            fromMaybe C.zero+            . C.coinFromIntegral @Integer+            . getNonNegative <$> arbitrary+    shrink n = catMaybes+        $ C.coinFromIntegral @Integer <$> shrink (C.coinToIntegral @Integer n)
+ src/test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ src/test/Test/Vector/Shuffle.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeApplications #-}++module Test.Vector.Shuffle+    ( -- * Simple+      shuffle+    , shuffleNonEmpty++      -- * Advanced+    , mkSeed+    , shuffleWith+    , shuffleNonEmptyWith+    ) where++import Prelude++import Control.Monad+    ( forM_ )+import Control.Monad.Trans.Class+    ( lift )+import Control.Monad.Trans.State.Strict+    ( evalStateT, state )+import Crypto.Hash+    ( hash )+import Crypto.Hash.Algorithms+    ( MD5 )+import Data.List.NonEmpty+    ( NonEmpty (..) )+import Data.Maybe+    ( fromMaybe )+import Data.Text+    ( Text )+import Data.Vector.Mutable+    ( IOVector )+import Data.Word+    ( Word8 )+import System.Random+    ( RandomGen, StdGen, mkStdGen, newStdGen, randomR )++import qualified Data.ByteArray as BA+import qualified Data.ByteString as BS+import qualified Data.List.NonEmpty as NE+import qualified Data.Text.Encoding as T+import qualified Data.Vector as V+import qualified Data.Vector.Mutable as MV++-- | Generate a random generator seed from a text string+mkSeed :: Text -> StdGen+mkSeed = mkStdGen . toInt . quickHash . T.encodeUtf16LE+  where+    quickHash = BA.convert . hash @_ @MD5+    toInt = snd . BS.foldl' exponentiation (0,0)+      where+        exponentiation :: (Int, Int) -> Word8 -> (Int, Int)+        exponentiation (e, n) i = (e+1, n + fromIntegral i*2^e)++-- | Shuffles a list of elements.+--+-- >>> shuffle (outputs coinSel)+-- [...]+shuffle :: [a] -> IO [a]+shuffle xs = newStdGen >>= flip shuffleWith xs++-- | Like 'shuffle', but from a given seed. 'shuffle' will use a randomly+-- generate seed using 'newStdGen' from @System.Random@.+--+-- __Properties:__+--+-- - @shuffleWith g es == shuffleWith g es@+-- - @∃Δ> 1. g ≠g', length es > Δ⇒ shuffleWith g es ≠shuffleWith g' es@+shuffleWith :: RandomGen g => g -> [a] -> IO [a]+shuffleWith seed = modifyInPlace $ \v -> flip evalStateT seed $ do+    let (lo, hi) = (0, MV.length v - 1)+    forM_ [lo .. hi] $ \i -> do+      j <- fromInteger <$> state (randomR (fromIntegral lo, fromIntegral hi))+      lift $ swapElems v i j+  where+    swapElems :: IOVector a -> Int -> Int -> IO ()+    swapElems v i j = do+        x <- MV.read v i+        y <- MV.read v j+        MV.write v i y+        MV.write v j x++    modifyInPlace :: forall a. (IOVector a -> IO ()) -> [a] -> IO [a]+    modifyInPlace f xs = do+        v' <- V.thaw $ V.fromList xs+        f v'+        V.toList <$> V.freeze v'++-- | Shuffles a /non-empty/ list of elements.+--+-- See 'shuffle'.+--+shuffleNonEmpty :: NonEmpty a -> IO (NonEmpty a)+shuffleNonEmpty xs = newStdGen >>= flip shuffleNonEmptyWith xs++-- | Shuffles a /non-empty/ list of elements with the given random generator.+--+-- See 'shuffleWith'.+--+shuffleNonEmptyWith :: RandomGen g => g -> NonEmpty a -> IO (NonEmpty a)+shuffleNonEmptyWith g =+    fmap (fromMaybe raiseError . NE.nonEmpty) . shuffleWith g . NE.toList+  where+    raiseError = error "shuffleNonEmptyWith encountered an empty list."
+ src/test/Test/Vector/ShuffleSpec.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Test.Vector.ShuffleSpec+    ( spec+    ) where++import Prelude++import Data.List.NonEmpty+    ( NonEmpty (..) )+import Test.Hspec+    ( Spec, describe, it )+import Test.QuickCheck+    ( Arbitrary (..)+    , Confidence (..)+    , NonEmptyList (..)+    , Positive (..)+    , PrintableString (..)+    , Property+    , Testable+    , arbitrary+    , checkCoverageWith+    , cover+    , genericShrink+    , label+    , vectorOf+    , (==>)+    )+import Test.QuickCheck.Monadic+    ( assert, monadicIO, monitor, pick, run )+import Test.Vector.Shuffle+    ( mkSeed, shuffle, shuffleNonEmpty, shuffleWith )++import qualified Data.List as L+import qualified Data.List.NonEmpty as NE+import qualified Data.Text as T++spec :: Spec+spec = do+    describe "shuffle" $ do+        it "every list can be shuffled, ultimately" $+            check prop_shuffleCanShuffle+        it "shuffle is non-deterministic" $+            check prop_shuffleNotDeterministic+        it "sort (shuffle xs) == sort xs" $+            check prop_shufflePreserveElements++    describe "shuffleNonEmpty" $ do+        it "every non-empty list can be shuffled, ultimately" $+            check prop_shuffleNonEmptyCanShuffle+        it "shuffleNonEmpty is non-deterministic" $+            check prop_shuffleNonEmptyNotDeterministic+        it "sort (shuffleNonEmpty xs) == sort xs" $+            check prop_shuffleNonEmptyPreserveElements++    describe "shuffleWith / mkSeed" $ do+        it "shuffling with the same seed is deterministic" $+            check prop_shuffleWithDeterministic+        it "different seed means different shuffles" $+            check prop_shuffleDifferentSeed++  where+    check :: forall p. Testable p => p -> Property+    check = checkCoverageWith lowerConfidence++    lowerConfidence :: Confidence+    lowerConfidence = Confidence (10^(6 :: Integer)) 0.75++--------------------------------------------------------------------------------+-- Properties+--------------------------------------------------------------------------------++prop_shuffleCanShuffle+    :: NonEmptyList Int+    -> Property+prop_shuffleCanShuffle (NonEmpty xs) = monadicIO $ run $ do+    xs' <- shuffle xs+    return $ cover 90 (xs /= xs') "shuffled" ()++prop_shuffleNonEmptyCanShuffle+    :: NonEmpty Int+    -> Property+prop_shuffleNonEmptyCanShuffle xs = monadicIO $ run $ do+    xs' <- shuffleNonEmpty xs+    return $ cover 90 (xs /= xs') "shuffled" ()++prop_shuffleNotDeterministic+    :: NonEmptyList Int+    -> Property+prop_shuffleNotDeterministic (NonEmpty xs) = monadicIO $ run $ do+    xs1 <- shuffle xs+    xs2 <- shuffle xs+    return $ cover 90 (xs1 /= xs2) "not deterministic" ()++prop_shuffleNonEmptyNotDeterministic+    :: NonEmpty Int+    -> Property+prop_shuffleNonEmptyNotDeterministic xs = monadicIO $ run $ do+    xs1 <- shuffleNonEmpty xs+    xs2 <- shuffleNonEmpty xs+    return $ cover 90 (xs1 /= xs2) "not deterministic" ()++prop_shufflePreserveElements+    :: [Int]+    -> Property+prop_shufflePreserveElements xs = monadicIO $ run $ do+    xs' <- shuffle xs+    return $ cover 90 (not $ null xs) "non-empty" (L.sort xs == L.sort xs')++prop_shuffleNonEmptyPreserveElements+    :: NonEmpty Int+    -> Property+prop_shuffleNonEmptyPreserveElements xs = monadicIO $ run $ do+    xs' <- shuffleNonEmpty xs+    return $ cover 90 (not $ null xs) "non-empty" (NE.sort xs == NE.sort xs')++-- ∀(g :: RandomGen).+-- ∀(es :: [a]).+--+-- shuffleWith g es == shuffleWith g es+prop_shuffleWithDeterministic+    :: PrintableString+    -> NonEmptyList Int+    -> Property+prop_shuffleWithDeterministic (PrintableString seed) (NonEmpty xs) =+    monadicIO $ do+        ys0 <- run $ shuffleWith (mkSeed $ T.pack seed) xs+        ys1 <- run $ shuffleWith (mkSeed $ T.pack seed) xs+        monitor $ cover 90 (length xs > 1) "non singleton"+        assert (ys0 == ys1)++-- ∀(x0 : Text, x1 : Text). g0 = mkSeed x0, g1 = mkSeed x1+-- ∃(Δ: Int).+-- ∀(es :: [a]).+--+-- g0 ≠g1, length es > Δ⇒ shuffleWith g0 es ≠shuffleWith g1 es+prop_shuffleDifferentSeed+    :: (PrintableString, PrintableString)+    -> Positive Int+    -> Property+prop_shuffleDifferentSeed (x0, x1) (Positive len) = do+    x0 /= x1 ==> monadicIO $ do+        let g0 = mkSeed $ T.pack $ getPrintableString x0+        let g1 = mkSeed $ T.pack $ getPrintableString x1+        es <- pick $ vectorOf len (arbitrary @Int)+        ys0 <- run $ shuffleWith g0 es+        ys1 <- run $ shuffleWith g1 es+        monitor $ label (prettyLen es)+        monitor $ cover 90 (ys0 /= ys1) "different"+  where+    prettyLen :: [a] -> String+    prettyLen xs = case length xs of+        n | n <= 1 -> "singleton"+        n | n <= 10 -> "small list"+        _ -> "big list"++instance Arbitrary a => Arbitrary (NonEmpty a) where+    arbitrary = (:|) <$> arbitrary <*> arbitrary+    shrink = genericShrink