Skip to content

Commit 4765fab

Browse files
committed
diffAssets tests
Some improvements to the diffAssets
1 parent 8fdab70 commit 4765fab

File tree

3 files changed

+118
-29
lines changed

3 files changed

+118
-29
lines changed

hydra-cluster/src/Hydra/Cluster/Scenarios.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ import Hydra.Node.DepositPeriod (DepositPeriod (..))
115115
import Hydra.Options (CardanoChainConfig (..), ChainBackendOptions (..), DirectOptions (..), RunOptions (..), startChainFrom)
116116
import Hydra.Tx (HeadId, IsTx (balance), Party, txId)
117117
import Hydra.Tx.ContestationPeriod qualified as CP
118-
import Hydra.Tx.Deposit (capUTxO, filterAssets)
118+
import Hydra.Tx.Deposit (capUTxO, diffAssets)
119119
import Hydra.Tx.Utils (dummyValidatorScript, verificationKeyToOnChainId)
120120
import HydraNode (
121121
HydraClient (..),
@@ -1347,7 +1347,8 @@ canDepositPartially tracer workDir blockTime backend hydraScriptsTxId =
13471347
let tokenAssetValue = assetsToValue tokenAssets
13481348
let quantityMoreThan20 = (> 20)
13491349
let partialTokenAssets = Map.map (\(CAPI.PolicyAssets policyAssetMap) -> CAPI.PolicyAssets $ Map.filter quantityMoreThan20 policyAssetMap) tokenAssets
1350-
let tokenDiff = filterAssets tokenAssets (Map.toList partialTokenAssets)
1350+
-- NOTE: using diffAssets here implicitly tests that function too.
1351+
let tokenDiff = assetsToValue $ Map.fromList $ diffAssets tokenAssets partialTokenAssets
13511352
let partialTokenAssetValue = assetsToValue partialTokenAssets
13521353
let tokenAssetValueWithoutAda = assetsToValue $ valueToPolicyAssets partialTokenAssetValue
13531354
let seedAmount = 5_000_000

hydra-tx/src/Hydra/Tx/Deposit.hs

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ depositTx networkId headId commitBlueprintTx upperSlot deadline amount tokens =
5959
utxoToDeposit = mergeUTxO utxoToDeposit' tokensToDepositUTxO
6060

6161
returnToUser =
62-
let returnToUserUTxO = leftoverUTxO `filterExistingAssets` tokensToDepositUTxO
62+
let returnToUserUTxO = leftoverUTxO `diffExistingAssets` tokensToDepositUTxO
6363
in if UTxO.null returnToUserUTxO
6464
then StrictSeq.empty
6565
else
@@ -72,37 +72,54 @@ depositTx networkId headId commitBlueprintTx upperSlot deadline amount tokens =
7272

7373
depositInputs = (,BuildTxWith $ KeyWitness KeyWitnessForSpending) <$> depositInputsList
7474

75-
-- | Filter the first argument UTxO's non ADA assets in case any asset exists in the second UTxO argument.
76-
-- Asset quantities will be subtracted if they are found.
77-
filterExistingAssets :: UTxO -> UTxO -> UTxO
78-
filterExistingAssets utxoToFilter utxoToLookup =
75+
-- | Find the difference between the first argument UTxO's non ADA assets in
76+
-- and the second UTxO argument. Matching asset quantities will be subtracted
77+
-- if they are found.
78+
diffExistingAssets :: UTxO -> UTxO -> UTxO
79+
diffExistingAssets utxoToFilter utxoToLookup =
7980
UTxO.fromList $ findAssets <$> UTxO.toList utxoToFilter
8081
where
8182
findAssets (i, TxOut a val d r) =
8283
let assets = valueToPolicyAssets val
8384
originalLovelace = selectLovelace val
85+
filteredAssets' = diffAssets assets forLookup
8486
filteredAssets =
85-
foldMap (uncurry policyAssetsToValue) $
86-
filterAssets assets forLookup
87-
in (i, TxOut a (lovelaceToValue originalLovelace <> filteredAssets) d r)
88-
forLookup =
89-
-- NOTE: Uses a list to store all policies, preserving multiple entries
90-
-- with the same policyId but different assets. A Map would silently
91-
-- overwrite duplicates.
92-
concatMap (Map.toList . valueToPolicyAssets . txOutValue . snd) $ UTxO.toList utxoToLookup
93-
94-
-- | Filter the first argument map of assets in case any asset exists in the second argument.
95-
filterAssets :: Map PolicyId PolicyAssets -> [(PolicyId, PolicyAssets)] -> [(PolicyId, PolicyAssets)]
96-
filterAssets assets forLookup =
97-
Map.assocs $
98-
Map.mapWithKey
99-
( \pid (PolicyAssets x) ->
100-
let samePid = List.filter (\(pid', _) -> pid' == pid) forLookup
101-
in case List.lookup pid samePid of
102-
Nothing -> PolicyAssets x
103-
Just (PolicyAssets foundAssets) -> PolicyAssets $ x `Map.difference` foundAssets
104-
)
105-
assets
87+
if null (snd <$> filteredAssets')
88+
then mempty
89+
else filteredAssets'
90+
filteredValue =
91+
foldMap (uncurry policyAssetsToValue) filteredAssets
92+
in (i, TxOut a (lovelaceToValue originalLovelace <> filteredValue) d r)
93+
forLookup = valueToPolicyAssets $ UTxO.totalValue utxoToLookup
94+
95+
-- | Diff the first argument map of assets in case any asset exists in the second argument.
96+
diffAssets :: Map PolicyId PolicyAssets -> Map PolicyId PolicyAssets -> [(PolicyId, PolicyAssets)]
97+
diffAssets assets forLookup =
98+
if null forLookup
99+
then Map.toList assets
100+
else
101+
Map.assocs $
102+
Map.foldrWithKey
103+
( \pid (PolicyAssets existing) result ->
104+
case Map.lookup pid forLookup of
105+
Nothing -> result
106+
Just foundAsset -> result `Map.union` Map.singleton pid (PolicyAssets $ go existing foundAsset)
107+
)
108+
Map.empty
109+
assets
110+
where
111+
go :: Map AssetName Quantity -> PolicyAssets -> Map AssetName Quantity
112+
go existing (PolicyAssets found) =
113+
Map.differenceWith
114+
checkQuantities
115+
existing
116+
found
117+
118+
checkQuantities :: Quantity -> Quantity -> Maybe Quantity
119+
checkQuantities existing wanted =
120+
if existing > wanted
121+
then Just $ existing - wanted
122+
else Nothing
106123

107124
-- | Merges the two 'UTxO' favoring data coming from the first argument 'UTxO'.
108125
-- In case the same 'TxIn' was found in the first 'UTxO' - second 'UTxO' value

hydra-tx/test/Hydra/Tx/DepositSpec.hs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Cardano.Api.UTxO qualified as UTxO
77
import Data.Map.Strict qualified as Map
88
import Data.Set qualified as Set
99
import Hydra.Cardano.Api (AssetId (..), AssetName, Coin (..), PolicyAssets (..), PolicyId, Quantity (..), UTxO, selectLovelace, txOutValue, valueToPolicyAssets)
10-
import Hydra.Tx.Deposit (capUTxO, pickTokensToDeposit, splitTokens)
10+
import Hydra.Tx.Deposit (capUTxO, diffAssets, pickTokensToDeposit, splitTokens)
1111
import Test.Hydra.Tx.Fixture (testPolicyId)
1212
import Test.Hydra.Tx.Gen (genUTxOSized, genUTxOWithAssetsSized)
1313
import Test.QuickCheck (Property, chooseInteger, counterexample, cover, elements, forAll, frequency, listOf, oneof, property, (===), (==>))
@@ -240,6 +240,77 @@ spec =
240240
prop "monotonic with respect to target" propMonotonicTarget
241241
prop "no UTxO loss" propNoUTxOLoss
242242

243+
describe "diffAssets" $ do
244+
describe "tests" $ do
245+
it "returns empty assets when both inputs are empty" $ do
246+
let assets = diffAssets mempty mempty
247+
assets `shouldBe` mempty
248+
it "returns existing assets when lookup input is empty" $
249+
forAll arbitrary $ \assetMap -> do
250+
let assets = diffAssets assetMap mempty
251+
assets `shouldBe` Map.toList assetMap
252+
it "returns empty assets if asset input is empty" $
253+
forAll arbitrary $ \lookupMap -> do
254+
let assets = diffAssets mempty lookupMap
255+
assets `shouldBe` mempty
256+
it "subracts found values" $
257+
forAll arbitrary $ \aPolicy -> do
258+
let policyAssets =
259+
Map.fromList [("SomeTokenA", 100), ("SomeTokenB", 200)]
260+
let policyAssets' =
261+
Map.fromList [("SomeTokenA", 30), ("SomeTokenB", 50)]
262+
let expectedAssets =
263+
Map.fromList [("SomeTokenA", 70), ("SomeTokenB", 150)]
264+
let expectedResult =
265+
Map.fromList [(aPolicy, PolicyAssets expectedAssets)]
266+
267+
let a = Map.fromList [(aPolicy, PolicyAssets policyAssets)]
268+
let b = Map.fromList [(aPolicy, PolicyAssets policyAssets')]
269+
let assets = diffAssets a b
270+
assets `shouldBe` Map.toList expectedResult
271+
it "keeps assets not found in the lookup map" $
272+
forAll arbitrary $ \aPolicy -> do
273+
let policyAssets =
274+
Map.fromList [("SomeTokenA", 100), ("SomeTokenC", 2)]
275+
let policyAssets' =
276+
Map.fromList [("SomeTokenA", 30), ("SomeTokenB", 50)]
277+
let expectedAssets =
278+
Map.fromList [("SomeTokenA", 70), ("SomeTokenC", 2)]
279+
let expectedResult =
280+
Map.fromList [(aPolicy, PolicyAssets expectedAssets)]
281+
282+
let a = Map.fromList [(aPolicy, PolicyAssets policyAssets)]
283+
let b = Map.fromList [(aPolicy, PolicyAssets policyAssets')]
284+
let assets = diffAssets a b
285+
assets `shouldBe` Map.toList expectedResult
286+
it "ignores extra assets in the lookup map" $
287+
forAll arbitrary $ \aPolicy -> do
288+
let policyAssets =
289+
Map.fromList [("SomeTokenA", 100), ("SomeTokenB", 85)]
290+
let policyAssets' =
291+
Map.fromList [("SomeTokenA", 30), ("SomeTokenB", 50), ("SomeTokenC", 400)]
292+
let expectedAssets =
293+
Map.fromList [("SomeTokenA", 70), ("SomeTokenB", 35)]
294+
let expectedResult =
295+
Map.fromList [(aPolicy, PolicyAssets expectedAssets)]
296+
297+
let a = Map.fromList [(aPolicy, PolicyAssets policyAssets)]
298+
let b = Map.fromList [(aPolicy, PolicyAssets policyAssets')]
299+
let assets = diffAssets a b
300+
assets `shouldBe` Map.toList expectedResult
301+
it "ignores assets with too low values" $
302+
forAll arbitrary $ \aPolicy -> do
303+
let policyAssets =
304+
Map.fromList [("SomeTokenA", 100), ("SomeTokenB", 85)]
305+
let policyAssets' =
306+
Map.fromList [("SomeTokenA", 100), ("SomeTokenB", 86)]
307+
let expectedResult = [(aPolicy, mempty)]
308+
309+
let a = Map.fromList [(aPolicy, PolicyAssets policyAssets)]
310+
let b = Map.fromList [(aPolicy, PolicyAssets policyAssets')]
311+
let assets = diffAssets a b
312+
assets `shouldBe` expectedResult
313+
243314
-- | Property: The sum of selected and leftover values equals the input value
244315
propPreservesTotalValue :: UTxO -> Coin -> Property
245316
propPreservesTotalValue utxo target =

0 commit comments

Comments
 (0)