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 theFA12.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 variablepymich.stdlib.SENDER
.Michelson’s
nat
type can be pushed onto the stack using thepymich.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.