Skip to content

Commit 3bf08e4

Browse files
committed
add docs and tests
1 parent 08e2ca6 commit 3bf08e4

File tree

2 files changed

+142
-32
lines changed

2 files changed

+142
-32
lines changed

src/BotPlutusInterface/CoinSelection.hs

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Control.Lens (
2020
(^?),
2121
_Just,
2222
)
23-
import Control.Monad.Except (throwError)
23+
import Control.Monad.Except (foldM, throwError, unless)
2424
import Control.Monad.Freer (Eff, Member)
2525
import Control.Monad.Trans.Class (lift)
2626
import Control.Monad.Trans.Either (hoistEither, newEitherT, runEitherT)
@@ -49,16 +49,16 @@ import Plutus.V1.Ledger.Api (
4949
import Prettyprinter (pretty, (<+>))
5050
import Prelude
5151

52-
-- 'Search' represents the possible search strategy.
53-
data Search
52+
-- 'searchStrategy' represents the possible search strategy.
53+
data SearchStrategy
5454
= -- | This is a greedy search that searches for nearest utxo using l2norm.
5555
Greedy
5656
| -- | This is like greedy search, but here there's
5757
-- additonal goal that the change utxo should be equal to the output utxo.
5858
GreedyApprox
5959
deriving stock (Eq, Show)
6060

61-
instance Default Search where
61+
instance Default SearchStrategy where
6262
def = GreedyApprox
6363

6464
-- 'selectTxIns' selects utxos using default search strategy, it also preprocesses
@@ -111,14 +111,20 @@ selectTxIns originalTxIns utxosIndex outValue =
111111

112112
-- we use the default search strategy to get indexes of optimal utxos, these indexes are for the
113113
-- remainingUtxos, as we are sampling utxos from that set.
114-
selectedUtxosIdxs <- newEitherT $ selectTxIns' @w def (isSufficient outVec) outVec txInsVec remainingUtxosVec
114+
selectedUtxosIdxs <- newEitherT $ searchTxIns @w def (isSufficient outVec) outVec txInsVec remainingUtxosVec
115115

116116
lift $ printBpiLog @w (Debug [CoinSelectionLog]) $ "" <+> "Selected UTxOs Index: " <+> pretty selectedUtxosIdxs
117117

118118
let -- These are the selected utxos that we get using `selectedUtxosIdxs`.
119119
selectedUtxos :: [(TxOutRef, TxOut)]
120120
selectedUtxos = selectedUtxosIdxs ^.. folded . to (\idx -> remainingUtxos ^? ix idx) . folded
121121

122+
selectedVectors :: [Vector Integer]
123+
selectedVectors = selectedUtxosIdxs ^.. folded . to (\idx -> remainingUtxosVec ^? ix idx) . folded
124+
125+
finalTxInputVector <- hoistEither $ foldM addVec txInsVec selectedVectors
126+
unless (isSufficient outVec finalTxInputVector) (throwError "Insufficient Funds")
127+
122128
selectedTxIns <- hoistEither $ mapM txOutToTxIn selectedUtxos
123129

124130
lift $ printBpiLog @w (Debug [CoinSelectionLog]) $ "Selected TxIns: " <+> pretty selectedTxIns
@@ -136,16 +142,18 @@ selectTxIns originalTxIns utxosIndex outValue =
136142
Vec.all (== True) (Vec.zipWith (<=) outVec txInsVec)
137143
&& txInsVec /= zeroVec (length txInsVec)
138144

139-
selectTxIns' ::
145+
-- `searchTxIns` searches for optimal utxos for a transaction as input given
146+
-- current input vector, output vector and a list of all the remaining utxo vectors.
147+
searchTxIns ::
140148
forall (w :: Type) (effs :: [Type -> Type]).
141149
Member (PABEffect w) effs =>
142-
Search ->
150+
SearchStrategy ->
143151
(Vector Integer -> Bool) ->
144152
Vector Integer ->
145153
Vector Integer ->
146154
[Vector Integer] ->
147155
Eff effs (Either Text [Int])
148-
selectTxIns' searchStrategy stopSearch outVec txInsVec utxosVec
156+
searchTxIns searchStrategy stopSearch outVec txInsVec utxosVec
149157
| searchStrategy == Greedy =
150158
printBpiLog @w (Debug [CoinSelectionLog]) "Selecting UTxOs via greedy search"
151159
>> greedySearch @w stopSearch outVec txInsVec utxosVec
@@ -154,6 +162,9 @@ selectTxIns' searchStrategy stopSearch outVec txInsVec utxosVec
154162
>> greedyApprox @w stopSearch outVec txInsVec utxosVec
155163
| otherwise = return $ throwError "Not a valid search strategy."
156164

165+
-- `greedySearch` searches for utxos vectors for input to a transaction,
166+
-- this is achieved by selecting the utxo vector that have closest euclidean distance
167+
-- from output vector.
157168
greedySearch ::
158169
forall (w :: Type) (effs :: [Type -> Type]).
159170
Member (PABEffect w) effs =>
@@ -163,16 +174,24 @@ greedySearch ::
163174
[Vector Integer] ->
164175
Eff effs (Either Text [Int])
165176
greedySearch stopSearch outVec txInsVec utxosVec
177+
-- we stop the search if there are no utxos vectors left, as we will not be able to
178+
-- select any further utxos as input to a transaction.
166179
| null utxosVec =
167180
printBpiLog @w (Debug [CoinSelectionLog]) "Greedy: The list of remanining UTxO vectors in null."
168181
>> return (Right mempty)
182+
-- we stop the search is the predicate `stopSearch` is true.
169183
| stopSearch txInsVec =
170184
printBpiLog @w (Debug [CoinSelectionLog]) "Greedy: Stopping search early."
171185
>> return (Right mempty)
172186
| otherwise =
173187
runEitherT $ do
188+
-- Here, we calculate the euclidean distance of the following vectors:
189+
-- l2norm(inputVec + remaining UTxO vector (U1), output vector).
190+
-- where U1 is just a vector from a list utxosVec.
174191
utxosDist <- hoistEither $ mapM (addVec txInsVec) utxosVec >>= mapM (l2norm outVec)
175192

193+
-- Now, we fold the distances with their current indexes, and then
194+
-- sort (lowest to highest) them using the distance from output vector.
176195
let sortedDist :: [(Int, Float)]
177196
sortedDist =
178197
utxosDist ^.. ifolded . withIndex
@@ -183,11 +202,14 @@ greedySearch stopSearch outVec txInsVec utxosVec
183202
loop :: [(Int, Float)] -> Vector Integer -> Eff effs (Either Text [Int])
184203
loop [] _ = return $ Right mempty
185204
loop ((idx, _) : remSortedDist) newTxInsVec =
186-
if stopSearch newTxInsVec
205+
if stopSearch newTxInsVec -- we check if we should stop the search.
187206
then return $ Right mempty
188207
else runEitherT $ do
208+
-- Get the selected utxo vector given the current idx.
189209
selectedUtxoVec <-
190210
hoistEither $ maybeToRight "Out of bounds" (utxosVec ^? ix idx)
211+
212+
-- Add the selected utxo vector to the current tx input vector.
191213
newTxInsVec' <- hoistEither $ addVec newTxInsVec selectedUtxoVec
192214

193215
lift $
@@ -198,6 +220,21 @@ greedySearch stopSearch outVec txInsVec utxosVec
198220

199221
(idx :) <$> newEitherT (loop remSortedDist newTxInsVec')
200222

223+
-- 'greedyApprox' uses greedy search, but then it filters and add the utxo(s)
224+
-- such that the change vector is close to the output vector.
225+
--
226+
-- Eg: output vector: [100]
227+
-- input vector: [0]
228+
--
229+
-- utxos vector: [50],[210],[500],[10]
230+
--
231+
-- so, now the greedy search will select the following utxos:
232+
-- -- [10], [50], [210]
233+
--
234+
-- But, if we are using utxo vector with [210] then we don't need to
235+
-- consume vectors like [10] and [50].
236+
-- So, we filter such unnecessary vectors.
237+
--
201238
greedyApprox ::
202239
forall (w :: Type) (effs :: [Type -> Type]).
203240
Member (PABEffect w) effs =>
@@ -207,17 +244,26 @@ greedyApprox ::
207244
[Vector Integer] ->
208245
Eff effs (Either Text [Int])
209246
greedyApprox stopSearch outVec txInsVec utxosVec
247+
-- we stop the search if there are no utxos vectors left, as we will not be able to
248+
-- select any further utxos as input to a transaction.
210249
| null utxosVec =
211250
printBpiLog @w (Debug [CoinSelectionLog]) "Greedy Pruning: The list of remanining UTxO vectors in null."
212251
>> return (Right mempty)
252+
-- we stop the search is the predicate `stopSearch` is true.
213253
| stopSearch txInsVec =
214254
printBpiLog @w (Debug [CoinSelectionLog]) "Greedy Pruning: Stopping search early."
215255
>> return (Right mempty)
216256
| otherwise =
217257
runEitherT $ do
258+
-- Here, we get the selected indexes of utxo vectors using greedy search.
218259
selectedUtxosIdx <- newEitherT $ greedySearch @w stopSearch outVec txInsVec utxosVec
219260

220-
let revSelectedUtxosVec :: [Vector Integer]
261+
let -- Reverse the order of the selected vectors
262+
-- The Idea here is that, the vectors that are selected at
263+
-- last will have greater distance from the output vector.
264+
-- Hence, they may contain all the values that's required for
265+
-- the output vector.
266+
revSelectedUtxosVec :: [Vector Integer]
221267
revSelectedUtxosVec =
222268
List.reverse $ selectedUtxosIdx ^.. folded . to (\idx -> utxosVec ^? ix idx) . folded
223269

@@ -228,17 +274,29 @@ greedyApprox stopSearch outVec txInsVec utxosVec
228274
where
229275
loop :: Vector Integer -> [Int] -> [Vector Integer] -> Either Text [Int]
230276
loop newTxInsVec (idx : idxs) (vec : vecs) = do
277+
-- Add the selected utxo vector to the current tx input vector.
231278
newTxInsVec' <- addVec newTxInsVec vec
279+
280+
-- Get the old change vector
232281
changeVec <- subVec outVec newTxInsVec
282+
283+
-- Get the new change vector
233284
changeVec' <- subVec outVec newTxInsVec'
234285

286+
-- compare the distance between old change vector with output vector
287+
-- and new change vector with the output vector.
235288
case l2norm outVec changeVec' < l2norm outVec changeVec of
289+
-- If the distance between new change vector and output
290+
-- vector is smaller then we add that utxo vector
236291
True -> (idx :) <$> loop newTxInsVec' idxs vecs
292+
-- Else we check if we should stop the search here.
237293
False | stopSearch newTxInsVec -> Right mempty
294+
-- We add the current utxo vector.
238295
False -> (idx :) <$> loop newTxInsVec' idxs vecs
239296
loop _newTxInsVec [] [] = pure mempty
240297
loop _newTxInsVec _idxs _vecs = Left "Length of idxs and list of vecs are not same."
241298

299+
-- calculate euclidean distance of two vectors, of same length/dimension.
242300
l2norm :: Vector Integer -> Vector Integer -> Either Text Float
243301
l2norm v1 v2
244302
| length v1 == length v2 = Right $ sqrt $ fromInteger $ sum $ Vec.zipWith formula v1 v2
@@ -256,34 +314,25 @@ l2norm v1 v2
256314
formula :: Integer -> Integer -> Integer
257315
formula n1 n2 = (n1 - n2) ^ (2 :: Integer)
258316

317+
-- Add two vectors of same length.
259318
addVec :: Num n => Vector n -> Vector n -> Either Text (Vector n)
260319
addVec = opVec (+)
261320

321+
-- Substract two vectors of same length.
262322
subVec :: Num n => Vector n -> Vector n -> Either Text (Vector n)
263323
subVec = opVec (-)
264324

265-
opVec :: Num n => (forall a. Num a => a -> a -> a) -> Vector n -> Vector n -> Either Text (Vector n)
266-
opVec f v1 v2
267-
| length v1 == length v2 = Right $ Vec.zipWith f v1 v2
268-
| otherwise =
269-
Left $
270-
pack $
271-
"Error: The length of the vectors should be same for addition."
272-
<> "length of vector v1: "
273-
<> show (length v1)
274-
<> " "
275-
<> "length of vector v2: "
276-
<> show (length v2)
277-
<> "."
278-
325+
-- create zero vector of specified length.
279326
zeroVec :: Int -> Vector Integer
280327
zeroVec n = Vec.fromList $ replicate n 0
281328

329+
-- convert a value to a vector.
282330
valueToVec :: Set AssetClass -> Value -> Either Text (Vector Integer)
283331
valueToVec allAssetClasses v =
284332
maybeToRight "Error: Not able to uncons from empty vector." $
285333
(over _Just fst . uncons) $ valuesToVecs allAssetClasses [v]
286334

335+
-- convert values to a list of vectors.
287336
valuesToVecs :: Set AssetClass -> [Value] -> Vector (Vector Integer)
288337
valuesToVecs allAssetClasses values = Vec.fromList $ map toVec values
289338
where
@@ -292,6 +341,7 @@ valuesToVecs allAssetClasses values = Vec.fromList $ map toVec values
292341
fmap (Value.assetClassValueOf v) $
293342
allAssetClasses & id %~ (Vec.fromList . Set.toList)
294343

344+
-- As the name suggests, we get a set of all the unique assetclass from given the lists of values.
295345
uniqueAssetClasses :: [Value] -> Set AssetClass
296346
uniqueAssetClasses = Set.fromList . concatMap valueToAssetClass
297347
where
@@ -304,3 +354,18 @@ txOutToTxIn (txOutRef, txOut) =
304354
case Ledger.addressCredential (txOutAddress txOut) of
305355
PubKeyCredential _ -> Right $ Ledger.pubKeyTxIn txOutRef
306356
ScriptCredential _ -> Left "Cannot covert a script output to TxIn"
357+
358+
-- Apply a binary operation on two vectors of same length.
359+
opVec :: Num n => (forall a. Num a => a -> a -> a) -> Vector n -> Vector n -> Either Text (Vector n)
360+
opVec f v1 v2
361+
| length v1 == length v2 = Right $ Vec.zipWith f v1 v2
362+
| otherwise =
363+
Left $
364+
pack $
365+
"Error: The length of the vectors should be same for addition."
366+
<> "length of vector v1: "
367+
<> show (length v1)
368+
<> " "
369+
<> "length of vector v2: "
370+
<> show (length v2)
371+
<> "."

test/Spec/BotPlutusInterface/CoinSelection.hs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
{-# OPTIONS_GHC -Wno-unused-binds #-}
2+
13
module Spec.BotPlutusInterface.CoinSelection (tests) where
24

3-
import BotPlutusInterface.CoinSelection (uniqueAssetClasses)
4-
import Control.Lens (folded, to, (^..))
5-
import Data.Set (Set)
5+
import BotPlutusInterface.CoinSelection (selectTxIns, uniqueAssetClasses)
6+
import BotPlutusInterface.Effects (PABEffect)
7+
import Data.Default (def)
8+
import Data.Map qualified as Map
69
import Data.Set qualified as Set
10+
import Data.Text qualified as Text
711
import Ledger qualified
812
import Ledger.Ada qualified as Ada
913
import Ledger.Address (Address, PaymentPubKeyHash (PaymentPubKeyHash))
1014
import Ledger.Address qualified as Address
1115
import Ledger.CardanoWallet qualified as Wallet
1216
import Ledger.Crypto (PubKeyHash)
13-
import Ledger.Tx (Tx (..), TxIn (..), TxInType (..), TxOut (..), TxOutRef (..))
17+
import Ledger.Tx (TxIn (..), TxInType (..), TxOut (..), TxOutRef (..))
18+
import Ledger.Value (Value)
1419
import Ledger.Value qualified as Value
20+
import Spec.MockContract (runPABEffectPure)
1521
import Test.Tasty (TestTree, testGroup)
1622
import Test.Tasty.HUnit (Assertion, assertFailure, testCase, (@?=))
1723
import Prelude
@@ -20,7 +26,8 @@ tests :: TestTree
2026
tests =
2127
testGroup
2228
"BotPlutusInterface.CoinSelection"
23-
[ testCase "validAssetClasses" validAssetClasses
29+
[ testCase "Have All unique assetClasses" validAssetClasses
30+
, testCase "Coin selection greedy Approx" greedyApprox
2431
]
2532

2633
pkh1 :: PubKeyHash
@@ -37,11 +44,18 @@ txOutRef4 = TxOutRef "d8a5630a9d7e913f9d186c95e5138a239a4e79ece3414ac894dbf37280
3744
txOutRef5 = TxOutRef "34d491e0596b3a04be6e3442ebf115b33900f26e5e415e5151f820778ba576ed" 0
3845

3946
utxo1, utxo2, utxo3, utxo4, utxo5 :: (TxOutRef, TxOut)
40-
utxo1 = (txOutRef1, TxOut addr1 (Ada.lovelaceValueOf 1_100_000) Nothing)
41-
utxo2 = (txOutRef2, TxOut addr1 (Ada.lovelaceValueOf 1_000_000) Nothing)
47+
utxo1 = (txOutRef1, TxOut addr1 (Ada.lovelaceValueOf 1_400_000) Nothing)
48+
utxo2 = (txOutRef2, TxOut addr1 (Ada.lovelaceValueOf 1_200_000) Nothing)
4249
utxo3 = (txOutRef3, TxOut addr1 (Ada.lovelaceValueOf 900_000) Nothing)
4350
utxo4 = (txOutRef4, TxOut addr1 (Ada.lovelaceValueOf 800_000 <> Value.singleton "11223344" "Token1" 500) Nothing)
44-
utxo5 = (txOutRef4, TxOut addr1 (Ada.lovelaceValueOf 800_000 <> Value.singleton "44332211" "Token2" 500) Nothing)
51+
utxo5 = (txOutRef5, TxOut addr1 (Ada.lovelaceValueOf 600_000 <> Value.singleton "44332211" "Token2" 500) Nothing)
52+
53+
txIn1, txIn2, txIn3, txIn4, txIn5 :: TxIn
54+
txIn1 = TxIn txOutRef1 (Just ConsumePublicKeyAddress)
55+
txIn2 = TxIn txOutRef2 (Just ConsumePublicKeyAddress)
56+
txIn3 = TxIn txOutRef3 (Just ConsumePublicKeyAddress)
57+
txIn4 = TxIn txOutRef4 (Just ConsumePublicKeyAddress)
58+
txIn5 = TxIn txOutRef5 (Just ConsumePublicKeyAddress)
4559

4660
validAssetClasses :: Assertion
4761
validAssetClasses =
@@ -51,3 +65,34 @@ validAssetClasses =
5165
, Value.assetClass "44332211" "Token2"
5266
, Value.assetClass "11223344" "Token1"
5367
]
68+
69+
testOutputValue1, testOutputValue2, testOutputValue3, testOutputValue4 :: Value
70+
testOutputValue1 = Ada.lovelaceValueOf 1_000_000 <> Value.singleton "11223344" "Token1" 100
71+
testOutputValue2 = Ada.lovelaceValueOf 1_000_000
72+
testOutputValue3 =
73+
Ada.lovelaceValueOf 1_000_000 <> Value.singleton "11223344" "Token1" 100
74+
<> Value.singleton "44332211" "Token2" 50
75+
testOutputValue4 = Ada.lovelaceValueOf 1_500_000
76+
77+
greedyApprox :: Assertion
78+
greedyApprox = do
79+
let eresult =
80+
mapM
81+
( fst
82+
. runPABEffectPure def
83+
. selectTxIns @() @'[PABEffect ()] mempty (Map.fromList [utxo1, utxo2, utxo3, utxo4, utxo5])
84+
)
85+
[testOutputValue1, testOutputValue2, testOutputValue3, testOutputValue4]
86+
87+
expectedResults =
88+
map
89+
(Right . Set.fromList)
90+
[ [txIn2, txIn4]
91+
, [txIn2]
92+
, [txIn4, txIn5, txIn1]
93+
, [txIn1, txIn2]
94+
]
95+
96+
case eresult of
97+
Left e -> assertFailure (Text.unpack e)
98+
Right result -> result @?= expectedResults

0 commit comments

Comments
 (0)