Last edit 13/06/2022
Factori by
Functori is a tool for the development and
testing of Tezos smart contracts. We will deliver our ambitious
roadmap thanks to the grant from the Tezos Foundation.
Currently, factori generates interfaces in two programming languages:
Typescript and OCaml. On the occasion of the first beta
release of
factori, Functori proposes a tutorial that will also serve to
illustrate what it can be used for.
Motivation
History
Factori is being developed in continuity with several projects built
here at Functori: tzfunc, the
mligo ppx. It benefits from
our years-long experience in blockchain and smart contract development
on Tezos-based blockchains.
What is Factori useful for?
Factori positions itself at a particular point in the development
process: it is not a smart contract development tool per
se. Instead, it focuses on a sometimes neglected pain point, starting
with a Michelson contract successfully compiled for the first time.
At this point, developers will want to deploy their contract, interact
with it, play some scenarios, and observe what is happening on the
blockchain. For this, they will want to write lots of code that does
little more than replicate, sometimes in several programming
languages, the interface of their code. Unless they do it all in the
command line in a bash script and then spend hours debugging it every
time some parameter changes.
However, this Michelson contract is probably not a final version: as
they fix bugs, expand features and test it on-chain, new needs will
emerge, and rewrite it, many times. Moreover, almost every time
they rewrite it, all the side-code will need to be rewritten, which
might discourage them from making changes and reduce the quality of
their project after a while.
The goal of factori is to automate as much as possible the generation
of this boilerplate code, generating clean, readable interfaces taking
advantage of annotations in statically typed languages such as
Typescript or OCaml. If developers want to write a scenario for their
contract(s), they can directly import the generated interface and
start. If their contract changes, they only need to reimport the
interface. They will only need to change very specific parts of their
scenarios, and these parts will be pointed out by easily fixable,
clean-type errors.
Of course, some smart contract languages provide an integrated
development environment with a lot of these features, and they are
handy. However, factori positions itself differently since it can
import any Michelson file compiled from any high-level language. In
a single project, developers can combine contracts developed in one or
several languages with contracts pulled directly from the blockchain
into arbitrarily complex scenarios.
Roadmap
Here is an overview of the roadmap we envision for the coming year. Of
course, as Factori becomes more widely used, we expect new needs and
directions to arise, as has been our experience since we started using
Factori internally. All milestones are independent of each other.
A working POC with OCaml and Typescript interface generation
In this initial milestone, we propose a working proof-of-concept of
Factori which can:
- import a smart contract either as a Michelson file or as a KT1,
generate an OCaml and a Typescript interface for its types and
entrypoints;
- create a structured project with a Makefile in which it is easy to start deploying and interacting with the contract.
Factori incorporates a test suite with several nontrivial smart
contracts, a CI on GitLab, as well as an automatically generated
docker executable.
Factori will incorporate our tool
crawlori
so that the
desired information about a smart contract is automatically stored in
a database, be it entrypoint calls, money transfers, storage/big map
updates.
Factori will automatically generate a dynamic web page from a Michelson
smart contract, including:
- forms for calling entrypoints (compatible with beacon)
- a live display of the content of the storage/big maps.
DSL for scenarios
Factori will incorporate a Domain Specific Language (DSL) for
scenarios. This DSL can be interpreted to various interfaces
(tezos-client instructions, Tzfunc, Taquito, etc.) to various Tezos
blockchains (mainnet or testnet or sandboxes).
Extended list of supported programming languages
Factori expands the list of programming languages for which it
provides interfaces. Two intended targets include Java and Python. One
of the goals is to help people write bots in languages they are
accustomed to, e.g., finance.
Code Linting / Static Analysis
Factori will incorporate some level of static analysis of Michelson smart
contracts: warnings about reentrencies, unused storage fields,
inference of permissions to use entrypoints, etc.
To the tutorial
Factori is tested on macOS and Linux for now. The following tutorial
explains how to use Factori to deploy and interact with an FA2 using
Typescript. We will shortly have a tutorial to do the same in OCaml.
Install
The easiest way to get factori if you are not an OCaml developer is
through Docker.
$ docker pull registry.gitlab.com/functori/dev/factori:0.1.4
In order to use the docker version of factori, you probably want to
use this script:
factori.sh
for example:
$ wget https://gitlab.com/-/snippets/2345857/raw/main/factori.sh -O factori
$ chmod +x factori
And then you may use this file as if it were the binary of factori
(try factori --help
for example).
Import a KT1 from the blockchain
The easiest way to try factori if you do not have your own contract at
hand is to import an existing contract from the blockchain. We choose
an FA2 contract on the Ithacanet blockchain.
$ factori import kt1 tutorial KT1ATZMPus96CFMg2s7mHp33bVkHwwpRDS3R \
--name fa2 \
--network ithacanet \
--typescript \
--force
Project 'my-factori-project' created.
tree -P '*.ml|dune|functolib.ts|imported*.ts|*_interface.ts' -I '_build|_opam' --prune /tmp/test_factori/tmp/test_factori
└── src
└── ts-sdk
└── functolib.ts
2 directories, 1 file
replaced /tmp/test_factori/src/ts-sdk/fa2_interface.ts
Import of KT1ATZMPus96CFMg2s7mHp33bVkHwwpRDS3R successful.
/tmp/test_factori
└── src
└── ts-sdk
└── fa2_interface.ts
2 directories, 1 file
Inventory of the generated interface
The file fa2_interface.ts
contains many functions. Note that for
every type it identified from the Michelson contract, it
systematically generated:
- a Typescript type (e.g.
storage
) - an encoder from the Typescript type to the corresponding Michelson type (e.g.
storage_encode
) - a decoder from Michelson to the Typescript type (e.g.
storage_decode
)
For every entrypoint of the contract, it also generated a calling function. And, of course, a deploying function.
Write a scenario
Let's write a small scenario (<dir>/src/ts-sdk/scenario.ts
) which we
will run on the flextesa
sandbox.
First, as always in Typescript, we need a small header:
import {
TezosToolkit,
MichelsonMap,
} from "@taquito/taquito";
import { MichelsonV1Expression } from "@taquito/rpc"
import * as fa2 from "./fa2_interface";
import {big_map,setSigner, config, alice_flextesa, bob_flextesa, wait_inclusion } from "./functolib";
const tezosKit = new TezosToolkit(config.node_addr + ':' + config.node_port);
To make the storage a little easier to write, let's write a small
helper function for building empty big_maps:
function empty_big_map <K,V>(){
let res : big_map<K,V> = {kind : 'literal',value : []}
return(res)
}
We are ready to write our main
function. You can try to fill out the
initial storage yourself by looking at the storage
type in
fa2_interface.ts
.
async function main_fa2(tezosKit: TezosToolkit) {
// Our deployer will be Alice, a pre-filled account on Flextesa
setSigner(tezosKit, alice_flextesa.sk);
// Our initial storage
let init_st : fa2.storage = {
metadata : empty_big_map(),
assets : [
[
[ empty_big_map(), empty_big_map()],
[ {kind: "Owner_or_operator_transfer_constructor"},
{kind: "Optional_owner_hook_constructor"},
{kind: "Optional_owner_hook_constructor"},
{config_api : alice_flextesa.pkh,tag : "TODO"}],
empty_big_map()
],
BigInt(0)
],
admin: [[alice_flextesa.pkh, false], null] };
// All that remains to do is deploy the contract and get its KT1 back
let kt1 = await fa2.deploy_fa2(tezosKit,init_st)
console.log(`KT1 : ${kt1}`)
}
main_fa2(tezosKit)
Compiling the scenario
In order to compile the scenario, you need to first install the
typescript dependencies from the root of your project and compile your
SDK:
$ make ts-deps
Now you can compile your scenario:
$ tsc -p src/ts-sdk/tsconfig.json
Get Flextesa running
Then, get the flextesa sandbox running. If this is your first time, it
may take several minutes and will use some bandwidth, but it will be
instantaneous the next time. Create and copy the following code in
sandbox.sh
:
#!/bin/sh
image=oxheadalpha/flextesa:latest
script=ithacabox
docker run --rm --name \
my-sandbox \
--detach \
-p 20000:20000 \
-e block_time=1 \
"$image" "$script" \
start
Run the scenario
Once the flextesa sandbox is running, run your scenario:
$ node src/ts-sdk/dist/scenarios.js
main from fa2_interface
[deploy_fa2_raw] Deploying new fa2 smart contract
Waiting for confirmation of origination for KT1Wvwo1xrJAqNmasH1dfDp3gA7fNMhkJKVb...
Origination completed.
Calling the deployed contract
Update_operator
Warning: the code snippets below should be added inside the main
function, because the await
keyword is not allowed at toplevel.
Now, let's call our contract, back to the main
function. Let's say
Alice wants to hand Bob control over her account by calling the
update_operator
feature of the FA2.
let param_update_operators : fa2.update_operators = [{kind : "Add_operator_constructor", Add_operator_element : {token_id : BigInt(0),operator : bob_flextesa.pkh, owner : alice_flextesa.pkh}}]
//Alice is the signer of this transaction
setSigner(tezosKit, alice_flextesa.sk);
let update_operator_op = await fa2.call_update_operators(tezosKit,kt1,param_update_operators)
await wait_inclusion(update_operator_op);
console.log(`update operator_op hash: ${update_operator_op.hash}`)
Mint
But Alice doesn't have any money! We want to mint her some tokens:
let mint_param : fa2.mint_tokens = [{amount : BigInt(10),owner : alice_flextesa.pkh}]
let mint_op = await fa2.call_mint_tokens(tezosKit,kt1,mint_param)
await wait_inclusion(mint_op)
console.log(`mint hash: ${mint_op.hash}`)
Transfer
Now Bob can send himself some money from Alice's account:
let param_transfer : fa2.transfer = [{txs : [{token_id : BigInt(0),amount : BigInt(1), to_ : bob_flextesa.pkh}],from_ : alice_flextesa.pkh}]
// We change the signer to Bob
setSigner(tezosKit,bob_flextesa.sk)
let op_transfer = await fa2.call_transfer (tezosKit,kt1,param_transfer)
console.log(`send op_hash: ${JSON.stringify(op_transfer)}`)
Run the complete main
function
First, remember to recompile your scenario:
$ tsc -p src/ts-sdk/tsconfig.json
If we rerun our main function, after a re-deployment, we read:
[wait_inclusion] Waiting inclusion of operation ong4DLA72EqRaxKzWhAdp9GYdk6F2ZDbS3sxRmdCwv2qxd8f36L at level 13
Waiting inclusion ... block level is 13
Waiting inclusion ... block level is 14
update operator_op hash: ong4DLA72EqRaxKzWhAdp9GYdk6F2ZDbS3sxRmdCwv2qxd8f36L
[wait_inclusion] Waiting inclusion of operation oosVXoT24HrHXrEH8wep1FpqxgQSFd9y2NG5Kvrkyh6JkPSghka at level 14
Waiting inclusion ... block level is 14
Waiting inclusion ... block level is 15
mint hash: oosVXoT24HrHXrEH8wep1FpqxgQSFd9y2NG5Kvrkyh6JkPSghka
send op_hash: {"hash":"ooCKqhcV9jRiNAEpt8U5jZjdYAqJjvTsUZ2JEJZZGvwcmzEa5Pe","level":15,"error":null}
Conclusion
Notice that we never had to look at the FA2's code to interact with
it. We do not know or care whether it was written in Ligo, SmartPy,
Archetype, Scaml, or in Michelson directly (well, we might care if we
start trusting this contract; but for interaction, we do not need to
know that).
We are looking for beta version users; while some parts may be rough
on the edges, the core functionalities should work. At Functori, we
have been successfully using factori for internal projects. Feel free
to contribute, from submitting
issues to making
merge
requests.
Shortly, we are looking to integrate one of our other flagship
products, crawlori, a highly
customizable and efficient crawler for Tezos blockchains. We are also
planning on making an explorer so that you can follow operations on
your contract from a comfortable web-based view.