forked from Plutonomicon/cardano-transaction-lib
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathUtxoDistribution.purs
281 lines (261 loc) · 9.24 KB
/
UtxoDistribution.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
module Test.Ctl.Testnet.UtxoDistribution
( ArbitraryUtxoDistr
, assertContract
, assertCorrectDistribution
, assertNoUtxosAtAddress
, assertNoUtxosAtEnterpriseAddress
, assertUtxosAtTestnetWalletAddress
, checkUtxoDistribution
, genInitialUtxo
, ppArbitraryUtxoDistr
, suite
, withArbUtxoDistr
) where
import Prelude
import Cardano.Types
( BigNum
, Credential(PubKeyHashCredential)
, TransactionInput
, TransactionOutput
, UtxoMap
)
import Cardano.Types.Address (Address(EnterpriseAddress))
import Cardano.Types.BigNum as BigNum
import Contract.Address (Address, getNetworkId)
import Contract.Monad (Contract, liftedM)
import Contract.Test.Testnet
( class UtxoDistribution
, InitialUTxOs
, InitialUTxOsWithStakeKey(InitialUTxOsWithStakeKey)
, defaultTestnetConfig
, runTestnetContract
, withStakeKey
)
import Contract.Utxos (utxosAt)
import Contract.Value (Value, lovelaceValueOf)
import Contract.Wallet
( KeyWallet
, getWalletAddresses
, ownPaymentPubKeyHashes
, ownStakePubKeyHashes
, withKeyWallet
)
import Control.Lazy (fix)
import Ctl.Internal.Test.UtxoDistribution (encodeDistribution, keyWallets)
import Data.Array (foldl, head, replicate, zip)
import Data.Array.NonEmpty (fromNonEmpty) as NEArray
import Data.Foldable (intercalate)
import Data.FoldableWithIndex (foldlWithIndex)
import Data.Map (empty, insert, isEmpty) as Map
import Data.Maybe (fromJust, isJust)
import Data.Newtype (unwrap, wrap)
import Data.NonEmpty ((:|))
import Data.Traversable (for_)
import Data.Tuple.Nested (type (/\), (/\))
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Effect.Exception (throw)
import Mote (group, test)
import Mote.TestPlanM (TestPlanM)
import Partial.Unsafe (unsafePartial)
import Test.Ctl.Testnet.Common (privateStakeKey)
import Test.QuickCheck (class Arbitrary, arbitrary, mkSeed)
import Test.QuickCheck.Gen
( Gen
, arrayOf
, chooseInt
, frequency
, resize
, sample
, sized
)
import Type.Prelude (Proxy(Proxy))
suite :: TestPlanM (Aff Unit) Unit
suite = group "UtxoDistribution" do
test
"stake key transfers with distribution: [[1000000000,1000000000]]"
do
let
distribution :: Array InitialUTxOs
distribution = replicate 2 [ BigNum.fromInt 1_000_000_000 ]
runTestnetContract defaultTestnetConfig distribution $
checkUtxoDistribution distribution
test
"stake key transfers with distribution: stake + [[1000000000,1000000000]]"
do
let
distribution :: Array InitialUTxOsWithStakeKey
distribution = withStakeKey privateStakeKey <$> replicate 2
[ BigNum.fromInt 1_000_000_000 ]
runTestnetContract defaultTestnetConfig distribution $
checkUtxoDistribution distribution
test
"stake key transfers with distribution: ([[1000000000,1000000000]], stake + [[1000000000,1000000000]])"
do
let
distribution1 :: Array InitialUTxOs
distribution1 = replicate 2 [ BigNum.fromInt 1_000_000_000 ]
distribution = distribution1 /\
(withStakeKey privateStakeKey <$> distribution1)
runTestnetContract defaultTestnetConfig distribution $
checkUtxoDistribution distribution
-- set seed to 5 and size to 10 to fail
let distrs = sample (mkSeed 2) 5 arbitrary
for_ distrs $ \distr -> do
test
( "stake key transfers with random distribution: "
<> ppArbitraryUtxoDistr distr
)
$
withArbUtxoDistr
distr
\randDistr -> runTestnetContract defaultTestnetConfig randDistr $
checkUtxoDistribution randDistr
checkUtxoDistribution
:: forall (distr :: Type) (wallet :: Type)
. UtxoDistribution distr wallet
=> distr
-> wallet
-> Contract Unit
checkUtxoDistribution distr wallets = do
let
walletsArray = keyWallets (Proxy :: Proxy distr) wallets
walletUtxos = encodeDistribution distr
for_ walletsArray assertUtxosAtTestnetWalletAddress
assertCorrectDistribution $ zip walletsArray walletUtxos
-- TODO: minimum value of 1 ada is hardcoded, tests become flaky below
-- that value. Ideally this shouldn't be hardcoded. We might be able
-- to remove this minimum after
-- https://github.com/Plutonomicon/cardano-transaction-lib/issues/857
-- is resolved
genInitialUtxo :: Gen InitialUTxOs
genInitialUtxo = unsafePartial $
map
(BigNum.fromInt >>> (_ `BigNum.mul` BigNum.fromInt 1_000_000) >>> fromJust)
<$> arrayOf (chooseInt 1 1000)
instance Arbitrary ArbitraryUtxoDistr where
arbitrary =
fix \_ -> sized $ \size -> resize size $ frequency $ NEArray.fromNonEmpty $
(1.0 /\ pure UDUnit) :|
[ 2.0 /\ (UDInitialUtxos <$> genInitialUtxo)
, 2.0 /\
( UDInitialUtxosWithStake <$>
( InitialUTxOsWithStakeKey
<$> (pure privateStakeKey)
<*> genInitialUtxo
)
)
, 4.0 /\
( UDTuple
<$> resize (size - 1) arbitrary
<*> resize (size - 1) arbitrary
)
]
-- TODO Add UDArray
-- https://github.com/Plutonomicon/cardano-transaction-lib/issues/1187
data ArbitraryUtxoDistr
= UDUnit
| UDInitialUtxos InitialUTxOs
| UDInitialUtxosWithStake InitialUTxOsWithStakeKey
| UDTuple ArbitraryUtxoDistr ArbitraryUtxoDistr
ppInitialUtxos :: InitialUTxOs -> String
ppInitialUtxos x = "[" <> intercalate ", " (map BigNum.toString x) <> "]"
ppArbitraryUtxoDistr :: ArbitraryUtxoDistr -> String
ppArbitraryUtxoDistr = case _ of
UDUnit -> "unit"
UDInitialUtxos x -> ppInitialUtxos x
UDInitialUtxosWithStake (InitialUTxOsWithStakeKey _ x) ->
"stake + " <> ppInitialUtxos x
UDTuple x y -> "(" <> ppArbitraryUtxoDistr x <> " /\\ "
<> ppArbitraryUtxoDistr y
<> ")"
withArbUtxoDistr
:: forall a
. ArbitraryUtxoDistr
-> (forall distr wallet. UtxoDistribution distr wallet => distr -> a)
-> a
withArbUtxoDistr d f = case d of
UDUnit -> f unit
UDInitialUtxos x -> f x
UDInitialUtxosWithStake x -> f x
UDTuple x y ->
withArbUtxoDistr x (\d1 -> withArbUtxoDistr y (f <<< (d1 /\ _)))
assertContract :: String -> Boolean -> Contract Unit
assertContract msg cond = if cond then pure unit else liftEffect $ throw msg
-- | For a testnet wallet, assert that any utxos held by the
-- | wallet are at the expected address. If the wallet has a stake
-- | key, this function assumes the expected address is the base
-- | address, otherwise it assumes the expected address is the
-- | enterprise address.
assertUtxosAtTestnetWalletAddress
:: KeyWallet -> Contract Unit
assertUtxosAtTestnetWalletAddress wallet = withKeyWallet wallet do
maybeStake <- join <<< head <$> ownStakePubKeyHashes
when (isJust maybeStake) $ assertNoUtxosAtEnterpriseAddress wallet
assertNoUtxosAtEnterpriseAddress
:: KeyWallet -> Contract Unit
assertNoUtxosAtEnterpriseAddress wallet = withKeyWallet wallet $
assertNoUtxosAtAddress =<<
( EnterpriseAddress <$>
( { networkId: _, paymentCredential: _ }
<$> getNetworkId
<*> liftedM "Could not get payment pubkeyhash"
( map (wrap <<< PubKeyHashCredential <<< unwrap) <<< head <$>
ownPaymentPubKeyHashes
)
)
)
assertNoUtxosAtAddress :: Address -> Contract Unit
assertNoUtxosAtAddress addr = do
utxos <- utxosAt addr
assertContract "Expected address to not hold utxos" $ Map.isEmpty utxos
-- | For each wallet, assert that there is a one-to-one correspondance
-- | between its utxo set and its expected utxo amounts.
assertCorrectDistribution
:: Array (KeyWallet /\ InitialUTxOs)
-> Contract Unit
assertCorrectDistribution wallets = for_ wallets \(wallet /\ expectedAmounts) ->
withKeyWallet wallet do
addr <- liftedM "Could not get wallet address" $ head <$> getWalletAddresses
utxos <- utxosAt addr
assertContract "Incorrect distribution of utxos" $
checkDistr utxos expectedAmounts
where
-- Idea here is to iterate through the expected amounts and remove
-- one matching utxo from the utxo set if found, otherwise return
-- false. Once we've gone through all expected amounts, if all of
-- them have been found in the utxo set, we expect there to be no
-- utxos remaining
checkDistr :: UtxoMap -> InitialUTxOs -> Boolean
checkDistr originalUtxos expectedAmounts =
let
allFound /\ remainingUtxos =
foldl findAndRemoveExpected (true /\ originalUtxos) expectedAmounts
in
allFound && Map.isEmpty remainingUtxos
where
-- Remove a single utxo containing the expected ada amount,
-- returning the updated utxo map and false if it could not be
-- found
findAndRemoveExpected :: Boolean /\ UtxoMap -> BigNum -> Boolean /\ UtxoMap
findAndRemoveExpected o@(false /\ _) _ = o
findAndRemoveExpected (_ /\ utxos) expected =
foldlWithIndex
(removeUtxoMatchingValue $ lovelaceValueOf expected)
(false /\ Map.empty)
utxos
-- Include the utxo if it does not match the value, return true if
-- the utxo matches the value
removeUtxoMatchingValue
:: Value
-> TransactionInput
-> Boolean /\ UtxoMap
-> TransactionOutput
-> Boolean /\ UtxoMap
removeUtxoMatchingValue
expected
i
(found /\ m)
output
| not found && expected == (unwrap output).amount = true /\ m
| otherwise = found /\ Map.insert i output m