Skip to content

Commit fcca512

Browse files
committed
add property test to check for valid balancing
1 parent e1533a6 commit fcca512

File tree

3 files changed

+119
-28
lines changed

3 files changed

+119
-28
lines changed

src/BotPlutusInterface/CoinSelection.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ selectTxIns originalTxIns utxosIndex outValue =
231231
selectedVectors = selectedUtxosIdxs ^.. folded . to (\idx -> remainingUtxosVec ^? ix idx) . folded
232232

233233
finalTxInputVector <- hoistEither $ foldM addVec txInsVec selectedVectors
234-
unless (isSufficient outVec finalTxInputVector) (throwError "Insufficient Funds")
234+
unless (isSufficient outVec finalTxInputVector) $ throwError "Insufficient Funds"
235235

236236
selectedTxIns <- hoistEither $ mapM txOutToTxIn selectedUtxos
237237

test/Spec/BotPlutusInterface/CoinSelection.hs

Lines changed: 114 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,28 @@ module Spec.BotPlutusInterface.CoinSelection (tests) where
44

55
import BotPlutusInterface.CoinSelection (selectTxIns, uniqueAssetClasses, valuesToVecs)
66
import BotPlutusInterface.Effects (PABEffect)
7-
import Control.Lens (folded, to, (^..))
8-
import Control.Monad (replicateM)
7+
import Control.Lens (filtered, foldOf, folded, to, (^..))
98
import Data.Default (def)
9+
import Data.Either (isLeft)
10+
import Data.Map (Map)
1011
import Data.Map qualified as Map
12+
import Data.Maybe (fromJust)
1113
import Data.Set (Set)
1214
import Data.Set qualified as Set
15+
import Data.Text (Text)
1316
import Data.Text qualified as Text
1417
import Data.Vector (Vector)
1518
import Data.Vector qualified as Vec
1619
import Ledger qualified
1720
import Ledger.Ada qualified as Ada
18-
import Ledger.Address (Address, PaymentPubKeyHash (PaymentPubKeyHash))
21+
import Ledger.Address (Address (Address), PaymentPubKeyHash (PaymentPubKeyHash))
1922
import Ledger.Address qualified as Address
2023
import Ledger.CardanoWallet qualified as Wallet
2124
import Ledger.Crypto (PubKeyHash)
2225
import Ledger.Tx (TxIn (..), TxInType (..), TxOut (..), TxOutRef (..))
23-
import Ledger.Value (Value, AssetClass)
26+
import Ledger.Value (AssetClass, Value, leq)
2427
import Ledger.Value qualified as Value
28+
import Plutus.V1.Ledger.Credential (Credential (PubKeyCredential, ScriptCredential))
2529
import Spec.MockContract (runPABEffectPure)
2630
import Spec.RandomLedger
2731
import Test.QuickCheck (Gen, Property, forAll, withMaxSuccess)
@@ -36,7 +40,8 @@ tests =
3640
"BotPlutusInterface.CoinSelection"
3741
[ testProperty "Have All unique assetClasses" assertUniqueAssetClasses
3842
, testProperty "columns of vectors represent same assetClass" validValueVectors
39-
, testCase "Coin selection greedy Approx" greedyApprox
43+
, testProperty "coin selection results in valid balance of Tx" validateBalancing
44+
, testCase "coin selection greedy Approx" greedyApprox
4045
]
4146

4247
pkh1 :: PubKeyHash
@@ -107,36 +112,118 @@ greedyApprox = do
107112
Right result -> result @?= expectedResults
108113

109114
assertUniqueAssetClasses :: Property
110-
assertUniqueAssetClasses = withMaxSuccess 1000 (forAll isSubsetGen id)
115+
assertUniqueAssetClasses = withMaxSuccess 1000 (forAll uniqueAssetClassesGen validate)
111116
where
112-
isSubsetGen :: Gen Bool
113-
isSubsetGen =
117+
validate :: (Set AssetClass, [TxOut]) -> Bool
118+
validate (allAssetClasses, utxos) =
119+
Set.isSubsetOf (uniqueAssetClasses $ map txOutValue utxos) allAssetClasses
120+
121+
uniqueAssetClassesGen :: Gen (Set AssetClass, [TxOut])
122+
uniqueAssetClassesGen =
114123
do
115-
allAcs <- randomAssetClasses 30
116-
values <- replicateM 10 (txOutValue <$> randomTxOut 10 allAcs)
124+
let numUniqueAssetClasses :: Int
125+
numUniqueAssetClasses = 30
126+
127+
assetClassSampleSize :: Int
128+
assetClassSampleSize = 30
129+
130+
numUTxOs :: Int
131+
numUTxOs = 10
117132

118-
let uniqueAcs :: Set AssetClass
119-
uniqueAcs = uniqueAssetClasses values
133+
allAssetClasses <- randomAssetClasses numUniqueAssetClasses
120134

121-
return $ Set.isSubsetOf uniqueAcs allAcs
135+
utxos <- randomTxOuts numUTxOs assetClassSampleSize allAssetClasses
136+
137+
return (allAssetClasses, utxos)
122138

