#
Emulator Trace & Contract Monads
#
The EmulatorTrace Monad
Lecture 4, Part 3
One of the important monads in Plutus is the EmulatorTrace
monad. With this
monad, you can test out Plutus code without having to start a Plutus Playground,
copy & pasting a contract into it, and setting up a simulation through the GUI.
It's defined in the Plutus.Trace.Emulator
package in plutus-contract
.
The way to run an emulator trace is by calling runEmulatorTrace
runEmulatorTrace :: EmulatorConfig ->
EmulatorTrace () ->
([EmulatorEvent], Maybe EmulatorErr, EmulatorState)
The first argument is an EmulatorConfig
The constructor for this config contains 3 items:
_initialChainState
_slotConfig
_feeConfig
Let's look at the InitialChainState
type
type InitialChainState = Either InitialDistribution TxPool
It can take either an InitialDistribution
or a TxPool
.
InitialDistribution
is a map of a wallet value. It specified what wallets you
want to emulate and what values they contain (initial funding).
type InitialDistribution = Map Wallet Value
You could also use TxPool
instead to define transactions that have "happened"
before the emulation.
The second item in the EmulatorConfig
constructor is a SlotConfig
.
This has to do with time. It allows you to set the length of 1 slot and the beginning of slot 0.
The third and final item in the EmulatorConfig
constructor is a FeeConfig
.
It defines the constant fee per transaction and the factor by which to multiply
that fee to scale script execution.
#
Setting up the REPL
Inside a REPL, import the necessary modules.
import Plutus.Trace.Emulator
import Data.Default
In a moment we'll need to pass an EmulatorConfig
to runEmulatorTrace
. We can
use the default
configuration.
We'll also need to pass an emulator trace. We haven't look at emulator traces
yet. But you can see from runEmulatorTrace
's info that EmulatorTrace
is a
monad. Since we know it's a monad, we know we can use the simplest emulator
trace we can do is return ()
.
#
runEmulatorTrace
It won't do anything special. It will just immediately return unit ().
runEmulatorTrace def $ return ()
Even with the simplest version of this trace, an overwhelming amount of data is printed to the console. It's virtually unusable.
#
runEmulatorTraceIO
Instead, you can use runEmulatorTraceIO
which uses the default config
automatically and runs in IO. The result is much more manageable, too.
runEmulatorTraceIO $ return ()
Slot 00000: TxnValidate 98d5fbcefe21113b3f0390c1441e075b8a870cc5a8fa2a56dcde1d8247e41715
Slot 00000: SlotAdd Slot 1
Slot 00001: W1bc5f27: InsertionSuccess: New tip is Tip(slot= Slot 1, blockId= BlockId(1033bd6bfb9d90108db08880ad32a58980ae8dafd14c6217d7f83db6fae6f70c), blockNo= 0). UTxO state was added to the end.
-- and so on
At the very beginning, a transaction is validated. That's the genesis block that's funding the wallets. Then it waits for 2 slots. Then it shows the final balances.
Since we didn't do anything in the emulator trace, nothing really happened and all the wallets still have their initial 100000000 Lovelace.
Final balances
Wallet 1bc5f27d7b4e20083977418e839e429d00cc87f3:
{, ""}: 100000000
Wallet 3a4778247ad35117d7c3150d194da389f3148f4a:
{, ""}: 100000000
-- and so on
#
runEmulatorTraceIO'
If you want to specify a different emulator configuration, you can use
runEmulatorTraceIO'
.
:t runEmulatorTraceIO'
runEmulatorTraceIO'
:: TraceConfig -> EmulatorConfig -> EmulatorTrace () -> IO ()
Notice it takes a new argument we haven't seen yet: TraceConfig
.
:t TraceConfig
TraceConfig
:: (Wallet.Emulator.MultiAgent.EmulatorEvent' -> Maybe String)
-> GHC.IO.Handle.Types.Handle -> TraceConfig
It basically allows you to filter events in the trace. There is an overwhelming
amount of events to trace. You can filter to see only the ones you want using
TraceConfig
and the argument Wallet.Emulator.MultiAgent.EmulatorEvent'
#
Trace.hs
All of my notes for this section are comments in the file itself .
Try out this trace emulation in the REPL by loading Trace.hs and calling test
:l src/Week04/Trace.hs
test
The output has all the information about the emulation. For example, you'll see a lot of information at the start in the genesis block about initially funding the wallets
Later in slot 2 you'll see where logInfo
(from Vesting.hs line 99
)
was called.
Slot 00002: *** CONTRACT LOG: "made a gift of 10000000 lovelace to 80a4f45b56b88d1139da23bc4c3c75ec6d32943c087f250b86193ca7 with deadline POSIXTime {getPOSIXTime = 1596059111000}"
You'll see a lot of waiting for the next few slots.
Then the wallet 2's grab
transaction is submitted in slot 20.
Then you'll see the log from Trace.hs
and the log from the grab
in
Vesting.hs
in slot 22.
Slot 00022: *** USER LOG: reached Slot {getSlot = 22}
Slot 00022: *** CONTRACT LOG: "collected gifts"
Finally you'll again see the final wallet balances.
Wallet 7ce812d7a4770bbf58004067665c3a48f28ddd58:
{, ""}: 109995870
Wallet 872cb83b5ee40eb23bfdab1772660c822a48d491:
{, ""}: 89999990
#
Failing Transaction
You can modify Trace.hs
to make the grab fail by not waiting long enough to
pass the deadline. In that case, the trace output will reflect that there was no
successful grab
.