Skip to main content

Minting NFTs 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:

const (
description = "Cool NFTs for sale!"
deposit = uint64(1000)
minBid = uint64(500)
)

func startAuction(t *testing.T) (*wasmsolo.SoloContext, *wasmsolo.SoloAgent, wasmtypes.ScNftID) {
ctx := wasmsolo.NewSoloContext(t, fairauction.ScName, fairauction.OnDispatch)
auctioneer := ctx.NewSoloAgent()
nftID := ctx.MintNFT(auctioneer, []byte("NFT metadata"))
require.NoError(t, ctx.Err)

// start the auction
sa := fairauction.ScFuncs.StartAuction(ctx.Sign(auctioneer))
sa.Params.MinimumBid().SetValue(minBid)
sa.Params.Description().SetValue(description)
transfer := wasmlib.NewScTransferBaseTokens(deposit) // deposit, must be >=minimum*margin
transfer.AddNFT(&nftID)
sa.Func.Transfer(transfer).Post()
require.NoError(t, ctx.Err)

return ctx, auctioneer, nftID
}

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 an NFT. To provide the auctioneer with this NFT we use the MintNFT() method to mint a fresh NFT into his account. The minting process will assign a unique NFT ID. Of course, we check that no error occurred during the minting process.

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 assets from the auctioneer address, and the transaction will be signed 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. After the parameters have been set up, we see something new happening. We create an ScTransfer proxy and initialize it with the base tokens that we need to deposit, plus the freshly minted NFT 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. Note that the function NewScTransferBaseTokens() is used as a shorthand to immediately initialize the new ScTransfer proxy with the desired amount of base tokens.

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

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

func TestStartAuction(t *testing.T) {
ctx, auctioneer, nftID := startAuction(t)

nfts := ctx.Chain.L2NFTs(auctioneer.AgentID())
require.Len(t, nfts, 0)
nfts = ctx.Chain.L2NFTs(ctx.Account().AgentID())
require.Len(t, nfts, 1)
require.Equal(t, nftID, ctx.Cvt.ScNftID(&nfts[0]))

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

This test function checks that the NFT was moved by startAuction from the auctioneer's on-chain account to the chain's on-chain account.

The startAuction function of the smart contract will also have posted a time-locked request to the finalizeAuction function by using the Delay() method. Therefore, we need to remove the pending finalizeAuction request from the backlog.

First we move the logical clock forward to a point in time when that request is supposed to have been 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 of having to explicitly wait for those requests is that you can inspect the in-between state, which means that you can test even a function that posts a request in isolation.