Factori now supports Python and C#

Tezos Blockchain Factori Interface dApps Python C#
Published on 2023/02/08
Factori now supports Python and C#


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.

Tzkt Screenshot

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.

  1. 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.
  2. 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.

|