Named Token Tutorial
Last updated
Last updated
First we'll learn how to write a very simple contract, called the "Named Token" contract. The contract simply mints a token with an identifier specified by the issuer, and locks the specified amount of the token to the specified return address.
Let's specify the contract.
First - the issuer executes the contract, giving it the following data:
name : string
The name of the issued token
amount: uint64
: The issued amount of the token
returnAddress: Zen.Types.lock
: The recipient of the token
Then - the contract mints the specified amount of the token using the given name as a subidentifier, and locks it to the specified return address.
The whole process looks like this:
Let's write the contract.
Create a text file called "NamedToken.fst", the "fst" suffix is the standard suffix for F* files.
At the top of the file put the module name - it should be identical to the file name (excluding the suffix):
We should also load (using the open
directive) a couple of useful modules (Zen.Base
, Zen.Cost
, and Zen.Data
) into the namespace, which we'll use later on.
The file should now look like this:
Each contract should have at least 2 top-level functions: main
, and cf
.
The main
function is the function which runs with each execution of the contract, and cf
function is the function which describes the cost of the main
function.
Let's write the main
function, it should always have the following type signature:
where n
is the cost of the function and equal to cf txSkel context command sender messageBody wallet state
(notice that cf
doesn't take the contractId
as an argument, since the cost shouldn't depend on it).
In practice we usually don't actually have to specify the types of the parameters, as they would be inferred by the compiler.
It should look like this:
We haven't supplied the body of the function yet, which should go below that line (instead of the ellipsis).
The first thing we need to do is to parse the data - to extract the name, amount, and return address out of it.
The data should be sent to the contract through the messageBody
parameter, in the form of a dictionary, which will contain the specified data as (key, value) pairs, where each key corresponds to one of the specified fields ("name", "amount", and "returnAddress").
Since we assume messageBody
is a dictionary, we need to try to extract a dictionary out of it - this is is done with the tryDict
function, defined in Zen.Data
.
The tryDict
function has the following type signature:
Recall that the data
type is a discriminated union of the following:
So what tryDict
does, is taking a value of type data
, and if that value is a Collection(Dict(d))
- it returns Some d
, and otherwise it returns None
.
Now - since the messageBody
is already an option data
, we can't apply tryDict
on it directly (since it expects a data
), so instead we use the (>!=)
operator from Zen.Data
which have the following type signature:
The dictionary extraction should look like this:
Let's name the result as dict
, using a let
expression, so the main
function should now look like this:
dict
will either contain a Some d
(where d
is a dictionary) or None
.
Now that we have the dictionary, let's extract the required fields out of it, using the tryFind
function (from Zen.Dictionary
).
The tryFind
function has the following type signature:
It takes a key name as an argument, and a dictionary, and if that dictionary has a value with the specified key name it returns it (within a Some
), and otherwise returns None
.
Since dict
is an option (dictionary data) `cost` 64
we can't use tryFind
on it directly, so we'll use the (>?=)
operator (defined in Zen.Data
) instead.
The (>?=)
operator has the following type signature:
To extract the value of the "returnAddress" key, we'll do:
(notice we use the full qualified name here, since we didn't load the Zen.Dictionary
module into the namespace with the open
directive)
This will give us a (costed) option data
value; to extract an actual lock out of that value we'll use the tryLock
function (defined in Zen.Data
):
Let's give a name to the extracted lock, using a let!
expression.
The let!
usage strips the cost out of the declared variable (using the cost monad), so it would be easier to work with - the type of returnAddress
will be option lock
, instead of option lock `cost` m
.
Now the whole main
function should look like this:
To extract the "amount" and "name" keys we'll do something similar (using tryU64
and tryString
, respectively, instead of tryLock
):
Now that we have all of the data, we can use it assuming everything was provided by the issuer.
To consider both the case where the issuer has provided everything and the case where there is missing information, we pattern match on the data, like this:
The 1st case will be executed when all the data was provided, and the 2nd case will be executed if any of the required parameters wasn't provided.
Let's throw an error when some of the parameters are missing.
The function autoFailw
in Zen.ResultT
throws an error (within a ResultT
) and infers the cost automatically.
If all the parameters were provided - we need to check that the provided name of the token is at most 32 characters, because that's the maximum size an asset subidentifier can have.
If the name is longer than 32 characters - we throw an error:
In Zen Protocol assets are defined by 2 parts:
Main Identifier - The contract ID of the contract which have minted the asset.
Subidentifier - The unique ID of the asset, given by 32 bytes.
If the name is short enough to fit as an asset subidentifier - we can define a token with the given name as the subidentifier and the contract ID of this contract as the main identifier (using the fromSubtypeString
function from Zen.Asset
):
(Notice that we're using begin
and end
here instead of parentheses, to make the code cleaner)
Now that we have defined the named token - we mint the specified amount of it, and then lock the minted tokens to the specified return address - this is done by modifying the supplied transaction (txSkel
) with mint
, and then modifying the result with lockToAddress
(both are defined in Zen.TxSkeleton
):
Notice the syntax we're using here - both mint
and lockToAddress
return a costed txSkeleton
, so to chain them we're using the (>>=)
operator (bind) of the cost monad, and then we name the result using a let!
so we can use it as a "pure" txSkeleton
(instead of a costed txSkeleton
).
Now that we've prepared the transaction - all that is left is to return it (using ofTxSkel
from Zen.ContractResult
), and the contract is done:
The whole file should now look like this:
Now we can verify the validity of this file with:
It should verify successfully, returning:
But hold on - we aren't done yet!
We have finished with the main
function, but we still need to define the cf
function.
The type signature of cf
is:
So we should add the cf
function to the end of the file, like this:
To start - let's give it to the value of 0
and then lift it into the cost monad with Zen.Cost.ret
:
Let's try to elaborate the contract, to make sure the cost is correct.
You should get the following error:
Notice how it infers that cf
returns an int
, while it should return a nat
.
To solve it we need to cast the value of cf
into a nat
, using the cast
function:
Let's elaborate it again, now we get the following error:
Look at the number at the bottom - this is the cost that was inferred by the compiler, so let's try to paste it into the function:
Let's try to elaborate again:
Congratulations!
You have written, elaborated, and verified your very first contract.
This time we were lucky - we didn't have to explicitly type our terms and the code was simple enough for the compiler to infer its cost.
With more complex contracts it might not be so easy - in many cases you'll have to explicitly type your terms to convince the compiler that the cost of the contract is what you claim it is.