Getting Started#

In this document, we’ll be writting a Pymich implementation of the FA12 standard.

If you have not installed Pymich yet, please go ahead and do so before moving on with the rest of this document.

Contract Structure#

A Pymich contract is defined as a class inheriting from pymich.michelson_types.Contract, and its attributes statically defined. Not that Pymich uses a subset of PEP 484 style hints to type the contract. Let’s investigate how an FA1.2 token enhanced with a mint functionality might be implemented:

from pymich.michelson_type import *
from pymich.stdlib import SENDER
from dataclasses import dataclass


@dataclass
class AllowanceKey(Record):
    owner: Address
    spender: Address


class FA12(Contract):
    tokens: BigMap[Address, Nat]
    allowances: BigMap[AllowanceKey, Nat]
    total_supply: Nat
    owner: Address

    def mint(self, _to: Address, value: Nat):
        if SENDER != self.owner:
            raise Exception("Only owner can mint")

        self.total_supply = self.total_supply + value

        self.tokens[_to] = self.tokens.get(_to, Nat(0)) + value

Storage#

The FA12 class implements our contract. We can see that it defines a storage with four keys, and is roughly equivalent to:

@dataclass
class Storage:
    tokens: BigMap[Address, Nat]
    allowances: BigMap[AllowanceKey, Nat]
    total_supply: Nat
    owner: Address

Where BigMap, Address and Nat are classes that will compile to the Michelson types big map, address and nat respectively. This storage defines the following keys:

  • storage.tokens, the contract ledger, used to associate balances to addresses.

  • storage.allowances, the FA1.2 standard allows for transfering tokens in the name of another address, provided that other address has allowed it via the FA12.approve method, which we’ll implement shortly.

  • storage.total_supply, an storage entry that keeps track of the token total supply.

  • storage.owner, to keep track of some administrator. For instance, only the owner will be allowed to mint.

Notice also the use of the Record type which allows, through inheritence, to define one’s own records (name tuples). We’re using this feature to define an AllowanceKey data structure that’ll compile to an annotated Michelson Pair n whene n is the record dimensionality. We’re using this user-defined record as the storage.allowances big map key type.

Entrypoints#

Entrypoints are defined as public methods for classes that inherit from pymich.michelson_types.Contract returning None. Our FA12.mint entrypoint takes two parameters, which will be refactored into a record by the compiler since Michelson entrypoints only support single arguments. Note that the entrypoint functions require type hints for the arguments.

Looking at the FA12.mint entrypoint, we are introduced to some of the Pymich syntax:

  • Michelson’s FAILWITH instruction can be raised by throwing regular Python excptions.

  • Michelson’s SENDER is available through the constant variable pymich.stdlib.SENDER.

  • Michelson’s nat type can be pushed onto the stack using the pymich.michelson_types.Nat constructor.

  • We use pymich.michelson_type.BigMap.get(key, default) to retrieve the value at a given key if it exists and return a default otherwise.

Finally, you can see that the pymich.michelson_types.BigMap defines attribute getters and setters the same way a regular python dictionary does. However, as described in the Pymich types section, they behave differently as they are more representative of the underlying Michelson datastructure. Having a look at the pymich.michelson_types.BigMap implementation, we can clearly see that:

class BigMap(MichelsonType, Generic[KeyType, ValueType]):
    """Michelson big map abstraction. It differs with a regular Python
    dictionary in that it:
    - is instanciated from literals by deepcopying the literal key/values
    - adding an element will add a copy of that element
    - getting an element will return a copy of that element
    - it is not iterable
    """

    def __init__(self):
        self.__dictionary: Dict[KeyType, ValueType] = {}

    def __getitem__(self, key: KeyType) -> ValueType:
        """Returns a COPY of the value to retrieve"""
        return deepcopy(self.__dictionary[key])

    def __setitem__(self, key: KeyType, value: ValueType):
        """Store a COPY of the value"""
        self.__dictionary[key] = deepcopy(value)

With that in mind, implementing the rest of the FA12 standard is fairly trivial. Let’s go on by implementing the FA2.approve entrypoint:

@dataclass
class FA12(Contract):
    ...

    def approve(self, spender: Address, value: Nat):
        allowance_key = AllowanceKey(SENDER, spender)

        previous_value = self.allowances.get(allowance_key, Nat(0))

        if previous_value > Nat(0) and value > Nat(0):
            raise Exception("UnsafeAllowanceChange")

        self.allowances[allowance_key] = value

We simply define a new public method taking as arguments a spender and a value that the spender can spend in the same of SENDER.

The transfer function is a little longer to implement since we need to take into account the allowances that might have been setup:

@dataclass
class FA12(Contract):
    ...

    def transfer(self, _from: Address, _to: Address, value: Nat):
        if SENDER != _from:
            allowance_key = AllowanceKey(_from, SENDER)

            authorized_value = self.allowances.get(allowance_key, Nat(0))

            if (authorized_value - value) < Int(0):
                raise Exception("NotEnoughAllowance")

            self.allowances[allowance_key] = abs(authorized_value - value)

        from_balance = self.tokens.get(_from, Nat(0))

        if (from_balance - value) < Int(0):
            raise Exception("NotEnoughBalance")

        self.tokens[_from] = abs(from_balance - value)

        to_balance = self.tokens.get(_to, Nat(0))

        self.tokens[_to] = to_balance + value

Notice that we are instantiating integers using pymich.michelson_types.Int when making the comparison (authorized_value - value) < Int(0). This is because the subtraction of two natural numbers results in an integer, and comparison is only compatible between either natural numbers or integers in Michelson. This behavior is implemented in both pymich.michelson_types.Nat and pymich.michelson_types.Int such that Python typecheckers such as Mypy and Pyright that can be integrated directly in your editor can signal you a type error.

Views (Pre-Hangzhou)#

Pre-Hangzhou, views were defined by the TZIP-4 standard and required a callback pattern. Although post-Hangzhou, this is not used so much, the FA1.2 and FA2 token standards were developped before Hangzhou and hence, require some getter entrypoints to behave the same way as TZIP-4. Pymich provides a simple to implement such views by having a contract class public method that returns some value. We now implement all three view methods that the FA1.2 standard defines:

class FA12(Contract):
    ....

    def getAllowance(self, owner: Address, spender: Address) -> Nat:
        return self.allowances.get(AllowanceKey(owner, spender), Nat(0))

    def getBalance(self, owner: Address) -> Nat:
        return self.tokens.get(owner, Nat(0))

    def getTotalSupply(self) -> Nat:
        return self.total_supply

Compiling the contract#

Let’s now compile the contract using the Pymich CLI. You can either output Michelson or its JSON serialization, Micheline. You can also view the Pymich intermediate repersentation if you so desire:

# Michelson output
pymich compile FA12.py > FA12.tz

# Micheline output
pymich compile FA12.py micheline > FA12.tz

# Pymich IR output
pymich compile FA12.py ir > FA12.pymich-ir

Appendix#

The whole contract can be found in the Pymich end-to-end folder.