Skip to content

Commit 1d3a960

Browse files
authored
Merge pull request #148 from mlabs-haskell/adjust-collateral-creation
Update to BPI with adjusted collateral creation
2 parents dd9d5e3 + 25326a3 commit 1d3a960

18 files changed

+137
-50
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ This format is based on [Keep A Changelog](https://keepachangelog.com/en/1.0.0).
77
- Wallets with Base Address support
88
- Lookups for wallets in tasty integration
99

10+
## [1.3.1] - 2022-11-04
11+
12+
### Fixed
13+
14+
- collateral creation - happens now before user contract execution
15+
16+
### Added
17+
18+
- collateral handling documentation
19+
1020
## [1.3.0] - 2022-10-26
1121

1222
### Added

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ If your project is importing and making use of `Plutip`s library you will need t
3636

3737
And the following ghc flag must to be set for the test execution: `-Wall -threaded -rtsopts`
3838

39-
NOTE: This branch launches local network in `Vasil`. It was tested with node `1.35.3` (this node version used in nix environment as well). Please use appropriate node version when setting up own binaries in `PATH`.
39+
## NOTES
40+
41+
⚠️ This branch launches local network in `Vasil`. It was tested with node `1.35.3` (this node version used in nix environment as well). Please use appropriate node version when setting up own binaries in `PATH`.
42+
43+
⚠️ [Collateral handling](./docs/collateral-handling.md)
4044

4145
## Tutorials
4246

contract-execution/Main.hs

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ main = do
4141
extraConf = def {ecSlotLength = slotLen}
4242
plutipConfig = def {extraConfig = extraConf}
4343

44+
addSomeWalletWithCollateral funds =
45+
addSomeWallet (toAda 10 : funds)
46+
4447
putStrLn "Starting cluster..."
4548
(st, _) <- startCluster plutipConfig $ do
46-
w <- addSomeWallet [toAda 10]
49+
w <- addSomeWalletWithCollateral [toAda 100]
4750
liftIO $ putStrLn "Waiting for wallets to be funded..."
4851
CI.awaitWalletFunded w slotLen
4952

docs/collateral-handling.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Collateral handling
2+
3+
Before running *any* contract `Plutip` under the hood creates dedicated UTxO at "own" wallet address to be used *only* as collateral. This UTxO is created by submitting transaction and spending "own" wallet's funds . For collateral `Plutip` always uses **10 Ada** at this point and if wallet address already has UTxO with 10 Ada on it, then Plutip will use it as collateral.
4+
5+
UTxO that was created or picked for collateral is stored in memory, so during Contract execution `Plutip` will always use this exact same UTxO. Collateral UTxO has special properties - to guard it from being consumed by accident it is not accessible from Contract. I.e. calls like `utxosAt` ***will not return*** UTxO used for collateral. This means, that users don't have to care really about collateral UTxO during contract execution.
6+
7+
The only place where collateral "sticks out" is the moment of wallet creation. Ii is also visible for `cardano-cli` queries.
8+
9+
## Cluster runner
10+
11+
With [cluster runners](../local-cluster/README.md), when creating wallet with `addSomeWallet [100_000_000]`, if you want to have UTxO with exactly 100 Ada while running the Contract, you should add 10 Ada more to wallet's initial distribution, or UTxO with 100 Ada will be used to create collateral.
12+
13+
E.g.:
14+
15+
```haskell
16+
main :: IO ()
17+
main = do
18+
let executeContract wallet contract =
19+
ask >>= \cEnv -> runContract cEnv wallet contract
20+
21+
(st, _) <- startCluster def $ do
22+
w <- addSomeWallet [100_000_000, 10_000_000] -- 10 Ada will be used as collateral
23+
awaitWalletFunded w 1
24+
result <- executeContract w someContract
25+
doSomething result
26+
stopCluster st
27+
```
28+
29+
Or just make helper function:
30+
31+
```haskell
32+
addSomeWalletWithCollateral funds =
33+
addSomeWallet (toAda 10 : funds)
34+
```
35+
36+
## Tasty integration
37+
38+
For collateral handling in tasty integration see [Collateral handling section](./tasty-integration.md#collateral-handling).

docs/tasty-integration.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ To assert the final `Value` which `wallet` will have after contract execution sp
126126
* `initAdaAssertValue [100] 133` - initialize `wallet` with single UTxO with 100 Ada and check that after contract execution final `Value` of all `wallet`'s UTxOs is equal to 133 Ada.
127127
* `initAndAssertLovelaceWith [1_000_000] VGt 2_000_000` - initialize `wallet` with single UTxO with 1000000 Lovelace and check that after contract execution final `Value` of all `wallet`'s UTxOs is *greater than* 2000000 Lovelace.
128128

129-
***One important note*** is that Plutip creates dedicated UTxO to be used *only* as collateral under the hood. This UTxO would normally be created by spending wallets funds, and the transaction fee and Ada amount used for collateral UTxO would mess up balance assertions. So when using assertions for `Value` it is advised to wrap `wallets` initialization with `withCollateral` function. This simply adds a small UTxO to the `wallets`'s balance during network setup that is then picked up for collateral instead avoiding the problem. Use it like so:
129+
#### Collateral handling
130+
131+
***One important note*** is that Plutip creates dedicated UTxO to be used *only* as collateral under the hood. This UTxO would normally be created by spending wallets funds, and the transaction fee and Ada amount used for collateral UTxO would mess up balance assertions. So when using any kind of assertions for `Value` it is advised to wrap `wallets` initialization with `withCollateral` function. This simply adds a small UTxO to the `wallets`'s balance during network setup that is then picked up for collateral instead avoiding the problem. Use it like so:
130132

131133
```haskell
132134
( withCollateral $

flake.lock

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
flake = false;
1212
};
1313
bot-plutus-interface.url =
14-
"github:mlabs-haskell/bot-plutus-interface?ref=d6cf1e3686bc31bb2571c6feefbe28e3a2c8bb06";
14+
"github:mlabs-haskell/bot-plutus-interface?ref=2f4b4c5104bd573039995d6d7eef0c9235ddbc32";
1515
};
1616

1717
outputs =

local-cluster/Main.hs

+4-1
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,11 @@ main = do
7979
amt -> Right $ fromInteger . toInteger $ amt
8080

8181
initWallets numWallets numUtxos amt dirWallets = do
82+
let collateralAmount = 10_000_000
8283
replicateM (max 0 numWallets) $
83-
addSomeWalletDir (replicate numUtxos amt) dirWallets
84+
addSomeWalletDir
85+
(collateralAmount : replicate numUtxos amt)
86+
dirWallets
8487

8588
printWallet (w, n) = do
8689
putStrLn $ "Wallet " ++ show n ++ " PKH: " ++ show (walletPkh w)

local-cluster/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ main = do
5555
ask >>= \cEnv -> runContract cEnv wallet contract
5656

5757
(st, _) <- startCluster def $ do
58-
w <- addSomeWallet [100_000_000]
58+
w <- addSomeWallet [100_000_000, 10_000_000] -- 10 Ada will be used as collateral
5959
awaitWalletFunded w 1
6060
result <- executeContract w someContract
6161
doSomething result

plutip-server/Main.hs

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
{-# LANGUAGE NumericUnderscores #-}
2-
31
module Main (main) where
42

53
import Api (app)

src/Test/Plutip/Contract.hs

+16-22
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,13 @@ import BotPlutusInterface.Types (
141141
)
142142

143143
import Control.Arrow (left)
144-
import Control.Monad.Reader (MonadIO (liftIO), MonadReader (ask), ReaderT, runReaderT, void)
144+
import Control.Monad.Reader (
145+
MonadIO (liftIO),
146+
MonadReader (ask),
147+
ReaderT,
148+
runReaderT,
149+
withReaderT,
150+
)
145151
import Data.Bool (bool)
146152
import Data.Kind (Type)
147153
import Data.List.NonEmpty (NonEmpty)
@@ -151,9 +157,8 @@ import Data.Row (Row)
151157
import Data.Tagged (Tagged (Tagged))
152158
import Data.Text qualified as Text
153159
import Ledger (PaymentPubKeyHash)
154-
import Ledger.Address (pubKeyHashAddress)
155160
import Ledger.Value (Value)
156-
import Plutus.Contract (Contract, waitNSlots)
161+
import Plutus.Contract (Contract)
157162
import PlutusPrelude (render)
158163
import Prettyprinter (Doc, Pretty (pretty), vcat, (<+>))
159164
import Test.Plutip.Contract.Init (
@@ -177,14 +182,15 @@ import Test.Plutip.Contract.Types (
177182
TestWallets (TestWallets, unTestWallets),
178183
ValueOrdering (VEq, VGEq, VGt, VLEq, VLt),
179184
)
180-
import Test.Plutip.Contract.Values (assertValues, valueAt)
185+
import Test.Plutip.Contract.Values (assertValues)
181186
import Test.Plutip.Internal.BotPlutusInterface.Run (runContract, runContractWithLogLvl)
182187
import Test.Plutip.Internal.BotPlutusInterface.Wallet (BpiWallet, ledgerPaymentPkh)
183188
import Test.Plutip.Internal.Types (
184189
ClusterEnv,
185190
ExecutionResult (contractLogs, outcome),
186191
budgets,
187192
)
193+
import Test.Plutip.LocalCluster (plutusValueFromWallet)
188194
import Test.Plutip.Options (TraceOption (ShowBudgets, ShowTrace, ShowTraceButOnlyContext))
189195
import Test.Plutip.Predicate (Predicate, noBudgetsMessage, pTag)
190196
import Test.Plutip.Tools.Format (fmtTxBudgets)
@@ -317,35 +323,23 @@ withContractAs walletIdx toContract = do
317323
-- to the user in `withContractAs`
318324
(ownWallet, otherWallets) = separateWallets walletIdx wallets'
319325

320-
{- these are `PaymentPubKeyHash`es of all wallets used in test case
321-
they stay in list is same order as `TestWallets` defined in test case
322-
so collected Values will be in same order as well
323-
it is important to preserve this order for Values check with `assertValues`
324-
as there is no other mechanism atm to match `TestWallet` with collected `Value`
325-
-}
326-
collectValuesPkhs :: NonEmpty PaymentPubKeyHash
327-
collectValuesPkhs = fmap ledgerPaymentPkh wallets'
328-
329326
-- wallet `PaymentPubKeyHash`es that will be available in
330327
-- `withContract` and `withContractAs`
331328
otherWalletsPkhs :: [PaymentPubKeyHash]
332329
otherWalletsPkhs = fmap ledgerPaymentPkh otherWallets
333330

334-
-- contract that gets all the values present at the test wallets.
335-
valuesAtWallet :: Contract w s e (NonEmpty Value)
336-
valuesAtWallet =
337-
void (waitNSlots 1)
338-
>> traverse (valueAt . (`pubKeyHashAddress` Nothing)) collectValuesPkhs
339-
331+
collectValues = do
332+
vs <- traverse plutusValueFromWallet wallets'
333+
return $ sequence vs
340334
-- run the test contract
341335
execRes <- liftIO $ runContract cEnv ownWallet (toContract otherWalletsPkhs)
342336

343337
-- get all the values present at the test wallets after the user given contracts has been executed.
344-
execValues <- liftIO $ runContract cEnv ownWallet valuesAtWallet
338+
values <- withReaderT fst collectValues
345339

346-
case outcome execValues of
340+
case values of
347341
Left e -> fail $ "Failed to get values. Error: " ++ show e
348-
Right values -> return $ execRes {outcome = (,values) <$> outcome execRes}
342+
Right vs -> return $ execRes {outcome = (,vs) <$> outcome execRes}
349343
where
350344
separateWallets :: forall b. Int -> NonEmpty b -> (b, [b])
351345
separateWallets i xss

src/Test/Plutip/Contract/Init.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,7 @@ withCollateral TestWallets {..} = TestWallets $ NonEmpty.map go unTestWallets
114114
go TestWallet {..} =
115115
TestWallet
116116
{ twInitDistribuition = fromInteger defCollateralSize : twInitDistribuition
117-
, twExpected =
118-
second (Value.unionWith (+) $ Ada.lovelaceValueOf defCollateralSize) <$> twExpected
117+
, twExpected = second (Value.unionWith (+) $ Ada.lovelaceValueOf defCollateralSize) <$> twExpected
119118
}
120119

121120
-- | Library functions works with amounts in `Lovelace`.

src/Test/Plutip/Contract/Types.hs

+2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ data TestWallet = TestWallet
7272
{ twInitDistribuition :: [Positive]
7373
, twExpected :: Maybe (ValueOrdering, Value)
7474
}
75+
deriving stock (Show)
7576

7677
data ValueOrdering = VEq | VGt | VLt | VGEq | VLEq
78+
deriving stock (Show)
7779

7880
-- | Value doesn't have an Ord instance, so we cannot use `compare`
7981
compareValuesWith :: ValueOrdering -> Value -> Value -> Bool

src/Test/Plutip/Contract/Values.hs

+3-6
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ import Data.Row (Row)
1717
import Data.Text (Text)
1818
import Data.Text qualified as Text
1919
import Data.Text.Encoding (decodeUtf8')
20-
import Ledger (Address, ChainIndexTxOut (PublicKeyChainIndexTxOut, ScriptChainIndexTxOut))
2120
import Ledger.Ada qualified as Ada
2221
import Ledger.Value (CurrencySymbol (unCurrencySymbol), TokenName (unTokenName), Value)
2322
import Ledger.Value qualified as Value
2423
import Plutus.Contract (AsContractError, Contract, utxosAt)
2524
import PlutusTx.Builtins (fromBuiltin)
2625

26+
import Ledger (Address, ciTxOutValue)
27+
import PlutusPrelude ((^.))
2728
import Test.Plutip.Contract.Types (
2829
ValueOrdering (VEq, VGEq, VGt, VLEq, VLt),
2930
compareValuesWith,
@@ -36,11 +37,7 @@ valueAt ::
3637
Contract w s e Value
3738
valueAt addr = do
3839
utxos <- utxosAt addr
39-
pure . mconcat . map utxoValue . Map.elems $ utxos
40-
where
41-
utxoValue :: ChainIndexTxOut -> Value
42-
utxoValue (PublicKeyChainIndexTxOut _ v _ _) = v
43-
utxoValue (ScriptChainIndexTxOut _ v _ _ _) = v
40+
pure . mconcat . map (^. ciTxOutValue) . Map.elems $ utxos
4441

4542
assertValues :: NonEmpty (Maybe (ValueOrdering, Value)) -> NonEmpty Value -> Either Text ()
4643
assertValues expected values =

src/Test/Plutip/LocalCluster.hs

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ module Test.Plutip.LocalCluster (
1111
withConfiguredCluster,
1212
startCluster,
1313
stopCluster,
14+
plutusValueFromWallet,
1415
) where
1516

1617
import Control.Concurrent (threadDelay)
17-
import Control.Monad.IO.Class (liftIO)
18+
import Control.Monad.IO.Class (MonadIO, liftIO)
1819
import Control.Monad.Reader (MonadReader (ask), ReaderT, ask)
1920
import Data.Bifunctor (second)
2021
import Data.Default (def)
2122
import Data.List.NonEmpty (NonEmpty)
2223
import Data.List.NonEmpty qualified as NE
24+
import Ledger (Value)
2325
import Numeric.Natural (Natural)
2426
import Numeric.Positive (Positive)
2527
import Test.Plutip.Config (PlutipConfig (extraConfig))
@@ -38,6 +40,7 @@ import Test.Plutip.Internal.BotPlutusInterface.Wallet (
3840
import Test.Plutip.Internal.Cluster.Extra.Types (ecSlotLength)
3941
import Test.Plutip.Internal.LocalCluster (startCluster, stopCluster)
4042
import Test.Plutip.Internal.Types (ClusterEnv)
43+
import Test.Plutip.Tools.CardanoApi (CardanoApiError, plutusValueFromAddress)
4144
import Test.Plutip.Tools.ChainIndex qualified as CI
4245
import Test.Tasty (testGroup, withResource)
4346
import Test.Tasty.Providers (TestTree)
@@ -113,3 +116,12 @@ type RetryDelay = Positive
113116

114117
imap :: (Int -> a -> b) -> [a] -> [b]
115118
imap fn = zipWith fn [0 ..]
119+
120+
-- Get total `Value` of all UTxOs at `BpiWallet` address.
121+
plutusValueFromWallet ::
122+
MonadIO m =>
123+
BpiWallet ->
124+
ReaderT ClusterEnv m (Either CardanoApiError Value)
125+
plutusValueFromWallet bw = do
126+
cEnv <- ask
127+
liftIO . plutusValueFromAddress cEnv . cardanoMainnetAddress $ bw

src/Test/Plutip/Tools/CardanoApi.hs

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ module Test.Plutip.Tools.CardanoApi (
55
queryProtocolParams,
66
queryTip,
77
awaitAddressFunded,
8+
plutusValueFromAddress,
9+
CardanoApiError,
810
) where
911

1012
import Cardano.Api qualified as C
11-
import Cardano.Api.Shelley (ProtocolParameters, UTxO (UTxO))
13+
import Cardano.Api.Shelley (ProtocolParameters, TxOut (TxOut), UTxO (UTxO, unUTxO), txOutValueToValue)
1214
import Cardano.Launcher.Node (nodeSocketFile)
1315
import Cardano.Slotting.Slot (WithOrigin)
1416
import Test.Plutip.Internal.Cluster (RunningNode (RunningNode))
@@ -22,6 +24,8 @@ import Data.Map qualified as Map
2224
import Data.Set qualified as Set
2325
import Data.Time (NominalDiffTime, nominalDiffTimeToSeconds)
2426
import GHC.Generics (Generic)
27+
import Ledger (Value)
28+
import Ledger.Tx.CardanoAPI (fromCardanoValue)
2529
import Ouroboros.Consensus.HardFork.Combinator.AcrossEras (EraMismatch)
2630
import Ouroboros.Network.Protocol.LocalStateQuery.Type (AcquireFailure)
2731
import Test.Plutip.Internal.Types (ClusterEnv (runningNode))
@@ -106,3 +110,14 @@ awaitAddressFunded addr retryDelay = do
106110
| Map.null utxo' ->
107111
throwString "No UTxOs returned by cardano API query for address"
108112
_ -> pure ()
113+
114+
-- | Get total `Value` of all UTxOs at address.
115+
plutusValueFromAddress ::
116+
ClusterEnv ->
117+
C.AddressAny ->
118+
IO (Either CardanoApiError Value)
119+
plutusValueFromAddress cEnv addr = do
120+
let getValues = mconcat . fmap extract . (Map.elems . unUTxO)
121+
extract (TxOut _ txoV _ _) = fromCardanoValue $ txOutValueToValue txoV
122+
res <- utxosAtAddress cEnv addr
123+
return $ getValues <$> res

0 commit comments

Comments
 (0)