Implementing a mini token contract with on-chain callbacks (TZIP-12)
Learn how to implement, test, deploy and extend a simplified token contract following the interface proposed in TZIP-12.
A video 📹 walkthrough of this article is available on YouTube as part of the Tezos Dev Day 2020, you can find it here.
Understanding the TZIP-12 standard proposal
TZIP-12 (FA2) is a new iteration of the original FA1 token standard, that was built with the idea of porting ERC-20 to Tezos. At Stove Labs, we drafted an ERC-721-like contract interface for Tezos as the base ground for the Tezos Hunt.
A natural evolution of those two efforts, is the TZIP-12 standard proposal which is inspired from the ERC-1155 standard, combining both ERC-20 & ERC-721 into a unified contract interface.
TZIP-12 is being proposed & actively worked on by the Tocqueville Group, at Stove Labs we’re working on one of the reference implementations using LIGO.
Features that are provided out of the box
The TZIP-12 standard proposal describes a token contract that can store more than a single type of a token, where the token itself is identified by a token_id
.
Single feature thats already implemented in our tutorial contract, is the token transfer
— it allows the token owner to transfer his very own tokens to someone else. Interface for the transfer, including entry-points and their respective type signatures are specified as the following:
You can always find a link to the source code below the code screenshot. Reason why screenshots are used instead of gists, is that Github gists do not have support for LIGO syntax highlighting yet.
Notice that the
transfer_param
is a list of transfers, this allows us to execute batch transfers effectively.
As far as the specification goes, we’re mainly concerned with precisely following the entrypoint interfaces — this means it’ll be easy to integrate for 3rd parties such as contracts or wallets, that might have support for TZIP-12 already.
Feature(s) that we’ll implement
One of the entry-points specified in TZIP-12, is the Balance_of
contract view. It allows 3rd party contracts to query the TZIP-12 instance, for a token balance of a certain owner, sending the result to a the requester contract in a callback operation.
It works like this:
- TZIP-12 contract exposes the
Balance_of
entrypoint, which accepts a list ofrequests
(owner & token_id) and a callback contract. - The callback contract is a combination of an address and an optional entrypoint, which is type-checked on-chain at runtime — which means we can be certain that it supports the required callback parameters, which is a list of
responses
(requests + balance). - List of responses is sent to the callback contract in an operation emitted internally by the TZIP-12 contract.
- It’s up to the receiving contract to decide what to do with the incoming data, however none of those transaction can fail, otherwise the whole chain of transactions will be rolled back.
Setting up the development environment
From now on we’ll get a bit technical, best way forward is to follow the tutorial step by step, install the native dependencies required, compile and run the required code locally. Or alternatively just read the snippets, sit back & relax!
Prerequisites
If you’d like to compile & run all the tutorial steps locally, you should have the following dependencies installed before you proceed:
- Docker — used to run a local Tezos node together with the LIGO compiler (If you’re on linux, follow the post-installation steps as well)
- Node.js —Javascript runtime environment that we’ll use for testing and deployment
- Better Call Dev (BCD) — Debugging service accessible online, without any installation.
We will also use the following dependencies but you don’t have to install them yet, we will install them in the following steps of the tutorial.
- Tezos starter kit — Preconfigured testing & development environment built with Truffle, Flextesa and LIGO.
(Commit hash:a22867c36ba39f398a6b26e680268dee92f90bbd
) - truffle@tezos — Testing framework, originally built for Ethereum that now includes support for Tezos. You don’t need to install it globally, the starter kit will provide the proper version accessible via npm scripts.
Getting started
Once we have the necessary dependencies installed, we can proceed by downloading the tezos starter kit & installing it’s dependencies:
The starter kit includes preconfigured Truffle, Flextesa, bunch of useful helper scripts and an example test suite. You can read more about it here.
Our contracts, or more specifically their main functions will be stored in the contracts/main
directory, while the includable partial snippets will be stored in contracts/partials
. Partials get included in the main file / function, this separation is necessary due to how truffle compiles contracts internally, our partials are not fully fledged contracts but rather standalone functions.
Deployments a.k.a. migration scripts are stored in the migrations
folder, here’s how a simple migration which defines the initial storage of the contract looks like:
Example test suite is also available in the test
directory, here’s how a test looks like:
Before we continue, let’s make sure your tests are really passing. Start your local development node by running:
Please make sure your Docker deamon is running, before attempting to start a local sandbox node
Once your sandbox node is up and running, you can run migrations and tests using truffle:
Implementing the Balance_of entrypoint
It’s perfectly normal to feel a little overwhelmed at any point while reading the upcoming sections of the tutorial, especially if you’re new to strictly typed languages. You can always jump back and forth between the section where you’re at & the tutorial ending — where you can find a ‘visual’ representation of the contract storage & transaction flow.
To implement and test a Balance_of
entrypoint, we will need to do two things:
- Extend the existing contract with
Balance_of
following the TZIP-12 interface spec - Implement a balance requester/receiver contract that exposes two entrypoints. One being
Request_balance
which is used to invokeBalance_of
. And a second one toReceive_balance
. We’ve seen those in the lifecycle diagram at an earlier section of this article.
The very first thing we need to implement are the new types that will be used for the Balance_of
implementation. TZIP-12 specifies a clear interface that we’ll stick to — basic outline is that when requesting a balance, you need to provide a list of requests and a callback view contract.
💡 If you find any inconsistencies between the current TZIP-12 spec and this article, please let us know.
Secondly, we need to implement the actual entrypoint logic — this function should map each request into a response by filling out the actual requested balances for those requests. Then it should emit a callback transaction operation to the specified callback contract.
💡 Did you know that
balance
is a reserved keyword in LIGO?
At last, we will introduce a new action variant, so the balance_of
function can be accessed as an entrypoint:
Now we’re ready to test the new Balance_of
implementation, and for that we will require a separately deployed smart contract, that can interact with our example TZIP-12 implementation, let’s implement an example balance_requester
next.
Implementing the balance requester contract
Our balance requester contract exists only to showcase and test how Balance_of
works. Outline of supported entrypoints for the balance requester can be found at an earlier sections of this article.
We’ll start again by defining the necessary types — the Request_balance
entrypoint will also accept an address of the token contract where Balance_of
should be invoked, together with a list of (balance) requests.
Storage for the balance requester is rather straightforward, it stores the received (balance) responses directly without any modifications, this will help us test both of the contracts together later on.
Crucial part of our implementation, is the request_balance
function, which is responsible for invoking Balance_of
on the provided address of a TZIP-12 contract. This is done by emitting an internal operation aimed at the provided contract address and it’s Balance entrypoint.
You’ll notice that Receive_balance
is so trivial, that it’s implementation can be inlined directly in the main file of the balance requester.
Last thing before we dive into testing our new features, is to write a new migration for the balance requester contract, this migration will run either when we run truffle test
or truffle migrate
.
Testing Balance using the balance requester contract
You’ve made it to the last part of the article unharmed, congratulations! We’re now ready to test, whether the Balance_of
we’ve implemented works as we expect it to. The easiest way to do this is to deploy both the TZIP-12 example contract and the balance requester contract on the same chain, and make them talk to each other in the tests.
The test itself will verify that the operation lifecycle as seen in the diagram earlier, works the way it should. We can make sure that our balance_requester
has received the information it asked for, by digging into its storage for results of the request. We will compare the actual on-chain balance of alice, versus the balance returned to the balance requested by the Balance_of
call.
Don’t forget to import & deploy the balance requester at the beginning of your tests. Same way as the TZIP-12 example is deployed.
Last step in order to verify our tests and contracts work as they should, is to use a block explorer / debugger to inspect the details of the applied operations. Tests have deployed both of our contracts at the following addresses:
- TZIP-12 contract:
KT1JMoWYUZ7jVmMCR9KPzgNi8jnudgvwxDAv
- Balance requester contract:
KT1RGeWQgXwKT5bpdkqdt29uCQps7GZVdin6
We can use those addresses to manually verify the test results, start by opening Better Call Dev and searching for both of the contract addresses. Make sure to select `Sandbox` in the network dropdown in order for BCD to use the RPC of your local sandbox node.
If the tests ran correctly, you should see the following after looking up the TZIP-12 contract:
- Origination of the contract (at the very bottom, not visible on the screenshot)
- Transfer call from the tests
- Balance request from the tests
All of the operations above will be displayed with their respective storage/gas/fees.
For the balance requester contract, you should see the following:
- Request balance call from the tests
- Internal Receive Balance operation sent from the
TZIP-12 contract (KT1JMoWYUZ7jVmMCR9KPzgNi8jnudgvwxDAv
)
You’ve completed the tutorial, congratulations! In the followup tutorial we will implement additional features of the TZIP-12 spec, together with a JS client to interact with the deployed contract in your apps.
You can find the resulting project repository here.
We’re working on a reference implementation of the TZIP-12 standard here.
You can reach out to me directly on telegram at t.me/maht0rz.