Factori, Introduction and Tutorial

Published on 2022/06/07


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.

Crawlers, web forms

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 functori/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.

Image placeholder