Introduction
Factori — the key Tezos dApps development framework
Welcome to Factori, a tool designed around the needs of Tezos smart contract & dApp developers based on our long experience in this domain. Factori's mantra is low code. Its primary goal is to automate, as much as possible, boilerplate code generation so that you can focus on writing, testing, and deploying your smart contracts.
The figure below summarizes Factori's main features: given Michelson smart contracts (whether you wrote them yourself or found them on a Tezos network), it automates the following tasks:
- SDKs generation in various languages, like OCaml, Typescript, Python, and C#;
- Basic Web dApp generation to graphically interact with the contracts;
- Plugins generation for Crawlori and Dip dup crawlers to easily index the calls to the smart contracts into a relational database;
- A powerful scenario language to write tests;
- Factori will also provide a light static analysis module for Michelson contracts (e.g., detection of entrypoints permissions and unused storage fields, use of sets/maps instead of big maps).
Factori's long-term goal is to provide an equivalent of Truffle and Ganache tools for Tezos' native smart contracts language, with an additional focus on meta-programming and automatic code generation.
Next step: quick start!
When you use Factori, you will have a working directory, which we will
sometimes call the Factori project directory. Whenever we give
relative paths (.e.g src/python_sdk
), the will implicitly always be
given relative to this working directory.
Let's move to the installation process. For that, we highly advise the Docker approach.
Quickstart
Generating a SDK
Factori can generate SDKs in several languages: Typescript, OCaml, C# and Python.
Generating a web interface (simple Dapp)
Factori can also generate a Web Interface for viewing and interacting with your smart contract using any standard wallet.
Generate a (contract-specific) configuration for a blockchain crawler
Factori can also generate code for the use of blockchain crawlers such as DipDup or Crawlori.
Installation
Factori could be installed in 2 ways.
- Using a docker image, see this section.
- Directly from source by using opam, see this section.
Installation using Docker
Factori script for Docker
The simplest way to use Factori via docker is via this script. Get it with:
wget https://gitlab.com/functori/dev/factori/-/snippets/2509259/raw/main/factori.sh -O factori
Then, make it executable with:
chmod +x factori
To call factori
without a path prefix, move it to, e.g., $HOME/bin/
(assuming $HOME/bin/
appears in $PATH
):
mv factori $HOME/bin/
Now try:
factori
If you didn't pull any factori image yet and 0.6.1 is the latest release, you will get something like:
Warning: It seems that you didn't pull any factori image for the moment.
The tag of the latest release is 0.6.1. Consider downloading it with docker pull registry.gitlab.com/functori/dev/factori:0.6.1.
Let's pull the indicated docker image:
docker pull registry.gitlab.com/functori/dev/factori:0.6.1
Again, run:
factori
Now, it should print something like:
Notice: using the latest pulled factori version 0.6.1.
Use 'factori --help' for help on commands
You are ready to use factori
! You can move to SDK
generation section.
Versions management with the script
The script above can be parameterized via a VERSION
environnement variable. For instance, to use the docker image of the release 0.6.1, you can write:
VERSION=0.6.1 factori <CMD>
Of course, if you didn't pull version 0.6.1 yet, you'll be asked to do it or to pull the latest released version.
In addition to the tag of a released/pulled version, the value of VERSION
environnement variable can be:
latest
, to use thelatest
docker image release;show-pulled
, to show the list of pulled factori images;show-latest-release
, to show the tag of the latest factori release.
Note that the latest release that has been pulled is systematically used when factori
is invoked with:
factori <CMD>
VERISON=next
can be used to target development versions of Factori.
Docker pull
If you don't want to use the script above, you have to pull the docker images yourself. For instance, you can get the image with the latest
with:
docker pull registry.gitlab.com/functori/dev/factori:latest
The list of released factori version is available via gitlab container registry. If you would like to use a previous version, e.g. 0.3.1
, you can run:
docker pull registry.gitlab.com/functori/dev/factori:0.6.1
Then, you should be able to run factori as follows (probably with some extra arguments depending on the usage):
docker run registry.gitlab.com/functori/dev/factori:latest
From source
To install Factori from source code you need to have opam installed in your machine.
Opam
Check this link to install opam.
Clone
Clone the repository:
git clone https://gitlab.com/functori/dev/factori.git
cd factori
(Optional) Opam switch
If you want to have a local opam switch for factori, just run:
make opam-switch
Dependencies
Once opam installed, install all dependencies by running:
make build-deps
Build
To build factori binaries, run:
make
Install
You can install Factori by using:
make install
This will, by default, copy the binary factori.asm
to ~/.local/bin/factori
.
If you want to customize the installation just edit the Makefile
.
SDKs generation
This section demonstrates the generation and use of SDKs through a simple example in the following programming languages:
We assume you are somewhat familiar with the development environment of your chosen language above (node
and npm
for Typescript, opam
for OCaml, etc.).
Before going into more details, let's introduce the example: we will use a contract we deployed on Ghostnet to illustrate the use of the various SDKs generated by Factori. Its address is KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of.
All you need to know about the smart contract is that it has two entrypoints:
hello
: it takes a string and puts it in the contract's storage, in addition to some other updates;ping
: is a NOP, as it doesn't update the contract's storage.
For reference, the corresponding Camligo code is provided below. Note that you don't need to read or understand it to be able to play with the SDKs.
#![allow(unused)] fn main() { (* This the contract's storage type. It is made of: - a [count] field of type nat; - a [date] field of type timestamp; - a [msg] field of type string. *) type storage = { count : nat; date : timestamp; msg: string; } (* The type action has two cases, from which two entrypoints for the contract are derived: - A [ping] entrypoint, that takes a unit parameter; - A [hello] entrpypoint, that takes a string parameter. *) type action = Ping | Hello of string (* This is the main entry of the smart contract. *) let main (param, storage : action * storage) = let ops = ([] : operation list) in match param with | Ping -> (* [ping] enptrypoint is a NOP. *) ops, storage | Hello msg -> (* [hello] entrypoint puts the message in the storage, and updates the counter and the date. *) ops, { count = storage.count + 1n; msg = msg; date = Tezos.get_now () } }
The corresponding Michelson code is:
{ parameter (or (string %hello) (unit %ping)) ;
storage (pair (pair (nat %count) (timestamp %date)) (string %msg)) ;
code { UNPAIR ;
NIL operation ;
SWAP ;
IF_LEFT
{ NOW ; PUSH nat 1 ; DIG 4 ; CAR ; CAR ; ADD ; PAIR ; PAIR }
{ DROP ; SWAP } ;
SWAP ;
PAIR } }
For a more complete documentation, go to the features > SDK generation section.
Typescript SDK
Let's generate a Typescript SDK for KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of and interact with it. See section SDK generation for the details about this contract. The Typescript SDK relies on the Taquito library for forging and signing operations.
Import a KT1 from the Tezos blockchain
To generate the SDK of the contract in the current directory, run:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--typescript \
--name my_contract \
--network ghostnet \
--force
Compile deps and SDK
You will need to have Node installed.
Then you want to install the necessary Typescript dependencies, compile your SDK, and cleanly format the generated code:
make ts-deps
make format-ts # optional but recommended
make ts
You are ready to use the Typescript SDK!
Hello world example
First, you may want to have a look at the content of src/typescript_sdk/src/
directory.
ls src/typescript_sdk/src/
Create a file src/typescript_sdk/src/test.ts
with the following content:
import * as C from "./my_contract_interface"
import * as functolib from "./functolib"
import {
TezosToolkit,
} from "@taquito/taquito"
var config = functolib.ghostnet_config;
const tezosKit = new TezosToolkit(config.node_addr)
const debug = false
async function main(tezosKit: TezosToolkit){
functolib.setSigner(tezosKit, functolib.alice_flextesa.sk);
let hello_kt1 = "KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of"
let res = await C.call_hello(tezosKit, hello_kt1, "Hello from typescript", 0, "", true);
console.log("Operation sent to a node. Its hash is: " + res.hash);
console.log("Check its status: https://ghostnet.tzkt.io/" + res.hash);
// TODO: Add a await inclusion here.
return;
}
main(tezosKit)
Recompile with
make ts
Run with
node src/typescript_sdk/dist/test.js
If everything goes well, you should see your transaction at: https://ghostnet.tzkt.io/KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of
Quick Deploy
If you want to quickly re-originate the contract, you can run
factori deploy --typescript --network ghostnet --storage blockchain my_contract
This will deploy the contract on Ghostnet, using Flextesa's Alice account, and with as initial storage the storage on the blockchain at the moment of importation.
Remark
If you run into the error
TypeError: Do not know how to serialize a BigInt
at JSON.stringify (<anonymous>)
when using JSON.stringify
in your code, the file functolib.ts
exports a variable JSONbig
which has a stringify
method that will
solve this error (thanks to the json-bigint
library). You can hence
simply use JSONbig.stringify
.
Note that, if no language is specified, the SDK generated by Factori is the Typescript one.
Python SDK
Let's generate a Python SDK for KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of and interact with it. See section SDK generation for the details about this contract. The Python SDK relies on the Pytezos library for forging and signing operations.
Import a KT1 from the Tezos blockchain
To generate the SDK of the contract in the current directory, run:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--python \
--name my_contract \
--network ghostnet \
--force
Compile deps and SDK
You want to install the necessary Python dependencies:
pip install pytezos mypy pyright black
Some of these packages may be installed in $HOME/.local/bin
; check
that it is in your path.
Now cleanly format the generated code:
make format-python
You are ready to use the Python SDK!
Hello world example
First, you may want to have a look at the content of
src/python_sdk/
directory.
ls src/python_sdk/
Create a file src/python_sdk/test.py
with the following content:
from importlib.metadata import metadata
import time
import my_contract_python_interface
import blockchain
def main():
debug = False
hello_kt1 = "KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of"
hello_param: my_contract_python_interface.Hello = "Hello from Python"
ophash = my_contract_python_interface.callHello(
hello_kt1,
_from=blockchain.alice,
param=hello_param,
networkName="ghostnet",
debug=debug,
)
print("A hello operation is sent to a node. Its hash is: " + ophash)
print("Waiting for its inclusion in a block ...")
time.sleep(15) # we need to wait for one block before calling the contract
print("Check the status of your operation: https://ghostnet.tzkt.io/" + ophash)
main()
To run it, simply run:
python3 src/python_sdk/test.py
If everything goes well, you should see your transaction at: https://ghostnet.tzkt.io/KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of
OCaml SDK
Let's generate a OCaml SDK for KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of and interact with it. See section SDK generation for the details about this contract. The OCaml SDK relies on the tzfunc library for forging and signing operations.
Import a KT1 from the Tezos blockchain
To generate the SDK of the contract in the current directory, run:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--ocaml \
--name my_contract \
--network ghostnet \
--force
Compile dependencies
You want to install the necessary OCaml dependencies. You need to be
inside an opam
switch, or you can create one with
make _opam
Then install needed dependencies:
opam update
make deps
Now cleanly format the generated code:
make format-ocaml
You are ready to use the OCaml SDK!
Hello world example
First, you may want to have a look at the content of
src/ocaml_sdk/
and src/ocaml_scenarios/
directories:
tree src/ocaml_sdk/ src/ocaml_scenarios/
Edit the file src/ocaml_scenarios/scenario.ml
with the following content:
module M = My_contract_ocaml_interface
module B = Blockchain
let main () =
let open Tzfunc.Rp in
let hello_kt1 = "KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of" in
let>? oph =
M.call_hello
~from:B.alice_flextesa
~kt1:hello_kt1
~node:B.ghostnet_node
"Hello from OCaml"
in
Format.eprintf "A hello operation was sent to the node, with hash %s@." oph ;
Format.eprintf
"Check the status of your operation: https://ghostnet.tzkt.io/%s@."
oph ;
Lwt.return_ok oph
let _ = Tzfunc.Node.set_silent true
let _ = Lwt_main.run @@ main ()
To run it, simply run:
make run_scenario_ocaml
If everything goes well, you should see your transaction at: https://ghostnet.tzkt.io/KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of
Quick Deploy
If you want to quickly re-originate the contract, you can run
factori deploy --ocaml --network ghostnet --storage blockchain my_contract
This will deploy the contract on Ghostnet, using Flextesa's Alice account, and with as initial storage the storage on the blockchain at the moment of importation.
C# SDK
Let's generate a C# SDK for KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of and interact with it. See section SDK generation for the details about this contract. The C# SDK relies on the Netezos library for forging and signing operations.
Import a KT1 from the Tezos blockchain
To generate the SDK of the contract in the current directory, run:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--csharp \
--name my_contract \
--network ghostnet \
--force
Compile deps and SDK
You need to have dotnet
and the Netezos package installed.
For resources, look at:
- Dotnet
- https://netezos.dev/
- https://github.com/baking-bad/netezos.
now initiate the C# project and cleanly format the generated code:
make csharp-init
make format-csharp
You are ready to use the C# SDK!
Hello world example
First, you may want to have a look at the content of
src/csharp_sdk/
directory.
ls src/csharp_sdk/
Edit the file src/csharp_sdk/Program.cs
with the following
content:
using static Blockchain.Identities;
async static Task main()
{
var hello_kt1 = "KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of";
var ophash = await my_contract.Functions.CallHello(aliceFlextesa, hello_kt1, "Hello from C#", 1000000, 100000, 30000, 1000000, "ghostnet", false);
Console.WriteLine($"A hello operation is sent to a node. Its hash is: {ophash}");
Console.WriteLine("Waiting for its inclusion in a block ...");
await Task.Delay(15000); // wait 15 seconds
Console.WriteLine($"Check the status of your operation: https://ghostnet.tzkt.io/{ophash}");
}
await main();
To run it, simply run:
dotnet run --project src/csharp_sdk/
If everything goes well, you should see your transaction at: https://ghostnet.tzkt.io/KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of
Crawler configuration generation
Quickstart generating an SDK for Crawlori or Dipdup:
Dipdup SDK
Let's generate a configuration for the DipDup crawler.
For this tutorial, you will need Poetry(it is a dependency of DipDup). One way to install it is
curl -sSL https://install.python-poetry.org | python3 -
Import KT1 from the blockchain
Start by generating an SDK for the contract in the current directory, with the extra --dipdup
option. If no option is given for the language, the Typescript SDK is generated:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--dipdup \
--name my_contract \
--network ghostnet \
--db-name tmp \
--force
Note: if you change the db-name
option, make sure it does not
contain any special characters except _
.
Look at the code
First, you may want to have a look at the content of src/dipdup/src/
directory.
tree src/dipdup/
src/dipdup/
├── dipdup.json
├── dipdup.yml
├── handlers
│ ├── on_my_contract_hello.json
│ ├── on_my_contract_hello.py
│ ├── on_my_contract_new_storage.json
│ ├── on_my_contract_new_storage.py
│ ├── on_my_contract_ping.json
│ └── on_my_contract_ping.py
├── models.json
└── models.py
First, get dipdup as follows:
poetry init --python ">=3.10,<3.11" -n
poetry add dipdup
poetry shell
Factori has generated a configuration file (dipdup.yml
) for you.
This configuration file will make DipDup indexer track every
transactions calling your contract and the big_map changes.
You should copy the ./src/dipdup/dipdup.yml
file
cp src/dipdup/dipdup.yml ./
Check that the field url
in dipdup.yml
is
url: https://api.ghostnet.tzkt.io/
Change it if it isn't.
The next step is:
dipdup init
This will create the python package and its structure in a
tmp
folder.
cp src/dipdup/models.py src/dipdup/handlers/*.py src/tmp/
Then you should be able to run it with:
dipdup run
Crawlori SDK
Let's generate a configuration for our Crawlori blockchain indexer. Crawlori requires a bit more setup than the other quick start tutorials, but it's well worth the (small) hassle! It allows to crawl Tezos blockchains in a very fast and modular way.
Import KT1 from the blockchain
Start by generating an SDK for the contract in the current directory, with the extra --crawlori
option. If no option is given for the language, the Typescript SDK is generated:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--crawlori \
--name my_contract \
--network ghostnet \
--force
Before we go further, you need to do some setup.
Setting things up
Postgresql setup
In the following, <USER>
is the user of your shell (typically you
can obtain it through the $USER
variable.
If this is your first time using pgocaml
, you might need to install
postgresql
and configure it to give the required rights to your
user.
sudo -i -u postgres -- psql -c 'create user <USER> with createdb;'
Opam setup
Warning: if you run into problems, check that you have disabled sandboxing as indicated at the end of this section.
You want to install the necessary OCaml dependencies. You need to be
inside an opam
switch, or you can create one with
make _opam
Then install needed dependencies:
opam update
make deps
Note: even if you already did this previously in the OCaml quickstart, you still need to re-do it because the dependencies for Crawlori are slightly bigger.
Now cleanly format the generated code:
make format-ocaml
The pgocaml
package doesn't play well with the sandbox mode of OPAM.
So you might need to disable it.
On a fresh install of opam you can proceed like this:
opam init --reinit --disable-sandboxing --bare
opam reinstall pgocaml pgocaml_ppx ez_pgocaml
Otherwise, you can edit the opam config file (default location is ~/.opam/config
).
To disable the sandboxing you can comment out the last three lines:
wrap-build-commands:
["%{hooks}%/sandbox.sh" "build"] {os = "linux" | os = "macos"}
wrap-install-commands:
["%{hooks}%/sandbox.sh" "install"] {os = "linux" | os = "macos"}
wrap-remove-commands:
["%{hooks}%/sandbox.sh" "remove"] {os = "linux" | os = "macos"}
If you edited ~/.opam/config
as said above, you will need to reinstall pgocaml:
opam reinstall pgocaml
Building the crawler
make crawlori
Hello world example
Let's generate a Crawlori plugin for KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of. See section SDK generation for the details about this contract.
Look at the code
First, you may want to have a look at the content of src/typescript_sdk/src/
directory.
ls src/ocaml_crawlori/
Edit the configuration file
Edit the file src/ocaml_crawlori/config.json
:
{ "nodes": [
"https://rpc.tzkt.io/ghostnet/"
],
"sleep": 1,
"start": 3026972,
"confirmations_needed": 2,
"step_forward": 30,
"register_kinds": [],
"originator_address" : "tz1XcgTCbVj8nbJb3DhrsYjNtjVdouZHMmYb"
}
Then to start crawlori, you can run the following command:
_build/default/src/ocaml_crawlori/crawler.exe src/ocaml_crawlori/config.json
Web Interface
Factori can generate a web interface from a KT1:
factori import kt1 . KT1Fjiwsmo49yM7jch6KG4ETLFkatF3Qj8of \
--web \
--name my_contract \
--network ghostnet \
--force
To generate the web page, run:
make ts-deps
make web
and click on the link to the local web page.
Linting & Analysis
Quickstart linting or analyzing a Michelson Smart Contract:
Linting
Lint a Michelson file
Lint a contract using its Michelson compiled form. It can be in either a JSON (.json) or normal Michelson format (.tz).
factori lint michelson [MICHELSON_FILE]
##Lint KT1
Lint an on-chain contract using its KT1.
factori lint kt1 [OPTION] [KT1]
--network=network
- Specify on which network (mainnet,ithacanet,etc...) you would like to perform the operation. Any prefix of more than three letters "mai", "main", "gho" will work.
Note that the Michelson file has a better printing interface because of the possibility to print the exact location of the errors.
Analysis
Analyze a Michelson file
Analyze a contract using its Michelson compiled form. It can be in either a JSON (.json) or normal Michelson format (.tz).
factori analyze michelson [MICHELSON_FILE]
Analyze a KT1
Analyze an on-chain contract using its KT1.
factori analyze kt1 [OPTION] [KT1]
--network=network
- Specify on which network (mainnet,ithacanet,etc...) you would like to perform the operation. Any prefix of more than three letters "mai", "main", "gho" will work.
Features
In this section, we give an overview of Factori's features. (Note that the next section gives quick tutorials for try out each feature.) Here is a list of subsections of the current section:
- SDK generation
- Crawler configuration generation
- Web App generation
- Scenarios
- Michelson Linting
- Quick Deploy
SDKs generation
Automatic code generation
Factori's first and foremost feature is the automatic generation of a static interface, or SDK (Software Development Kit), from a smart contract. This smart contract can be retrieved from a Tezos blockchain (mainnet, ghostnet, flextesa, etc...) or directly from its Michelson code. The interface may be generated once and for all, for instance if you are trying to interact with a fixed, on-chain contract, or it can be generated on the fly as many times as needed, e.g. during the development of a smart contract. If you have done any significant smart contract development on Tezos, it is very likely that you will have written, by hand, some or all of the code that Factori will generate for you in one second. It is even likely that you have rewritten it several times over, and taken significant time to debug your code when your smart contract inevitably changed and the data structures evolved. At least, at Functori, we found ourselves writing and debugging such tedious, boilerplate code over and over until we wrote this tool.
These interfaces can (currently) be generated in four distinct languages: Typescript, Python, C# and OCaml. There are specific subsections for each target programming language, listed below.
When you import a contract into your factori project, the SDK will be
found in a subfolder of src
.
List of supported languages:
Note that each interface involves some amount of static typing (to the level permitted by each language). The next section goes into why that is a good idea.
Why static interfaces?
Why do we generate static code in Factori? What are the advantages over a dynamic interface?
First, note that although the interface generated by Factori is static, it can be re-generated instantly, either because Factori has been updated or because the contract we are working on is evolving (we are developing it, or it has been "updated" on-chain in one of the limited ways this is even possible). It is static in the sense that we don't discover the interface at (contract-interacting, not entrypoint-inferring) runtime.
By contrast, a dynamic interface would typically generate an object, at runtime, containing various methods to interact with the contract (this is the case, for instance, of Pytezos and Taquito).
Typically, when we interact with a smart contract, we want to define, in advance, a storage and some parameters of entrypoint calls. With a dynamic interface and in a dynamic language, we could do this by creating values belonging to the type generated by the framework for the storage and parameters. As a consequence, this type must already exist before runtime.
- First: not all languages are dynamic, and this would not work in e.g. OCaml. If the type of the storage is a record, then we can't possibly write OCaml code which will accept this record before it has been defined.
- Second objection: even if we are working in a dynamic language, we are not certain that, when the contract or the framework changes, these dynamically-generated types are not going to change as well. We will discover this when trying to run our code interacting with the blockchain. Or maybe we won't discover it immediately because duck typing will coerce the previous type into the new one in unpredictable ways. To protect against this, a natural response is to use static typing.
With a static interface, we can access whatever amount of static typing our language provides. This is maximal in a language like OCaml, but Typescript has decent static typing, as do Python (3) and C#. Even if their static typing sometimes is an afterthought (on Javascript and Python), it is enough to catch some bugs and increase confidence in our SDK.
With a dynamic interface, we need either to trust the interface not to change, or to add static type annotations to our types ourselves. And of course, these static type annotations will need to change every time the contract changes, that is to say: some of what Factori does automatically will have to be done by hand.
All this being said, we make extensive use of the existing dynamic tools mentioned above: what we do is add a layer of static typing to protect the programmer against uncaught interface changes. Our work would not be possible without theirs, so shout out to Taquito, Netezos, and Pytezos.
Typescript
In order to use the Typescript Interface generation feature of
Factori, you will need to have npm
installed on your machine.
The Typescript SDK can be found in the src/typescript_sdk
subfolder
of your Factori project directory.
It is generated by activating the --typescript
option of the
factori import kt1
or factori import michelson
commands, as seen below:
factori import kt1 <dir> KT1...XXX --network mainnet --name my_contract --typescript
<dir>
is the working directory (it may be relative, such as .
),
and KT1...XXX
is the address of the contract.
will create the directory <dir>/src/typescript_sdk
where the
following files and directories will appear:
src
:functolib.ts
: A library of types and functions used by all imported interfacesmy_contract_code.json
: the Micheline code of your contract (useful for deployment)my_contract_interface.ts
: the Typescript interface to your contract, described below.
public
(empty folder for now)package.json
(javascript config file)tsconfig.json
(typescript config file)
The command also creates a Makefile with useful commands such as:
make ts-deps
which will install all needed typescript dependencies, and
make ts
which will compile your interface, but also any scenario which you may find yourself writing inside <dir>/src/typescript_sdk/
. For instance, if you create a <dir>/src/typescript_sdk/scenario.ts
, you can run:
make ts
node <dir>/src/dist/scenario.js
Description of the interface file
The interface file (my_contract_interface.ts
using the convention
from above) consists of both
- Types
- Functions
The types describe the contract's storage as well as all the input types of the contract's entrypoints (and any intermediate types which may need to be defined in the process). The functions are (mainly) of three kinds:
- A deploy function
deploy_my_contract
; - Calling functions for each entrypoint, of the form
call_<entrypoint_name>
. - Utility functions for manipulating types, so that in principle, you
never have to use Micheline or Michelson directly:
- Encoding functions from a type to Micheline (
<type name>_encode
); - Decoding functions from Micheline to a type (
<type name>_decode
); - Random generation of elements of the type (
<type name>_generator
); - (technically not a function, but rather a constant): the type
itself expressed in Micheline; this is useful e.g. for querying
big maps (
<type name>_micheline
).
- Encoding functions from a type to Micheline (
Note about Taquito
Factori's Typescript generation uses the awesome Taquito library. Please note that although there are many similarities between the kind of storage that Taquito accepts for a given contract and that generated by Factori, they are not the same and it is not advised to mix them. Taquito generates a dynamic interface (you generally can't look at it until you have retrieved a storage and looked at it), while Factori generates a static interface (you know its exact structure in advance).
Python
In order to use the Python Interface generation feature of Factori,
you will need to have pytezos
installed on your machine.
The Python SDK can be found in the src/python_sdk
subfolder
of your Factori project directory.
It is generated by activating the --python
option of the
factori import kt1
or factori import michelson
commands, as seen below:
factori import kt1 <dir> KT1...XXX --network mainnet --name my_contract --python
<dir>
is the working directory (it may be relative, such as .
),
and KT1...XXX
is the address of the contract.
will create the directory <dir>/src/python_sdk
where the
following files and directories will appear:
blockchain.py
: A library for blockchain specific operations;factori_types.py
: A library of types and functions used by all imported interfaces;my_contract_code.json
: the Micheline code of your contract (useful for deployment);my_contract_python_interface.py
: the Python interface to your contract, described below.
The command also creates a Makefile with useful commands such as:
make python-static-check
which will typecheck all code (generated and custom added) using mypy
and/or pyright
(you can comment whichever line you don't want).
make python-compile
which will run python
on all files of the interface, but also on any scenario which you may find yourself writing inside <dir>/src/python_sdk/
. If you created a <dir>/src/python_sdk/scenario.py
, you can also directly run:
python <dir>/python_sdk/scenario.py
Description of the interface file
The interface file (my_contract_python_interface.py
using the
convention from above) consists of both
- Types
- Functions
The types describe the contract's storage as well as all the input types of the contract's entrypoints (and any intermediate types which may need to be defined in the process). The functions are (mainly) of three kinds:
- A deploy function
deploy_my_contract
; - Calling functions for each entrypoint, of the form
call_<entrypoint_name>
. - Utility functions for manipulating types, so that in principle, you
never have to use Micheline or Michelson directly:
- Encoding functions from a type to Micheline (
<type name>_encode
); - Decoding functions from Micheline to a type (
<type name>_decode
); - Random generation of elements of the type (
<type name>_generator
).
- Encoding functions from a type to Micheline (
Note about Pytezos
Factori's Python generation uses the awesome Pytezos library. Please note that although there are many similarities between the kind of storage that Pytezos accepts for a given contract and that generated by Factori, they are not the same and it is not advised to mix them. Pytezos generates a dynamic interface (you generally can't look at it until you have retrieved a storage and looked at it), while Factori generates a static interface (you know its exact structure in advance).
C#
In order to use the C# Interface generation feature of Factori,
you will need to have Netezos
and dotnet
installed on your machine. For resources, look at:
- Dotnet
- https://netezos.dev/
- https://github.com/baking-bad/netezos.
The C# SDK can be found in the src/csharp_sdk
subfolder of your
Factori project directory.
It is generated by activating the --csharp
option of the
factori import kt1
or factori import michelson
commands, as seen below:
factori import kt1 <dir> KT1...XXX --network mainnet --name my_contract --csharp
<dir>
is the working directory (it may be relative, such as .
),
and KT1...XXX
is the address of the contract.
will create the directory <dir>/src/csharp_sdk
where the
following files and directories will appear:
blockchain.cs
: A library for blockchain specific operations;factori_types.cs
: A library of types and functions used by all imported interfaces;my_contract_code.json
: the Micheline code of your contract (useful for deployment)my_contract_csharp_interface.cs
: the C# interface to your contract, described below.
The command also creates a Makefile with useful commands such as:
make csharp-init
which will initiate a C# project.
make format-csharp
will format the generated code.
make csharp-build
will build the code.
In order to run Program.cs
, if you write e.g. a scenario in it, run
dotnet run --project src/csharp_sdk/
Description of the interface file
The interface file (my_contract_csharp_interface.cs
using the
convention from above) consists of both
- Types
- Functions
The types describe the contract's storage as well as all the input types of the contract's entrypoints (and any intermediate types which may need to be defined in the process). The functions are (mainly) of three kinds:
- A deploy function
deploy_my_contract
; - Calling functions for each entrypoint, of the form
call_<entrypoint_name>
. - Utility functions for manipulating types, so that in principle, you
never have to use Micheline or Michelson directly:
- Encoding functions from a type to Micheline (
<type name>_encode
); - Decoding functions from Micheline to a type (
<type name>_decode
); - Random generation of elements of the type (
<type name>_generator
).
- Encoding functions from a type to Micheline (
OCaml
In order to use the OCaml Interface generation feature of Factori, you
will need to have opam
installed on your machine.
The OCaml SDK can be found in the src/ocaml_sdk
subfolder of your
Factori project directory.
It is generated by activating the --ocaml
option of the
factori import kt1
or factori import michelson
commands, as seen below:
factori import kt1 <dir> KT1...XXX --network mainnet --name my_contract --ocaml
<dir>
is the working directory (it may be relative, such as .
),
and KT1...XXX
is the address of the contract.
will create the directories <dir>/src/ocaml_sdk
,
<dir>/src/ocaml_scenarios
and <dir>/src/libraries
. In src
, the
following files and directories will appear:
- the
src/libraries
folder:blockchain.ml
: A library for blockchain specific operations;factori_types.ml
: A library of types and functions used by all imported interfaces;factory_abstract_types.ml
: A library for advanced scenarios;utils.ml
: Various utilities.
- the
src/ocaml_sdk
folder:my_contract_code.json
: the Micheline code of your contract (useful for deployment)my_contract_ocaml_interface.ml
: the OCaml interface to your contract, described below.my_contract_abstract_ocaml_interface.ml
: the OCaml abstract interface to your contract, only useful for abstract scenarios.
- the
src/ocaml_scenarios
folderscenario.ml
: an empty scenario file, ready with adune
file with all needed dependencies.
The command also creates a Makefile with useful commands such as:
make format-ocaml
which will format your project.
make format-ocaml
will format the generated code.
make ocaml
will build the code.
make run_scenario_ocaml
will run your scenario.
Description of the interface file
The interface file (my_contract_csharp_interface.ml
using the
convention from above) consists of both
- Types
- Functions
The types describe the contract's storage as well as all the input types of the contract's entrypoints (and any intermediate types which may need to be defined in the process). The functions are (mainly) of three kinds:
- A deploy function
deploy_my_contract
; - Calling functions for each entrypoint, of the form
call_<entrypoint_name>
. - Utility functions for manipulating types, so that in principle, you
never have to use Micheline or Michelson directly:
- Encoding functions from a type to Micheline (
<type name>_encode
); - Decoding functions from Micheline to a type (
<type name>_decode
); - Random generation of elements of the type (
<type name>_generator
); - (technically not a function, but rather a constant): the type
itself expressed in Micheline; this is useful e.g. for querying
big maps (
<type name>_micheline
).
- Encoding functions from a type to Micheline (
Crawler configuration generation
This part of the documentation is still in the works. However, you can refer to the Quickstart section.
Web App generation
This part of the documentation is still in the works. However, you can refer to the Quickstart section.
Scenarios
Factori allows you to write scenarios for your smart contracts.
Scenarios
On this topic, see also our blog article which contains examples of OCaml scenarios and another one with elementary OCaml and Typescript scenarios.
Focus on the high-level
You can use the SDK generated by Factori to write scenarios for your smart contracts. This is useful at all stages of a smart contract's life: prototyping, testing, Dapp development, and deployment.
When you are developing a smart contract, you are bound to make many
design changes, each of which might slightly change the interface of
your contract. Instead of manually modifying storage and entrypoint
parameter encodings each time you add or remove a field, you can just
regenerate the interface (it's instantaneous, because Factori is
really fast). Because our interfaces are statically typed, if your
scenario is no more up to date with respect to the contract, instead
of cryptic error from a Tezos node, you will get localized, easily
understandable and fixable errors. For instance, if you just added the
field admin
(of type address
to your storage, and your scenario
deploys an initial storage without the field admin
, you will get an
error saying precisely this. In contrast, if you were deploying by
hand, you might get an error from the node telling you that it expects
a
Pair int (Pair address (Pair (Pair int string) (big_map address int)))
and got a
Pair int (Pair (Pair int string (big_map address int)))
instead...
Speed of iteration
It should be easy and fast to write and execute a scenario in Factori, on any Tezos blockchain (from Flextesa to ghostnet to mainnet). Using Flextesa, you can run it as fast as one block per second, which makes it easy to try out quick changes and adjust.
Defaults
Each contract imported from a blockchain comes with a its downloaded storage from the blockchain, so that you can quickly deploy it using the same storage with sensible defaults. Typically, you might deploy a contract with the same initial storage and change the admin to an address you control.
Advanced scenarios
For advanced uses, Factori comes with a Domain Specific Language (DSL) for scenarios. These uses are typically:
- needing to compile scenarios down to several languages (e.g., OCaml and Typescript);
- needing to execute several scenarios in parallel.
To learn more about this DSL, check out our extensive blog article on the topic.
Linting
Factori offers some linting and analysis capabilities on Michelson contracts. This feature is in a beta state:
Linting
Factori offers some linting capabilities on Michelson contracts. This feature is in a beta state.
Linting
The Factori lint instruction goal is to detect some syntactic problems in the Michelson file that we thought were worth keeping a trace of.
Smart contract structure
The structure of a Michelson smart contract is not predetermined, it has mandatory sections (like the parameter, storage, and code sections) but they can appear in a different order, it depends on the compilation. The order that Factori lint choose is this one: parameter -> storage -> code -> views. If a smart contract doesn't follow this order, the linter will print a warning telling what section is at the wrong place and what was expected.
Annotations
The annotations in the Michelson files are important, they define the entrypoint in the parameter and also the field of the storage.
Missing ones
The lint command shows when an annotation in the storage/parameter is missing. It will be easier for Factori to generate an interface with a lot of annotations.
Forbidden ones
Factori also holds a set of forbidden annotations to warn the user. For example, if you have a field of the storage named transfer_tokens
, Factori will print a warning because it's a reserved word from Michelson. This set of forbidden annotations can grow or shrink depending on the evolution of the tool.
Incoherent ones
Michelson allows multiple uses of the same annotation. It can become confusing if the annotation %token_id
defines a natural but at the same time another annotation %token_id
defines a string. Factori detects these weird cases and warns the user.
Michelson Analysis
Factori offers some analysis capabilities on Michelson contracts. This feature is in a beta state.
Analysis
The factori analysis currently handles the following:
- Unused storage variables detection: It can happen that a field in the storage of a contract is unused, which leads to unnecessary costs;
- Use of
set
,list
,map
in storage: This might be dangerous as these data structures are unbounded and will be fully deserialized every time the contract is called. If they become too big, deserialization gas limits might be hit, preventing any future call to the smart contract. ABS
detection: TheABS
instruction can break a contract's logic if it is (mis)used to convert integers to natural numbers without checking if the integer is negative.IS_NAT
should be preferred to handle the failing case explicitly.- Reentrancy detection: To prevent reentrancy, the analysis returns a
warning if it detects potential reentrancy with the
TRANSFER_TOKENS
Michelson instruction. - Permission analysis: Factori detects the conditions which lead to a failure, and then prints them for each entrypoint.
Quick deploy (re-originate)
Factori lets you quickly re-originate a contract you have imported from the blockchain.
The syntax is
factori deploy OPTIONS CONTRACT_NAME
where
CONTRACT_NAME
is the name under which you have imported your contract. If you don't remember it, you can look up the filecontracts.json
in you Factori project folder, it will appear in thecontract_name
field as in:json "contracts": [ { "contract_name": "test", "original_format": "kT1", "original_kt1": [ "KT1Ty5iiGhgBKxUgAbNbSPU58dx6J3ULVRdk", "mainnet" ], "import_date": "13/4/2023 11:50:6", "options": [ "ocaml", "csharp" ]
where the name of the contract is test.OPTIONS
can benetwork
is the network you wish to deploy to. Typical choices will beghostnet
(wherealice
fromflextesa
is an account which is always replenished and from which you can originate for free) orflextesa
if you are deploying in a sandbox).--ocaml
means that an OCaml interface will be used to deploy the contract. For this to work, you will need to have generated the OCaml SDK for your contract;--typescript
means that a Typescript interface will be used to deploy the contract. For this to work, you will need to have generated the Typescript SDK for your contract.--storage
is the initial storage you want to use, it can be eitherrandom
orblockchain
:random
will use a randomly generated storageblockchain
will use the initial blockchain storage from the contract at the time you imported it. Please note that this will not, however, include the content ofbig_map
s. Other languages will be added in thedeploy
command in the future.
When run, this command will generate a (OCaml or Typescript) scenario which deploys your contract to the desired network. Upon successful deployment, the scenario will be destroyed. If the deployment fails, the scenario will remain so that you can debug what went wrong or submit an issue.
Commands
You can print a list of commands by running
factori --help
You will get
FACTORI(1) Factori Manual FACTORI(1)
NAME
factori - Manage and interact with Tezos smart contracts in multiple
programming languages
SYNOPSIS
factori [COMMAND] …
COMMANDS
analyze kt1 [OPTION]… [KT1]
Lint a KT1 using its Michelson compiled form.
analyze michelson [--force] [--quiet] [--verbose] [OPTION]…
[MICHELSON_FILE]
Lint a contract using its Michelson compiled form. It can be in
either a Json or normal Michelson format.
deploy clean [--force] [--quiet] [--verbose] [OPTION]…
cleanup leftover scenario of the `factori deploy` command
deploy [OPTION]… [CONTRACT_NAME]
Quickly deploy one of your imported contracts.
empty project [OPTION]… [DIRECTORY]
Create an empty Factori project. In most cases you don't need to do
this, you can just start importing your contracts.
help [--man-format=FMT] [OPTION]… [TOPIC]
display help about factori and factori commands
import kt1 [OPTION]… [DIRECTORY] [KT1]
Import an on-chain contract using its KT1. You may specify from
which network it should be pulled.
import michelson [OPTION]… [DIRECTORY] [MICHELSON_FILE]
Import a contract using its Michelson compiled form. It can be in
either a Json or normal Michelson format.
lint kt1 [OPTION]… [KT1]
Lint a KT1 using its Michelson compiled form.
lint michelson [--force] [--quiet] [--verbose] [OPTION]…
[MICHELSON_FILE]
Lint a contract using its Michelson compiled form. It can be in
either a Json or normal Michelson format.
remove contract [OPTION]… [DIRECTORY]
Remove an existing contract from the project. If the --purge option
is activated, it will remove the file, otherwise it will only
remove it logically from the build infrastructure.
rename variables [--force] [--quiet] [--verbose] [OPTION]…
[CONTRACT_NAME]
Rename some or all anonymous types (type0,type1,etc...) possibly
generated by the previous build of the interface. This will be
effective for all programming language interfaces (OCaml,
Typescript, future languages)
sandbox start [OPTION]…
Start a Flextesa sandbox. You will need docker installed as well as
proper permissions (see for instance
https://docs.docker.com/engine/install/linux-postinstall/)
sandbox stop [--force] [--quiet] [--verbose] [OPTION]…
Stop the Flextesa sandbox started with Factori.
COMMON OPTIONS
--help[=FMT] (default=auto)
Show this help in format FMT. The value FMT must be one of auto,
pager, groff or plain. With auto, the format is pager or plain
whenever the TERM env var is dumb or undefined.
--version
Show version information.
EXIT STATUS
factori exits with the following status:
0 on success.
123 on indiscriminate errors reported on standard error.
124 on command line parsing errors.
125 on unexpected internal errors (bugs).
Import KT1
Import an on-chain contract using its KT1.
factori import kt1 [OPTION]… [DIRECTORY] [KT1]
-
--crawlori
- If activated, a crawlori plugin that can crawl the smart contract(s) will be generated (needs ocaml option).
-
--csharp
- If activated, a C# interface for the smart contract(s) will be generated.
-
--db-name=db-name
- This is the name for the psql database (default is project directory basename).
-
--dipdup
- If activated, necessary files to use didup will be generated.
-
-f, --force
- If set to true, Factori will overwrite the input folder for commands that write to files. Defaults to false.
-
--field_prefixes=field_prefixes
- Determines whether record fields are prefixed by the entrypoint name to disambiguate when several entrypoints have the same parameter names.
-
--library
- Determines whether the OCaml code is generated only as a library.
-
--name=CONTRACT_NAME
- Provide the contract name for which you would like to perform the operation.
-
--network=network
- Specify on which network (mainnet,ithacanet,etc...) you would like to perform the operation. Any prefix of more than three letters "mai", "main", "gho" will work.
-
--ocaml
- If activated, an OCaml interface for the smart contract(s) will be generated.
-
--project_name=PROJECT_NAME
- Provide a project name. This will be used e.g. for the opam file name.
-
--python
- If activated, a Python interface for the smart contract(s) will be generated.
-
-q, --quiet
- Set verbosity level to 0.
-
--typescript
- If activated, a Typescript interface for the smart contract(s) will be generated.
Import Michelson
Import a contract using its Michelson compiled form. It can be in
either a Json or normal Michelson format. The options are similar to
import kt1
:
factori import michelson [OPTION]… [DIRECTORY] [MICHELSON_FILE]
-
--crawlori
- If activated, a crawlori plugin that can crawl the smart contract(s) will be generated (needs ocaml option).
-
--csharp
- If activated, a C# interface for the smart contract(s) will be generated.
-
--db-name=db-name
- This is the name for the psql database (default is project directory basename).
-
-f, --force
- If set to true, Factori will overwrite the input folder for commands that write to files. Defaults to false.
-
--field_prefixes=field_prefixes
- Determines whether record fields are prefixed by the entrypoint name to disambiguate when several entrypoints have the same parameter names.
-
--library
- Determines whether the OCaml code is generated only as a library.
-
--name=CONTRACT_NAME
- Provide the contract name for which you would like to perform the operation.
-
--ocaml
- If activated, an OCaml interface for the smart contract(s) will be generated.
-
--project_name=PROJECT_NAME
- Provide a project name. This will be used e.g. for the opam file name.
-
--python
- If activated, a Python interface for the smart contract(s) will be generated.
-
-q, --quiet
- Set verbosity level to 0.
-
--typescript
- If activated, a Typescript interface for the smart contract(s) will be generated.
-
-v, --verbose
- Increase verbosity level.
-
--web
- Determines whether the Web page of the smart contract is generated.
Deploy and Deploy-clean
Deploy
This command enables you to quickly deploy one of your imported contracts. If you want to do a controlled, refined deploy, you should probably not use this command and write a scenario (in one of our 4 supported languages).
-
-f, --force
- If set to true, Factori will overwrite the input folder for commands that write to files. Defaults to false.
-
--network=network
- Specify on which network (mainnet,ithacanet,etc...) you would like to perform the operation.
-
--ocaml
- The contract will be deployed using an OCaml scenario (only use this if you have imported an OCaml interface)
-
-q, --quiet
- Set verbosity level to 0
-
--storage=storage
- Storage type used for the
factori deploy
command.
- Storage type used for the
-
--typescript
- If activated, a Typescript interface for the smart contract(s) will be generated (only use this if you have imported a Typescript interface; in doubt between OCaml and Typescript, prefer Typescript which is simpler to set up).
-
-v, --verbose
- Increase verbosity level.
Deploy-clean
The factori deploy-clean
command will clean up the files generated
by the deploy command for the purpose of originating your contract. It
can be useful if these files are interfering with your development.
Empty project
Importing a contract into an empty folder will create a new project.
However, in some cases, you may want to first create an empty project
and e.g. install all dependencies to gain time before you actually
start working with contracts. In this case, the command factori empty project
, with similar options as factori import kt1
or factori import michelson
will be useful.
--crawlori
- If activated, a crawlori plugin that can crawl the smart contract(s) will be generated (needs ocaml option).
--csharp
- If activated, a C# interface for the smart contract(s) will be generated.
--db-name=db-name
- This is the name for the psql database (default is project directory basename).
-f, --force
- If set to true, Factori will overwrite the input folder for commands that write to files. Defaults to false.
--library
- Determines whether the OCaml code is generated only as a library.
--ocaml
- If activated, an OCaml interface for the smart contract(s) will be generated.
--python
- If activated, a Python interface for the smart contract(s) will be generated.
-q, --quiet
- Set verbosity level to 0.
--typescript
- If activated, a Typescript interface for the smart contract(s) will be generated.
-v, --verbose
- Increase verbosity level.
--web
- Determines whether the Web page of the smart contract is generated.
Sandbox
Factori provides a wrapper to quickly launch (and stop) a flextesa sandbox from Docker. You will need to have Docker installed.
-
sandbox start [OPTION]…
- Start a Flextesa sandbox. You will need docker installed as well as proper permissions (see for instance https://docs.docker.com/engine/install/linux-postinstall/)
-
sandbox stop
- Stop the Flextesa sandbox started with Factori.
Extra
In all likelihood, you won't be using Factori in isolation. In this section we describe how to use the Flextesa local network and the Explorus blockchain interface in conjunction with Factori.
Local network with Flextesa
You can start a Flextesa sandbox
using Factori. This is very useful because you can make it as fast as
one block per second; moreover, you have full access to an alice
account with a large amount of funds (every generated SDK knows how to
use this account). Run
factori sandbox start
to start it, and
factori sandbox stop
to stop it.
There is a --block-interval
option which will set the number of
seconds between blocks (default is 1
).
You can monitor your Flextesa sandbox visually using Explorus.
Explorus Interface
Explorus is a full consensus explorer and light block explorer by Functori. It is very useful to monitor what is happening on any Tezos blockchain, and in particular a sandbox blockchain for which you have no out of the box explorer. Simply open https://explorus.functori.com/, go to the dented wheel on the top right corner, and enter the URL of your sandbox node. If you are using a default Flextesa setup, this will be http://localhost:20000.