diff --git a/src/pages/getting-started/endpoints.mdx b/src/pages/getting-started/endpoints.mdx index 4a2e8b2..63fd7f0 100644 --- a/src/pages/getting-started/endpoints.mdx +++ b/src/pages/getting-started/endpoints.mdx @@ -26,7 +26,8 @@ As noted earlier, building transaction bodies require gathering suitable informa Currently Atlas supports the following providers (& it would be highly appreciated if community enriches this by contributing to [Atlas](https://github.com/geniusyield/atlas/tree/main)): * [Maestro](https://www.gomaestro.org/). -* Locally ran node along with [Kupo](https://cardanosolutions.github.io/kupo/). We have tested with version 8.1.2 of `cardano-node` and 2.7.2 of Kupo. +* Locally ran node along with [Kupo](https://cardanosolutions.github.io/kupo/). We have tested with version 10.1.3 of `cardano-node` and 2.10.0 of Kupo. + * Note that node connection is supported both via network socket and Ogmios URL. * [Blockfrost](https://blockfrost.io/). @@ -63,7 +64,7 @@ To provide information about the provider, we will create a `config.json` file w where `coreProvider` field can have one of following possible values: - + ```json "coreProvider": { "maestroToken": "", "turboSubmit": false }, @@ -74,6 +75,11 @@ where `coreProvider` field can have one of following possible values: "coreProvider": { "socketPath": "path-to-node-socket", "kupoUrl": "http://localhost:1442" }, ``` + + ```json + "coreProvider": { "ogmiosUrl": "http://localhost:1337", "kupoUrl": "http://localhost:1442" }, + ``` + ```json "coreProvider": { "blockfrostKey": "" }, diff --git a/src/pages/getting-started/integration-tests.mdx b/src/pages/getting-started/integration-tests.mdx deleted file mode 100644 index e8a6e2e..0000000 --- a/src/pages/getting-started/integration-tests.mdx +++ /dev/null @@ -1,251 +0,0 @@ -import { Callout } from 'nextra-theme-docs' - -# Integration Tests - -We already saw how we can conveniently write tests for our smart contract using our wrapper upon Plutus simple model. But these tests were running against a mock ledger, i.e., we really were just simulating it by having some mock data-structures (say set of UTxOs) which were getting updated on submission of successful transaction. We could however write tests to test against the real node and have it slightly more convenient to program against by spinning up our own private network (_privnet_ for short). Here is the table which outlines the differences between the two approaches: - - -| **Tests using PSM Wrapper** | **Tests using Private Network** | -|--------------------------------------------------------------------|-------------------------------------------------------------------------------| -| Runs against mock ledger | Runs against real node | -| Each unit test gets fresh set of wallets (having original balance) | Each subsequent unit test continues upon the effects caused by previous ones | -| Fast, purer (no `IO`) & convenient | Slow as each slot is configured to be 0.1 second | - -Thus these tests are suitable for integration testing. - -## Spinning up private network - - - To access our private network in Atlas, we'll be using "Local node with Kupo" provider and so, `cardano-node` & Kupo version is expected to be 8.1.2 & 2.7.2 respectively. - - -Our private network is adapted from WoofPool's [`cardano-private-testnet-setup`](https://github.com/woofpool/cardano-private-testnet-setup) repository. - -To spin up it up: - -1. Clone [this](https://github.com/geniusyield/cardano-private-testnet-setup) repository. Make sure to not clone it in some deep nested path as then the path length towards the generated socket file (`node.sock`) may exceed 108 characters[^5]. -2. Enter it & checkout `geniusyield` branch. -3. Enter the following in terminal: `./scripts/automate.sh` (you would need to have `cardano-node` & `cardano-cli` available in your `PATH`). - -Once it says, "_Congrats! Your network is ready for use!_" we are ready to move forward and setup Kupo. - -Assuming `TESTNET` environment variable points to the directory of clone private testnet repository, we can start Kupo with following command: - -``` -kupo \ - --node-socket $TESTNET/private-testnet/node-spo1/node.sock \ - --node-config $TESTNET/private-testnet/configuration.yaml \ - --since origin \ - --match "*" \ - --prune-utxo \ - --in-memory -``` - -We are now complete with our setup. To run tests, execute `KUPO_URL=http://localhost:1442 GENIUSYIELD_PRIVNET_DIR=$TESTNET/private-testnet cabal run betref-privnet-tests -- -j1` inside our example project folder. - -The `-j1` is needed so that the tests run sequentially. - - -Remember to stop (`CTRL-C`, and `killall cardano-node`) the private testnet, or it will eventually eat all of your disk space. - - -The way we have it setup for our test boilerplate is that we have nine users where users second to nine start with the following balances: - -* 5 UTxOs each containing thousand ada -* 1 million each of gold & iron tokens - -First user is called "_funder_" as it has far more ada (couple of 100 thousands) and the number of gold & iron tokens is 2 millions. - -We'll also see how to create a new user soon, if required. - - - Unless you kill & restart the private network, running your privnet tests again, would have them run in the modified network state[^1]. - - -## Understanding our first test - - -The tests are written in [this](https://github.com/geniusyield/atlas-examples/tree/main/bet-ref/tests-privnet/BetRef/Tests/Privnet/Tests.hs) file and are being called [here](https://github.com/geniusyield/atlas-examples/tree/main/bet-ref/tests-privnet/betref-privnet-tests.hs). - - -Here is the code (& explanation follows after it): - -```haskell - testCaseSteps "Balance checks & taking pot by closest guesser should pass" $ \info -> withSetup setup info $ \ctx -> do - - -- First step: Construct the parameters and obtain validator from it. - -- - -- Let's define a new User to represent Oracle (not necessary though) - oracleUser <- newTempUserCtx ctx (ctxUserF ctx) (valueFromLovelace 20_000_000) def - (currentSlot, slotConfig) <- getSlotAndConfig ctx - let betUntilSlotDelta = 100 - betRevealSlotDelta = 200 - betUntilTime = slotToBeginTimePure slotConfig (unsafeAdvanceSlot currentSlot betUntilSlotDelta) - betRevealTime = slotToBeginTimePure slotConfig (unsafeAdvanceSlot currentSlot betRevealSlotDelta) - brp = BetRefParams (pubKeyHashToPlutus $ userPkh oracleUser) (timeToPlutus betUntilTime) (timeToPlutus betRevealTime) (valueToPlutus $ valueFromLovelace 10_000_000) - validator = betRefValidator' brp - validatorAddress <- ctxRunC ctx (ctxUserF ctx) $ betRefAddress brp - -- Second step: Putting reference script for validator. - refScript <- addRefScriptCtx ctx (ctxUserF ctx) (validatorToScript validator) - -- Third step: Put some bets. - -- - -- 1st bet. - txBodyLock <- ctxRunI ctx (ctxUser3 ctx) $ placeBet refScript brp (OracleAnswerDatum 1) (valueFromLovelace 10_000_000) (userAddr (ctxUser3 ctx)) Nothing - lockedORef <- findOutput validatorAddress txBodyLock - void $ submitTx ctx (ctxUser3 ctx) txBodyLock - - -- Balance of `(ctxUser2 ctx)` before placing the bet - balance <- ctxQueryBalance ctx (ctxUser2 ctx) - -- - -- 2nd bet. - txBodyLockUser2 <- ctxRunI ctx (ctxUser2 ctx) $ placeBet refScript brp (OracleAnswerDatum 2) (valueFromLovelace 20_000_000) (userAddr (ctxUser2 ctx)) (Just lockedORef) - lockedORef <- findOutput validatorAddress txBodyLockUser2 - void $ submitTx ctx (ctxUser2 ctx) txBodyLockUser2 - -- - -- 3rd bet. - txBodyLock <- ctxRunI ctx (ctxUser3 ctx) $ placeBet refScript brp (OracleAnswerDatum 3) (valueFromLovelace 35_000_000) (userAddr (ctxUser3 ctx)) (Just lockedORef) - lockedORef <- findOutput validatorAddress txBodyLock - void $ submitTx ctx (ctxUser3 ctx) txBodyLock - - -- Fourth step, get the bets pot. - -- - -- Let's first wait for the required amount - ctxWaitUntilSlot ctx (unsafeAdvanceSlot currentSlot betRevealSlotDelta) -- here this `currentSlot` is what we obtained sometime ago, the actual current slot has certainly increased a lot by now. - -- - -- Let's then add for the reference input - refInputORef <- addRefInputCtx ctx (ctxUserF ctx) True (userAddr oracleUser) (datumFromPlutusData (OracleAnswerDatum 2)) - -- - -- Unlock operation - txBodyUnlock <- ctxRunI ctx (ctxUser2 ctx) $ takeBets refScript brp lockedORef (userAddr (ctxUser2 ctx)) refInputORef - void $ submitTx ctx (ctxUser2 ctx) txBodyUnlock - -- - -- Balance of `(ctxUser2 ctx)` after unlocking - let adaExpectedIncrease = valueFromLovelace 45_000_000 - assertUserFunds (txBodyFee txBodyUnlock + txBodyFee txBodyLockUser2) ctx (ctxUser2 ctx) $ balance <> adaExpectedIncrease -``` - -The first line `testCaseSteps "test description" $ \info -> withSetup setup info $ \ctx -> do` can be seen as a boilerplate for all of your tests. - -`ctx` denotes the so called context (of type `Ctx`) and contains information about our users, additional tokens, etc. It is defined in [`GeniusYield.Test.Privnet.Ctx`](https://haddock.atlas-app.io/GeniusYield-Test-Privnet-Ctx.html) module and it is essential to go over that module if you intend to write these tests. - -Variable `info` is used to log messages and you can use it in your test's `do` block like `info $ printf "Hello from %s" "Atlas"` - -We next see the use of `newTempUserCtx` utility function. As mentioned before, we already have nine users in our context, where they have the type `User`: - -```haskell -data User = User - { userPaymentSKey :: !GYPaymentSigningKey - , userStakeSKey :: !(Maybe GYStakeSigningKey) - , userAddr :: !GYAddress - } -``` - -But at rare times, we might need to create a new user. Such a user would not be part of the context and thus would be local to the test creating it[^2]. - -We can do that with the help of `newTempUserCtx` function. It accepts the context parameter, the user which will fund this new user, the value to be given to this new user and a value of type [`CreateUserConfig`](https://haddock.atlas-app.io/GeniusYield-Test-Privnet-Ctx.html#t:CreateUserConfig). - -Next we see the use of `getSlotAndConfig` function. Earlier when we wrote for PSM tests, we could work in absolute slots as we were always running each test from the beginning of ledger but this is not the case here. Thus, we would need to work with relative slots, i.e., we find the current slot and then add offset with respect to it. Function `getSlotAndConfig` has the following definition: - -```haskell -getSlotAndConfig :: Ctx -> IO (GYSlot, GYSlotConfig) -getSlotAndConfig ctx = do - slot <- ctxCurrentSlot ctx - sc <- ctxSlotConfig ctx - return (slot, sc) -``` - -Next we compute for our contract parameters and since we already obtained the slot config, we can use `slotToBeginTimePure` instead of `slotToBeginTime`. - -We next see the use of `ctxRunC`. To understand it, we need to first look at signature of `ctxRunF`. - -```haskell -ctxRunF :: forall t v. Traversable t => Ctx -> User -> GYTxMonadNode (t (GYTxSkeleton v)) -> IO (t GYTxBody) -``` - -We see that it has a type variable `t` which should have an instance of `Traversable`. The other two functions, namely `ctxRunC` & `ctxRunI` call this `ctxRunF` function with suitable instantiation of type variable `t`. - -Here is the table which explains about these three (`ctxRunF`, `ctxRunC` & `ctxRunI`) related functions: - -| Function | When to use? | What does it do? | -|-----------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ctxRunI` | When you want to build for single `GYTxSkeleton` | It wraps our skeleton under `Identity`[^3], that is what suffix `I` stands for | -| `ctxRunF` | When you have say multiple skeletons, like `[GYTxSkeleton]`, or `Maybe GYTxSkeleton` | - | -| `ctxRunC` | When you don't want to build skeletons. This is in particular useful for operations like `utxosAtAddress` | The type constructor `Const` is defined as `newtype Const a b = Const { getConst :: a }` and therefore type parameter `b` is phantom and thus this function helps us ignore for `GYTxSkeleton` | - -We next add for reference script using helper utility function `addRefScriptCtx`. - -We then start placing our bets, once we have the transaction body, we use `findOutput` function which gives us the reference to the UTxO (the first one it finds[^4]) that is being locked at the script address. - -After placing our bets, we use `ctxWaitUntilSlot` to wait till the unlock slot. - -Note that we queried the balance of unlocker so that we can compare with it later. - -We next add for our reference input using `addRefInputCtx` helper utility function. - -Next we perform the unlock operation (calling our `takeBets` operation). - -Lastly, we verify that the unlocker was able to take all the bets by comparing the balance using `assertUserFunds` method. Here is it's definition: - -```haskell --- | Asserts if the user funds change as expected. This function subtracts fees from the given expected value. -assertUserFunds :: Integer -> Ctx -> User -> GYValue -> IO () -assertUserFunds fees ctx u expectedValue = do - currentValue <- ctxQueryBalance ctx u - let expectedValue' = expectedValue `valueMinus` valueFromLovelace fees - assertBool (unwords ["The value didn't change as expected", - "\nExpected: ", show expectedValue', - "\nCurrent: ", show currentValue]) - (currentValue == expectedValue') -``` - -## Writing a failing test - -Now let's see another test where we slightly modify the last step (all the rest is same) and this time we instead try to take funds by not the closest guesser. - -```haskell - -- Fourth step, get the bets pot. - -- - -- Let's first wait for the required amount - ctxWaitUntilSlot ctx (unsafeAdvanceSlot currentSlot betRevealSlotDelta) -- here this `currentSlot` is what we obtained sometime ago, the actual current slot has certainly increased a lot by now. - -- - -- Let's then add for the reference input - refInputORef <- addRefInputCtx ctx (ctxUserF ctx) True (userAddr oracleUser) (datumFromPlutusData (OracleAnswerDatum 2)) - -- - -- Unlock operation - -- But this time by wrong guesser - assertThrown isTxBodyErrorAutoBalance $ ctxRunI ctx (ctxUser3 ctx) $ takeBets refScript brp lockedORef (userAddr (ctxUser3 ctx)) refInputORef -``` - -Notice that we try catching the error using `assertThrown` function. Here `isTxBodyErrorAutoBalance` is defined as (both this & `assertThrown` have their definitions in [`GeniusYield.Test.Privnet.Asserts`](https://haddock.atlas-app.io/GeniusYield-Test-Privnet-Asserts.html) module): - -```haskell -isTxBodyErrorAutoBalance :: BuildTxException -> Bool -isTxBodyErrorAutoBalance (BuildTxBodyErrorAutoBalance _) = True -isTxBodyErrorAutoBalance _ = False -``` - -Thus our `assertThrown` function checks for two things: - -1. Whether our action indeed raises an exception. -2. If an exception is raised, does it satisfy our predicate? For instance, here our predicate was `isTxBodyErrorAutoBalance`. - - -You can also catch for `IO` error like: - -```haskell - errored <- catchIOError (submitTx ctx (ctxUserF ctx) txBody >> pure False) (\_ -> pure True) - unless errored $ assertFailure "Expecting an IOError exception" -``` - - -With this we conclude upon writing integration tests. - -[^1]: For convenience, you can write a bash script which combines setup, running tests & closing the privnet all into one simple script. - -[^2]: Even though this user is local to the test which created it, it would still persist in our private network. - -[^3]: Technically, it's not wrapper that is happening place here but rather we coerce with `Identity` newtype. - -[^4]: Therefore this function is intended to be used when we create only a single output for an external address. - -[^5]: https://unix.stackexchange.com/q/367008. diff --git a/src/pages/getting-started/unit-tests.mdx b/src/pages/getting-started/unit-tests.mdx deleted file mode 100644 index e6a7d8a..0000000 --- a/src/pages/getting-started/unit-tests.mdx +++ /dev/null @@ -1,391 +0,0 @@ -import { Callout } from 'nextra-theme-docs' - -# Unit Tests - -Writing smart contracts & writing tests go hand in hand. Tests are also an excellent way to conveniently check working of our smart contract instead of building transactions using `cardano-cli` and submitting them to local node. - -Now that we have written our smart contract and defined the required operations over it, let's see whether its working as expected. - -Our test suite is a wrapper around [Plutus simple model](https://github.com/mlabs-haskell/plutus-simple-model)[^1] which is created by MLabs. - - - MLabs is working on an evolution of PSM, namely [CLB](https://github.com/mlabs-haskell/clb) which is intended to work exclusively with Atlas. Thus, we have deprecated support of PSM and would soon document overhaul of this test suite. If you would like to avoid using PSM and wait till CLB is ready, you can skip to next section, namely, [Integration Tests](./integration-tests.mdx). - - Currently our PSM wrapper does not support operations related to staking, namely, stake key registration, delegation, de-registration and rewards withdrawal. - - -## Why not just use "Plutus simple model" instead of the wrapper? - -1. **Reusability**: Well firstly to maintain compatibility with our toolchain. For instance, our operations were making use of `GYTxQueryMonad` monad and thus to be able to reuse those same operations we would need to define an instance for it. - -2. **Additional checks**: But secondly and more importantly, plutus simple model lacks some basic checks, for instance: - - * Whether a UTxO satisfies minimum ada requirement. - * Transaction fees requirement. - * Transaction signatures requirement, etc. - - We already handle these cases using our transaction building machinery and thus tests written here reflect the actual environment more. - - -For this guide there should be no need to go over the plutus simple model documentation but this doesn't mean that one shouldn't. It's very lucid and takes few minutes to cover and can be accessed by cloning [their](https://github.com/mlabs-haskell/plutus-simple-model) repository, entering the `docs` folder and running `mdbook serve --open`. - - -## Unit tests for placing a bet operation - - -Entire code file for tests pertaining to this operation is available [here](https://github.com/geniusyield/atlas-examples/tree/main/bet-ref/tests/BetRef/Tests/PlaceBet.hs). Note that we are using [`tasty`](https://hackage.haskell.org/package/tasty) to write our tests and a file calling these individual unit tests is [here](https://github.com/geniusyield/atlas-examples/tree/main/bet-ref/tests/betref-tests.hs). - - -Our objective here would be to write tests for each of our operation, hence the name "unit tests". Though one may write other sort of tests as well, including property based ones. - -### Defining _Run_ for placing a bet operation - -Before any jibber-jabber, let's see the code so that we know it isn't as complex as it might seem: - -```haskell -placeBetRun :: GYTxOutRef -> BetRefParams -> OracleAnswerDatum -> GYValue -> Maybe GYTxOutRef -> GYTxMonadRun GYTxId -placeBetRun refScript brp guess bet mPreviousBetsUtxoRef = do - addr <- ownAddress - skeleton <- placeBet refScript brp guess bet addr mPreviousBetsUtxoRef - sendSkeleton skeleton -``` - - - **Why do we call it "_run_"?** Well if you have gone over the documentation of [plutus simple model](https://github.com/mlabs-haskell/plutus-simple-model), you'll know that they have this "_Run_" monad where actually most of the test code gets executed and we have wrapper around this type, which we call [`GYTxMonadRun`](https://haddock.atlas-app.io/GeniusYield-TxBuilder-Class.html#v:utxoAtTxOutRef). But as an end developer, there is no need to understand about it. - - Also our `GYTxMonadRun` has an instance of `GYTxQueryMonad`. - - -The idea here is that any tests we do related to performing the bet operation would need to call the `placeBet` function which we have defined before. Therefore we have defined a _run_ to call this function. Our `placeBetRun` function takes all those parameters which are required by `placeBet` function, except the address as that we are able to get using `ownAddress` function[^2]. [`ownAddress`](https://haddock.atlas-app.io/GeniusYield-TxBuilder-Run.html#v:ownAddress) function is defined in [`GeniusYield.TxBuilder.Run`](https://haddock.atlas-app.io/GeniusYield-TxBuilder-Run.html) module where actually most of the code related to our wrapper lives and it gives the address of the wallet running this _run_ as we'll shortly see. - -Lastly `sendSkeleton` can be understood as submitting the transaction. It will update the mock ledger state and return the transaction id for the submitted transaction. Note that it does raise an exception in case it fails to submit the transaction. - - -### Understanding `testRun` - -Before we see a trace calling the run we just defined, notice that in our `testGroup`, we have the first test written as: - -```haskell -testRun "Balance checks after placing first bet" $ firstBetTrace (OracleAnswerDatum 3) (valueFromLovelace 20_000_000) 0_182_793 -``` - -Now what is this [`testRun`](https://haddock.atlas-app.io/GeniusYield-Test-Utils.html#v:testRun)? - -This function takes a string to represent the name of the test and a continuation function (of type `Wallets -> Run a`) and then internally generates wallets to give to our continuation function. - -The type `Wallets` is defined as: - -```haskell -data Wallets = Wallets - { w1 :: !Wallet - , w2 :: !Wallet - , w3 :: !Wallet - , w4 :: !Wallet - , w5 :: !Wallet - , w6 :: !Wallet - , w7 :: !Wallet - , w8 :: !Wallet - , w9 :: !Wallet - } deriving (Show, Eq, Ord) -``` - -where `Wallet` is: - -```haskell -data Wallet = Wallet - { walletPaymentSigningKey :: !GYPaymentSigningKey - , walletNetworkId :: !GYNetworkId - , walletName :: !String - } - deriving (Show, Eq, Ord) -``` - -Thus our `testRun` function, generates these 9 wallets where each wallet is having the following three assets: -* Million ada. -* Million `fakeGold`. -* Million `fakeIron`. - -where `fakeGold` and `fakeIron` are our two non-native assets. - -Each call to `testRun` (as you can see - we have multiple tests, all beginning with `testRun`) runs the given test with a fresh (new) blockchain ledger state having given the above balances to those 9 wallets. - -In our case, `"Balance checks after placing first bet"` is the name of the test and `firstBetTrace (OracleAnswerDatum 3) (valueFromLovelace 20_000_000) 0_182_793` is our continuation function. - -### Defining a trace to call `placeBetRun` - -Now let's see the definition `firstBetTrace` we briefly encountered above: - -```haskell --- | Trace for placing the first bet. -firstBetTrace :: OracleAnswerDatum -- ^ Guess - -> GYValue -- ^ Bet - -> Wallets -> Run () -- Our continuation function -firstBetTrace dat bet ws@Wallets{..} = do - -- First step: Get the required parameters for initializing our parameterized script and add the corresponding reference script - (brp, refScript) <- computeParamsAndAddRefScript 40 100 (valueFromLovelace 200_000_000) ws - void $ runWallet w1 $ do -- following operations are ran by first wallet, `w1` - -- Second step: Perform the actual run. - withWalletBalancesCheckSimple [w1 := valueNegate bet] $ do - placeBetRun refScript brp dat bet Nothing -``` - -Here the last argument is of type `Wallets` as we noted. - -Note that this function starts by calling `computeParamsAndAddRefScript`, therefore let's see about it: - -```haskell --- | Function to compute the parameters for the contract and add the corresponding refernce script. -computeParamsAndAddRefScript - :: Integer -- ^ Bet Until slot - -> Integer -- ^ Bet Reveal slot - -> GYValue -- ^ Bet step value - -> Wallets -> Run (BetRefParams, GYTxOutRef) -- Our continuation -computeParamsAndAddRefScript betUntil' betReveal' betStep Wallets{..} = do - let betUntil = slotFromApi (fromInteger betUntil') - betReveal = slotFromApi (fromInteger betReveal') - fmap fromJust $ runWallet w1 $ do - betUntilTime <- slotToBeginTime betUntil - betRevealTime <- slotToBeginTime betReveal - let brp = BetRefParams (pubKeyHashToPlutus $ walletPubKeyHash w8) (timeToPlutus betUntilTime) (timeToPlutus betRevealTime) (valueToPlutus betStep) -- let oracle be wallet `w8`. - mORef <- addRefScript (walletAddress w9) (betRefValidator' brp) - case mORef of - Nothing -> fail "Couldn't find index of the Reference Script in outputs" - Just refScript -> return (brp, refScript) - -``` - -Our first step is to construct the parameter (`BetRefParams`) for our parameterized contract. Recall its type is: - -```haskell -data BetRefParams = BetRefParams - { - brpOraclePkh :: PubKeyHash -- ^ Oracle's payment public key hash. This is needed to assert that UTxO being looked at indeed belongs to the Oracle. - , brpBetUntil :: POSIXTime -- ^ Time until which bets can be placed. - , brpBetReveal :: POSIXTime -- ^ Time at which Oracle will reveal the correct match result. - , brpBetStep :: Value -- ^ Each newly placed bet must be more than previous bet by `brpBetStep` amount. - } -``` - -For `brpBetUntil`, we choose slot 40 but since plutus works in posix time, we need to enter a monad having an instance of `GYTxQueryMonad` to get posix time from slot and therefore that calculation happens inside `runWallet w1`. Similarly for `brpBetReveal` we chose slot 100. - - -[`runWallet`](https://haddock.atlas-app.io/GeniusYield-Test-Utils.html#v:runWallet) is a utility function which enables us to give the environment. Hm.. what environment you ask? Well in general when constructing the transaction from skeleton we need some context, like who is actually submitting this transaction? As we'll need their address to give them the change output. `runWallet` takes as first argument, the wallet to generate context from and then the actual _run_ to run against this context. - - -Now coming back to our parameters, for `brpOraclePkh` parameter, we chose that for wallet 8. And we take our step amount to be 200 ada. - -Though it is not required for this operation (where we place the first bet) but since our `placeBet` function is overloaded to accept the subsequent bet case too - we need to give reference to the UTxO containing reference script. For that we have a helper function called [`addRefScript`](https://haddock.atlas-app.io/GeniusYield-Test-Utils.html#v:addRefScript) which adds the given script at a given address (we chose that for wallet 9) and returns the reference to it (in `Maybe`). - -Now we are almost done to call our _run_ with just one more line to understand. - -`withWalletBalancesCheckSimple` takes a list of tuple[^3] where the first element of the tuple is the wallet and second element denotes the difference in the wallet's value which we expect after the execution of the operation defined inside its `do` block excluding ada required for transaction fees and to satisfy minimum ada requirements of the generated output[^4]. Here we want the balance of wallet 1 (which is the one actually calling this operation) to decrease by the bet amount and also the fees. - - - **How do we know the fees?** - Well by running the test without it and then noting the transaction fees from the log messages. - - -And this covers our first test 🥳. - -### Multiple bets trace - -Now let's write a slightly more involved trace. This time we'll make our trace parameteric over the required contract parameters. - -Here is the signature of our trace: - -```haskell --- | Trace which allows for multiple bets. -multipleBetsTraceWrapper - :: Integer -- ^ slot for betUntil - -> Integer -- ^ slot for betReveal - -> GYValue -- ^ bet step - -> [(Wallets -> Wallet, OracleAnswerDatum, GYValue)] -- ^ List denoting the bets - -> Wallets -> Run () -- Our continuation function -multipleBetsTraceWrapper betUntil' betReveal' betStep walletBets ws = do - -- First step: Get the required parameters for initializing our parameterized script and add the corresponding reference script - (brp, refScript) <- computeParamsAndAddRefScript betUntil' betReveal' betStep ws - -- Second step: Perform the actual bet operations - multipleBetsTraceCore brp refScript walletBets ws -``` - -The first three parameters correspond to the parameters of contract. - -The fourth parameter denotes the different bets. - -We may for instance call this function like so: - -``` -testRun "Balance checks with multiple bets" $ multipleBetsTraceWrapper 400 1000 (valueFromLovelace 10_000_000) - [ (w1, OracleAnswerDatum 1, valueFromLovelace 10_000_000) - , (w2, OracleAnswerDatum 2, valueFromLovelace 20_000_000) - , (w3, OracleAnswerDatum 3, valueFromLovelace 30_000_000) - , (w2, OracleAnswerDatum 4, valueFromLovelace 50_000_000) - , (w4, OracleAnswerDatum 5, valueFromLovelace 65_000_000 <> fakeGold 1000) - ] -``` - -Next we want to add our reference script and compute the actual contract parameters (converting slot to posix) - which is again handled like before. - -We would then like to perform the actual bet operations. But this time we won't concern ourselves much with actual fees but rather take a threshold of 1 ada. Our approach here is to compare the balances before performaing any operation and after performing all the operations and then see that each wallet has lost the bet amount they placed considering threshold fees. - -Note: We use `balance` function to get the balance for the given wallet. - -```haskell --- | Trace which allows for multiple bets. -multipleBetsTraceCore - :: BetRefParams - -> GYTxOutRef -- ^ Reference script - -> [(Wallets -> Wallet, OracleAnswerDatum, GYValue)] -- ^ List denoting the bets - -> Wallets -> Run () -- Our continuation function -multipleBetsTraceCore brp refScript walletBets ws@Wallets{..} = do - let - -- | Perform the actual bet operation by the corresponding wallet. - performBetOperations [] _ = return () - performBetOperations ((getWallet, dat, bet) : remWalletBets) isFirst = do - if isFirst then do - void $ runWallet (getWallet ws) $ do - void $ placeBetRun refScript brp dat bet Nothing - performBetOperations remWalletBets False - else do - -- need to get previous bet utxo - void $ runWallet (getWallet ws) $ do - betRefAddr <- betRefAddress brp - [_scriptUtxo@GYUTxO {utxoRef}] <- utxosToList <$> utxosAtAddress betRefAddr - void $ placeBetRun refScript brp dat bet (Just utxoRef) - performBetOperations remWalletBets False - -- | To sum the bet amount for the corresponding wallet. - sumWalletBets _wallet [] acc = acc - sumWalletBets wallet ((getWallet, _dat, bet) : remWalletBets) acc = sumWalletBets wallet remWalletBets (if getWallet ws == wallet then acc <> valueNegate bet else acc) - -- | Idea here is that for each wallet, we want to know how much has been bet. If we encounter a new wallet, i.e., wallet for whose we haven't yet computed value lost, we call `sumWalletBets` on it. - getBalanceDiff [] _set acc = acc - getBalanceDiff wlBets@((getWallet, _dat, _bet) : remWalletBets) set acc = - let wallet = getWallet ws - wallet'sName = walletName wallet - in - if Set.member wallet'sName set then getBalanceDiff remWalletBets set acc - else - getBalanceDiff remWalletBets (Set.insert wallet'sName set) ((wallet := sumWalletBets wallet wlBets mempty) : acc) - balanceDiffWithoutFees = getBalanceDiff walletBets Set.empty [] - balanceBeforeAllTheseOps <- fmap fromJust $ runWallet w1 $ traverse (\(wallet, _value) -> balance wallet) balanceDiffWithoutFees - performBetOperations walletBets True - balanceAfterAllTheseOps <- fmap fromJust $ runWallet w1 $ traverse (\(wallet, _value) -> balance wallet) balanceDiffWithoutFees - void $ runWallet w1 $ verify (zip3 balanceDiffWithoutFees balanceBeforeAllTheseOps balanceAfterAllTheseOps) - where - -- | Function to verify that the wallet indeed lost by /roughly/ the bet amount. We say /roughly/ as fees is assumed to be within (0, 1 ada]. - verify [] = return () - verify (((wallet, diff), vBefore, vAfter) : xs) = - let vAfterWithoutFees = vBefore <> diff - (expectedAdaWithoutFees, expectedOtherAssets) = valueSplitAda vAfterWithoutFees - (actualAda, actualOtherAssets) = valueSplitAda vAfter - -- threshold = valueFromLovelace 1_000_000 -- 1 ada - threshold = 1_000_000 -- 1 ada - in if expectedOtherAssets == actualOtherAssets && actualAda < expectedAdaWithoutFees && expectedAdaWithoutFees - threshold <= actualAda then verify xs - -- valueGreater vAfterWithoutFees vAfter && valueGreaterOrEqual vAfter (valueMinus vAfterWithoutFees threshold) then verify xs - else fail ("For wallet " <> walletName wallet <> " expected value (without fees) " <> show vAfterWithoutFees <> " but actual is " <> show vAfter) -``` - - - An eagle eye might notice two comments inside the `verify` function. - - Firstly, note that `valueSplitAda` splits our `GYValue` into lovelaces and that which remains besides it. Since fees don't affect non-ada tokens (not yet), we compare with respect to threshold using ada tokens. - - We could also compare `GYValue`'s directly using `valueGreater` (there is also `valueGreaterOrEqual`) as done in comments but the current one is slightly more optimal as we need not compare on non-ada tokens again. - - -### But sometimes we want a test to fail! - -What happens if the newly placed bet is not more than atleast `brpBetStep` amount? What happens if the transaction skeleton was somewhat wrong, say we didn't put `mustBeSignedBy`? What if someone tries to place a bet after `brpBetUntil`? What if... - -Well for all such cases, we can assert that a given trace must fail using `mustFail` like: - -``` -testRun "Not adding atleast bet step amount should fail" $ mustFail . multipleBetsTrace 400 1000 (valueFromLovelace 10_000_000) - [ (w1, OracleAnswerDatum 1, valueFromLovelace 10_000_000) - , (w2, OracleAnswerDatum 2, valueFromLovelace 20_000_000) - , (w3, OracleAnswerDatum 3, valueFromLovelace 30_000_000) - , (w2, OracleAnswerDatum 4, valueFromLovelace 50_000_000) - , (w4, OracleAnswerDatum 5, valueFromLovelace 55_000_000 <> fakeGold 1000)] -``` - -Here wallet `w4` didn't increase the bet by 10 ada and thus must fail. - - - - Sometimes we want to assert specific failure among other possible failures. As `mustFail` above doesn't distinguish among them, one can simply use `catchError` like in [this](https://github.com/geniusyield/atlas/tree/main/tests/GeniusYield/Test/RefInput.hs) test. - - - -## Unit tests for taking the bet pot - - -Entire code file for tests pertaining to this operation is available [here](https://github.com/geniusyield/atlas-examples/tree/main/bet-ref/tests/BetRef/Tests/TakeBetPot.hs). - - -On similar lines as before, let's first define our _run_ for `takeBets` operation: - -```haskell --- | Run to call the `takeBets` operation. -takeBetsRun :: GYTxOutRef -> BetRefParams -> GYTxOutRef -> GYTxOutRef -> GYTxMonadRun GYTxId -takeBetsRun refScript brp toConsume refInput = do - addr <- ownAddress - skeleton <- takeBets refScript brp toConsume addr refInput - sendSkeleton skeleton -``` - -Next, we'll define our trace to call this _run_: - -```haskell --- | Trace for taking bet pot. -takeBetsTrace :: Integer -- ^ slot for betUntil - -> Integer -- ^ slot for betReveal - -> GYValue -- ^ bet step - -> [(Wallets -> Wallet, OracleAnswerDatum, GYValue)] -- ^ List denoting the bets - -> Integer -- ^ Actual answer - -> (Wallets -> Wallet) -- ^ Taker - -> Bool -- ^ To check balance - -> Wallets -> Run () -- Our continuation function -takeBetsTrace betUntil' betReveal' betStep walletBets answer getTaker toCheckBalance ws@Wallets{..} = do - (brp, refScript) <- computeParamsAndAddRefScript betUntil' betReveal' betStep ws - multipleBetsTraceCore brp refScript walletBets ws - -- Now lets take the bet - mMRef <- runWallet w1 $ addRefInput True (walletAddress w8) (datumFromPlutusData $ OracleAnswerDatum answer) - let taker = getTaker ws - case mMRef of - Just (Just refInput) -> do - void $ runWallet taker $ do - betRefAddr <- betRefAddress brp - [_scriptUtxo@GYUTxO {utxoRef, utxoValue}] <- utxosToList <$> utxosAtAddress betRefAddr Nothing - waitUntilSlot $ slotFromApi (fromInteger betReveal') - (if toCheckBalance then withWalletBalancesCheckSimple [taker := utxoValue] $ do - takeBetsRun refScript brp utxoRef refInput else takeBetsRun refScript brp utxoRef refInput) - _anyOtherMatch -> fail "Couldn't place reference input successfully" -``` - -Here we first did the common step of computing the required script parameters and adding the reference script. - -Then we used [`addRefInput`](https://haddock.atlas-app.io/GeniusYield-Test-Utils.html#v:addRefInput) whose purpose here would become clear by seeing its haddock documentation below: - -```haskell --- | Adds an input (whose datum we'll refer later) and returns the reference to it. -addRefInput:: Bool -- ^ Whether to inline this datum? - -> GYAddress -- ^ Where to place this output? - -> GYDatum -- ^ Our datum. - -> GYTxMonadRun (Maybe GYTxOutRef) -``` - -Next we simply wait until time for bet revealation and claim our pot! - -Now that we have our trace for taking bet pot, we can try testing for other conditions - examples for some are written in the [`TakeBetPot.hs`](https://github.com/geniusyield/atlas-examples/tree/main/bet-ref/tests/BetRef/Tests/TakeBetPot.hs) file. - -[^1]: We use a [custom fork](https://github.com/geniusyield/plutus-simple-model/tree/compat) of Plutus simple model. - -[^2]: To convey the message better, we have a defined [`(:=)`](https://haddock.atlas-app.io/GeniusYield-Test-Utils.html#v::-61-) pattern synonym: - - ```haskell - pattern (:=) :: x -> y -> (x, y) - pattern (:=) x y = (x, y) - ``` - -[^3]: Since we require the signature being present in the skeleton, we can't place bet on anyone else's behalf anyways. - -[^4]: If you would like exact fine grained control over balance change, use `withWalletBalancesCheck` instead. diff --git a/src/pages/introduction.mdx b/src/pages/introduction.mdx index ea12188..278daa8 100644 --- a/src/pages/introduction.mdx +++ b/src/pages/introduction.mdx @@ -27,6 +27,6 @@ Work through an end-to-end example here: [Getting Started](./getting-started). - This guide tracks the Atlas version [0.8.1](https://github.com/geniusyield/atlas/releases/tag/v0.8.1). + This guide tracks the Atlas version [0.11.1](https://github.com/geniusyield/atlas/releases/tag/v0.11.1).