The Factori tool's core is about generating static interfaces for Tezos smart contracts. We want to take advantage of this new release of Factori to announce the support of two new languages (Python and C#).
Factori setup
Create a folder for the tutorial, e.g.:
$ mkdir /tmp/tutorial && cd /tmp/tutorial
Install (or update) factori by using docker:
$ docker pull registry.gitlab.com/functori/dev/factori:0.5.2
To use factori with docker, we advise using the script available
here as the
factori
executable:
$ wget https://gitlab.com/-/snippets/2345857/raw/main/factori.sh -O factori
$ chmod +x factori
When we use factori
below, we mean to use this new executable.
Python
Let's try out the Python framework with an FA2 contract. In order to
run our Python code, we will need to have Pytezos installed
(https://pytezos.org/quick_start.html#installation).
This tutorial was tested with version 3.7.1
of Pytezos.
$ mkdir test_python && cd test_python
$ ../factori import kt1 . KT1ERjU9W6jzZkq8v6caxu65pQkamv1ptn35 --name fa2 --python
If you care about extra-clean code formatting, you can use the command format-python
:
$ pip install black #only if you don't already have it
$ make format-python
You can look at the generated static interface
src/python_sdk/fa2_python_interface.py
(here's an online
version).
It contains type definitions for entrypoint parameters and storage, a
function for deploying the contract, and functions for calling various
entrypoints.
Let's try to play with our contract on ghostnet by writing a custom
src/python_sdk/main.py
. We are going to deploy the FA2 with its
original blockchain storage, and then mint 1000 tokens for Alice.
from importlib.metadata import metadata
import time
import fa2_python_interface
import factori_types
import blockchain
def main():
debug = False
initial_storage = fa2_python_interface.initial_blockchain_storage
initial_storage["administrator"] = blockchain.alice["pkh"]
kt1 = fa2_python_interface.deploy(
_from=blockchain.alice,
initial_storage=initial_storage,
amount=0,
network="ghostnet",
debug=debug,
)
print("Deploy operation of KT1 " + str(kt1) + " sent to a node.")
print("Waiting for its inclusion in a block ...")
time.sleep(15) # we need to wait for one block before calling the contract
mint_param: fa2_python_interface.Mint = {
"amount": 1000,
"address": blockchain.alice["pkh"],
"metadata": factori_types.Map(),
"token_id": 0,
}
ophash = fa2_python_interface.callMint(
kt1,
_from=blockchain.alice,
param=mint_param,
networkName="ghostnet",
debug=debug,
)
print("A mint 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()
We may now run this program:
$ python3 src/python_sdk/main.py
Deploy operation of KT1 KT1BD3d6sFcaNQXzZx9cBPfhMseDUfqA9YvJ sent to a node.
Waiting for its inclusion in a block ...
A mint operation is sent to a node. Its hash is: op7himM4H9KNoTthvPf1p3rZS4qAD1GfCn1FsRnwngDwTPvHTUv
Waiting for its inclusion in a block ...
Check the status of your operation: https://ghostnet.tzkt.io/op7himM4H9KNoTthvPf1p3rZS4qAD1GfCn1FsRnwngDwTPvHTUv
You can go check that it worked on Tzkt.
C#
Let's do the same in C#. Make sure you have everything installed on your system to run C# programs (dotnet, etc...) and that you have installed the Netezos package. For resources, please look at:
This tutorial was tested with version 2.7.5
.
$ mkdir /tmp/tutorial/test_csharp && cd /tmp/tutorial/test_csharp
$ ../factori import kt1 . KT1ERjU9W6jzZkq8v6caxu65pQkamv1ptn35 --name fa2 --csharp
We need to create a new project and install Netezos before building our project. To do so, we can use a predefine Makefile rule that will create a new project and install the Netezos package:
$ make csharp-init
Then let's format the code and build our project:
$ make csharp-format
$ make csharp-build
As before, you can take a look at the generated code (here's an
online version of the main
file)
Just like we wrote a main.py
, we are going to write a
src/csharp_sdk/Program.cs
. One difference with Python is that there
is no automated mechanism in Netezos for guessing parameters such as
fee
, gasLimit
, storageLimit
, so for the time being, we will have
to input them manually. I've done the work for you and the following
contains sensible defaults. Notice that the program is much more
verbose, because C#'s static type system is more rigid (and rigorous)
than Python's.
using static Blockchain.Identities;
using static fa2.initialBlockchainStorage;
using static FactoriTypes.Types;
using System.Numerics;
async static Task main()
{
var storage = initial_blockchain_storage();
storage.administrator = new fa2.Constructor_operator(aliceFlextesa.Address);
var kt1 = await fa2.Functions.Deploy(aliceFlextesa, storage, 1000000, 100000, 30000, 1000000, "ghostnet", false);
if (kt1 == null)
{
Console.WriteLine("Deployment seems to have failed, no KT1 in output.");
return;
}
Console.WriteLine($"Deploy operation of KT1 {kt1} sent to a node.");
Console.WriteLine("Waiting for its inclusion in a block ...");
await Task.Delay(15000); // wait 15 seconds
fa2.Token_id amount = new fa2.Token_id(new BigInteger(1000));
fa2.Token_id token_id = new fa2.Token_id(new BigInteger(0));
fa2.Constructor_operator addr = new fa2.Constructor_operator(aliceFlextesa.Address);
fa2.Mint mintParam = new fa2.Mint(amount, addr, new Map<fa2.K, fa2.V>(), token_id);
var ophash = await fa2.Functions.CallMint(aliceFlextesa, kt1, mintParam, 1000000, 100000, 30000, 1000000, "ghostnet", false);
Console.WriteLine($"A mint 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();
Let's run our program:
$ dotnet run --project src/csharp_sdk/
Result: opLHDaDMo9jJDUMFzcDyGkVZjsVvYCktwaRbx9iNqf4nFE6guza
KT1 : KT1QhLTt4k6G9wRK9oHB6gnCdPGQoJyLixNA
Deploy operation of KT1 KT1QhLTt4k6G9wRK9oHB6gnCdPGQoJyLixNA sent to a node.
Waiting for its inclusion in a block ...
Successful call to mint: ono2aoNesxCRL2daD67EExUV79ciaLyUyZ3AhL43pbQuJmGEUtN
A mint operation is sent to a node. Its hash is: ono2aoNesxCRL2daD67EExUV79ciaLyUyZ3AhL43pbQuJmGEUtN
Waiting for its inclusion in a block ...
Check the status of your operation: https://ghostnet.tzkt.io/ono2aoNesxCRL2daD67EExUV79ciaLyUyZ3AhL43pbQuJmGEUtN
Motivation for generating static interfaces vs. dynamic 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.
Conclusion
That's it! We managed to mint 1000 tokens for Alice in both Python and C#. You can now use Factori to interact with any Michelson smart contract! We are happy to receive any feedback and bug reports, see our repository at Factori's gitlab.