Testing a validator with the emulator

Now, we will test a validator that we created earlier with the Plutus emulator. We will use the GuessingGame.hs validator to define two emulator traces, one for a valid script spend and one for an invalid script spend. We will edit the GuessingGame.hs file directly, adding the new tracing functions. As part of this exercise, we will also learn about the Contract monad, which we will use to create and submit transactions in the emulation.

Writing the emulator trace

Since EmulatorTrace is a monad, we will use do-notation to define its actions. In general, what we want to do is have a contract function that describes the sequence of transactions that will be generated and submitted. We then call the activateContractWallet function with a wallet (we can use knownWallet 1 - corresponding to the first of the 10 default wallets in the emulator) and the contract:

emulatorTrace :: EmulatorTrace ()
emulatorTrace = do
    void Prelude.$ activateContractWallet (knownWallet 1) contract
    void Prelude.$ Emulator.waitNSlots 2

We can also define a helper function for running the trace:

runTrace :: IO ()
runTrace = runEmulatorTraceIO emulatorTrace

Writing the contract

To run a meaningful EmulatorTrace, we have to define a contract that can be used for emulation. The contract type is Contract w s e a:

  • w is the state type of the contract. The state can be updated from inside the contract and is generally used for communication between contract instances. It should not be confused with general logging which is always available through the Contract.logInfo function.

  • s stands for schema, a list of endpoints available to the contract

  • e is the type of error that will be generated if an exception is thrown

  • a is the type of the final value the contract produces if no exception is thrown

For simple examples, such as ours, we do not need to use a contract state, an endpoint schema, or produce a final value. Therefore, a simple type signature for our contract for the GuessingGame validator will be:

contract :: Contract () Empty Text ()

Empty for s means no endpoints are available, and we just use Text to log error messages. Since Contract is a monad, we can use do-notation again. We can start off with some logging:

Okay, now we need to create the first transaction. This is done by using the Ledger.Tx.Constraints module.

The more recent release of plutus-apps has the following change:

  • plutus-ledger-constraints was replaced with plutus-tx-constraints.

Since we are using an older commit, we still use the old import:

We define the constraints of the transaction, i.e. what we want it to do, and the contract constructs a valid transaction based on its constraints. We want the first transaction to send some ADA to the script address along with a datum that will need to be guessed to spend it later, so we use the mustPayToOtherScriptWithDatumInTx function. After we submit the transaction we use awaitTxConfirmed to make sure the transaction is accounted for on the emulated chain.

Next, we want to try to spend the newly generated script UTxO by matching the datum with the redeemer. This is a bit more tricky because we have to tell the contract where to find the UTxO via lookups. In this case, we are spending a script output, so the lookup must know the validator behind the script address (as we have seen before with cardano-cli, to construct a valid transaction spending the script output, we must supply the validator). To make it a bit clearer, we will add a logging line to inspect the utxos and lookups in the contract and inspect it later.

First, we have to get the UTxO(s) at the script address with utxosAt scriptAddress. We create two helper functions for referencing the script address and the validator hash:

Note that this will get all the UTxOs and for simplicity (since we know there will always be only one), we can take just one with the head function. The lookups we need for this transaction are the validator itself, which we define with Constraints.plutusV2OtherScript and the UTxO(s) that are sitting at the script address that we can get with Constraints.unspentOutputs. We join these two monoidal values together with <> (mappend):

Now, we just need to construct the transaction with the correct redeemer that matches the datum (in our case just the unitRedeemer). We use the Constraints.mustSpendScriptOutput function and specify the output reference that we defined oref along with the unitRedeemer. We also must include the unitDatum in the transaction via Constraints.mustIncludeDatumInTx:

The final part is simply submitting the transaction with our given constraints. We use submitTxConstraintsWith and awaitTxConfirmed. Before we do, we can log the oref and lookups as mentioned before:

There are a lot of different imports that we need to take care of for all of the above code to work, so below is a full reference of the imports. We have to import some extra modules from the standard Prelude, most importantly Semigroup as there seems to be some issue when using the emulator with the PlutuxTx version of Semigroup. We also need to hide the module from the PlutusTx.Prelude.

The entire emulator code all together looks like this:

We also need to add the runTrace function to the module export list:

Running the emulator

Finally, we can load the module and run the trace:

Besides seeing that the script output was successfully spent in the second transaction, we can see that our lookups contain information about the output ref and the script itself, which the emulator needs as basic information about where to find the required data for this transaction. After prettifying the output log a bit, it looks like this:

Last updated