Skip to main content

Colored Tokens and Time Locks

Let's examine some less commonly used member functions of the SoloContext. We will switch to the fairauction example to show their usage. Here is the startAuction() function of the fairauction test suite:

var (
auctioneer *wasmsolo.SoloAgent
tokenColor wasmlib.ScColor

func startAuction(t *testing.T) *wasmsolo.SoloContext {
ctx := wasmsolo.NewSoloContract(t, fairauction.ScName, fairauction.OnLoad)

// set up auctioneer account and mint some tokens to auction off
auctioneer = ctx.NewSoloAgent()
tokenColor, ctx.Err = auctioneer.Mint(10)
require.NoError(t, ctx.Err)
require.EqualValues(t, solo.Saldo-10, auctioneer.Balance())
require.EqualValues(t, 10, auctioneer.Balance(tokenColor))

// start the auction
sa := fairauction.ScFuncs.StartAuction(ctx.Sign(auctioneer))
sa.Params.Description().SetValue("Cool tokens for sale!")
transfer := ctx.Transfer()
transfer.Set(wasmlib.IOTA, 25) // deposit, must be >=minimum*margin
transfer.Set(tokenColor, 10) // the tokens to auction
require.NoError(t, ctx.Err)
return ctx

The function first sets up the SoloContext as usual, and then it performs quite a bit of extra work. This is because we want the startAuction() function to start an auction, so that the tests that subsequently use startAuction() can then focus on testing all kinds of bidding and auction results.

First, we are going to need an agent that functions as the auctioneer. This auctioneer will auction off some colored tokens. To provide the auctioneer with colored tokens we use the Mint() method to convert 10 of his plain iota tokens into colored tokens. The mint process will assign the color value, which is equal to the hash of the Tangle transaction that minted them. We save the resulting ScColor value in tokenColor. Note that both auctioneer and tokenColor are global variables that are accessible by any test that needs them.

Next we check that no error occurred during the minting process, and then we verify that the auctioneer now has 10 less plain iota and also has a balance of 10 tokens with the saved token color in its address. Notice how we use the same Balance() method to retrieve both balances. When the token color parameter is omitted, the Balance() method defaults to returning the balance of plain iotas in the address.

Now we are going to start the auction by calling the startAuction function of the fairauction contract. We get the function descriptor in the usual way, but we also call the Sign() method of the SoloContext to make sure that the transaction we are about to post takes its tokens from the auctioneer address, and signs the transaction with the corresponding private key. Very often you won't care who posts a request, and we have set it up in such a way that by default tokens come from the chain originator address, which has been seeded with tokens just for this occasion. But whenever it is important where the tokens come from, or who invokes the request, you need to specify the agent involved by using the Sign() method.

Next we set up the function parameters as usual. Note how we pass the saved tokenColor for example. After the parameters have been set up, we see something new happening. We create a Transfer proxy and initialize it with the 25 iota that we need to deposit, plus the 10 tokens of the saved tokenColor that we are auctioning. Next we use the Transfer() method to pass this proxy before posting the request. This is exactly how we would do it from within the smart contract code. We also have a shorthand function called TransferIotas() that can be used when all you need to transfer is plain iotas which encapsulates the creation of the Transfer proxy, and the initialization with the required amount of iotas.

Finally, we make sure there was no error while posting the request and return the SoloContext. That concludes the startAuction() function.

Here is the first test function that uses our startAuction() function:

func TestFaStartAuction(t *testing.T) {
ctx := startAuction(t)

// note 1 iota should be stuck in the delayed finalize_auction
require.EqualValues(t, 25-1, ctx.Balance(nil))
require.EqualValues(t, 10, ctx.Balance(nil, tokenColor))

// auctioneer sent 25 deposit + 10 tokenColor
require.EqualValues(t, solo.Saldo-25-10, auctioneer.Balance())
require.EqualValues(t, 0, auctioneer.Balance(tokenColor))
require.EqualValues(t, 0, ctx.Balance(auctioneer))

// remove pending finalize_auction from backlog
ctx.AdvanceClockBy(61 * time.Minute)
require.True(t, ctx.WaitForPendingRequests(1))

The startAuction function of the smart contract will have posted a time-locked request to the finalizeAuction function by using the Delay() method. This request needed 1 iota for the request, but the request is still 'in transit' until it is unlocked. We can verify the contract balance after the transfer of 25 iota plus 10 colorToken, minus the 1 iota still locked. Note how we again have an account Balance() method where the color parameter can be omitted, in which case it defaults to the account balance of plain iotas.

We also verify the address balance of the auctioneer after sending the startAuction request, and double-check that no tokens ended up in his contract account.

The final 2 lines of the code are used to remove the pending finalizeAuction request from the backlog. First we move the logical clock forward to a point when that request is supposed to have triggered. Then we wait for this request to actually be processed. Note that this will happen in a separate goroutine in the background, so we explicitly wait for the request counters to catch up with the one request that is pending.

The WaitForPendingRequests() method can also be used whenever a smart contract function is known to Post() a request to itself. Such requests are not immediately executed, but added to the backlog, so you need to wait for these pending requests to actually be processed. The advantage here is that you can inspect the in-between state, which means that you can test even a function that posts a request in isolation.