123139
validValueVectors :: Property
124-
validValueVectors = withMaxSuccess 1000 (forAll valueVectorsGen id)
140+
validValueVectors = withMaxSuccess 1000 (forAll txOutsGen validate)
125141
where
126-
valueVectorsGen :: Gen Bool
127-
valueVectorsGen =
142+
validate :: [TxOut] -> Bool
143+
validate utxos =
144+
let values :: [Value]
145+
values = map txOutValue utxos
146+
147+
uniqueAcs :: Set AssetClass
148+
uniqueAcs = uniqueAssetClasses values
149+
150+
valueCheck :: Value -> Vector Integer -> Bool
151+
valueCheck value vec =
152+
Vec.fromList (uniqueAcs ^.. folded . to (Value.assetClassValueOf value)) == vec
153+
154+
valuesVec :: [Vector Integer]
155+
valuesVec = Vec.toList $ valuesToVecs uniqueAcs values
156+
in all (uncurry valueCheck) (zip values valuesVec)
157+
158+
txOutsGen :: Gen [TxOut]
159+
txOutsGen =
128160
do
129-
allAcs <- randomAssetClasses 30
130-
values <- replicateM 10 (txOutValue <$> randomTxOut 10 allAcs)
161+
let numUniqueAssetClasses :: Int
162+
numUniqueAssetClasses = 30
163+
164+
assetClassSampleSize :: Int
165+
assetClassSampleSize = 30
166+
167+
numUTxOs :: Int
168+
numUTxOs = 10
169+
170+
allAssetClasses <- randomAssetClasses numUniqueAssetClasses
171+
randomTxOuts numUTxOs assetClassSampleSize allAssetClasses
172+
173+
validateBalancing :: Property
174+
validateBalancing = withMaxSuccess 10000 (forAll balanceGen validate)
175+
where
176+
validate :: (TxOut, Map TxOutRef TxOut) -> Bool
177+
validate (txOutput, utxos) =
178+
let result :: (Either Text (Either Text (Set TxIn)))
179+
result =
180+
fst $
181+
runPABEffectPure def $
182+
selectTxIns @() @'[PABEffect ()] mempty utxos (txOutValue txOutput)
183+
184+
isScriptOutput :: TxOut -> Bool
185+
isScriptOutput TxOut {txOutAddress = Address {addressCredential = ScriptCredential _}} = True
186+
isScriptOutput TxOut {txOutAddress = Address {addressCredential = PubKeyCredential _}} = False
187+
188+
sufficientValue :: Bool
189+
sufficientValue =
190+
txOutValue txOutput
191+
`leq` foldOf (folded . filtered (not . isScriptOutput) . to txOutValue) utxos
192+
193+
toTxOut :: TxIn -> TxOut
194+
toTxOut = fromJust . (`Map.lookup` utxos) . txInRef
195+
in case result of
196+
Left _ -> False
197+
Right eselectedTxIns
198+
| not sufficientValue -> isLeft eselectedTxIns
199+
| otherwise ->
200+
case eselectedTxIns of
201+
Left err
202+
| txOutValue txOutput == mempty -> True
203+
| otherwise -> error (show err)
204+
Right selectedTxIns ->
205+
txOutValue txOutput `leq` foldOf (folded . to (txOutValue . toTxOut)) selectedTxIns
206+
207+
balanceGen :: Gen (TxOut, Map TxOutRef TxOut)
208+
balanceGen =
209+
do
210+
let numUniqueAssetClasses :: Int
211+
numUniqueAssetClasses = 10
212+
213+
assetClassSampleSize :: Int
214+
assetClassSampleSize = 30
215+
216+
numUTxOs :: Int
217+
numUTxOs = 20
218+
219+
allAcs <- randomAssetClasses numUniqueAssetClasses
220+
rTxOuts <- randomTxOuts numUTxOs assetClassSampleSize allAcs
221+
rTxOutRefs <- randomTxOutRefs 19
131222

132-
let uniqueAcs :: Set AssetClass
133-
uniqueAcs = uniqueAssetClasses values
223+
let txOutput :: TxOut
224+
txOutput = head rTxOuts
134225

135-
valueCheck :: Value -> Vector Integer -> Bool
136-
valueCheck value vec =
137-
Vec.fromList (uniqueAcs ^.. folded . to (Value.assetClassValueOf value)) == vec
226+
utxos :: Map TxOutRef TxOut
227+
utxos = Map.fromList $ zip (tail rTxOutRefs) (tail rTxOuts)
138228

139-
valuesVec :: [Vector Integer]
140-
valuesVec = Vec.toList $ valuesToVecs uniqueAcs values
141-
142-
return $ all (uncurry valueCheck) (zip values valuesVec)
229+
return (txOutput, utxos)

test/Spec/RandomLedger.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Spec.RandomLedger (
66
randomTxOut,
77
randomTxOuts,
88
randomTxOutRef,
9+
randomTxOutRefs,
910
) where
1011

1112
import Plutus.PAB.Arbitrary ()
@@ -61,3 +62,6 @@ randomTxOutRef =
6162
txId <- arbitrary
6263
txIdx <- toInteger <$> arbitrary @Natural
6364
return (TxOutRef txId txIdx)
65+
66+
randomTxOutRefs :: Int -> Gen [TxOutRef]
67+
randomTxOutRefs n = replicateM n randomTxOutRef

0 commit comments

Comments
 (0)