{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# 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 ( CoinMap (..) , CoinMapEntry (..) , CoinSelection (..) , CoinSelectionAlgorithm (..) , CoinSelectionError (..) , CoinSelectionLimit (..) , CoinSelectionParameters (..) , CoinSelectionResult (..) , InputLimitExceededError (..) , InputValueInsufficientError (..) , coinMapFromList , coinMapToList , coinMapValue ) import Control.Monad.Trans.Except ( ExceptT (..), throwE ) import Data.Function ( (&) ) import Data.Ord ( Down (..) ) import Data.Word ( Word16 ) 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. -- -- The Largest-First coin selection algorithm considers available inputs in -- /descending/ order of value, from /largest/ to /smallest/. -- -- When applied to a set of requested outputs, the algorithm repeatedly selects -- entries from the available inputs set until the total value of selected -- entries is greater than or equal to the total value of requested outputs. -- -- === Change Values -- -- If the total value of selected inputs is /greater than/ the total value of -- all requested outputs, the 'change' set of the resulting selection will -- contain /a single coin/ with the excess value. -- -- If the total value of selected inputs is /exactly equal to/ the total value -- of all requested outputs, the 'change' set of the resulting selection will -- be /empty/. -- -- === Failure Modes -- -- The algorithm terminates with an __error__ if: -- -- 1. The /total value/ of 'inputsAvailable' (the amount of money -- /available/) is /less than/ the total value of 'outputsRequested' (the -- amount of money /required/). -- -- See: __'InputValueInsufficientError'__. -- -- 2. It is not possible to cover the total value of 'outputsRequested' -- without selecting a number of inputs from 'inputsAvailable' that -- would exceed the maximum defined by 'limit'. -- -- See: __'InputLimitExceededError'__. -- -- @since 1.0.0 largestFirst :: (Ord i, Monad m) => CoinSelectionAlgorithm i o m largestFirst = CoinSelectionAlgorithm payForOutputs payForOutputs :: forall i o m . (Ord i, Monad m) => CoinSelectionParameters i o -> ExceptT CoinSelectionError m (CoinSelectionResult i o) payForOutputs params | amountAvailable < amountRequired = throwE $ InputValueInsufficient $ InputValueInsufficientError amountAvailable amountRequired | length inputsSelected > inputCountMax = throwE $ InputLimitExceeded $ InputLimitExceededError $ fromIntegral inputCountMax | otherwise = pure CoinSelectionResult {coinSelection, inputsRemaining} where amountAvailable = coinMapValue $ inputsAvailable params amountRequired = coinMapValue $ outputsRequested params coinSelection = CoinSelection { inputs = inputsSelected , outputs = outputsRequested params , change = filter (> C.zero) $ F.toList $ coinMapValue inputsSelected `C.sub` amountRequired } inputsAvailableDescending :: [CoinMapEntry i] inputsAvailableDescending = inputsAvailable params & coinMapToList & L.sortOn (Down . entryValue) inputCountMax :: Int inputCountMax = outputsRequested params & coinMapToList & length & fromIntegral @Int @Word16 & calculateLimit (limit params) & fromIntegral @Word16 @Int inputsSelected :: CoinMap i inputsSelected = inputsAvailableDescending & fmap entryValue & scanl1 (<>) & takeUntil (>= amountRequired) & zip inputsAvailableDescending & fmap fst & coinMapFromList inputsRemaining :: CoinMap i inputsRemaining = inputsAvailableDescending & drop (length inputsSelected) & coinMapFromList -------------------------------------------------------------------------------- -- Utilities -------------------------------------------------------------------------------- takeUntil :: (a -> Bool) -> [a] -> [a] takeUntil p = foldr (\x ys -> x : if p x then [] else ys) []