Skip to content

experimenting compiling python to michelson using the ast module

Notifications You must be signed in to change notification settings

tbinetruy/py-mich

Repository files navigation

Goal of the project

In order to simplify onboarding of new developers into the Tezos smartcontract ecosystem, the projects aim to implement a Python backend for Michelson. The goal is thus to allow writing Tezos smart contracts in Python such that the contract behaves isomorphically between the CPython interpreter and the Tezos virtual machine.

Since a contract in PyMich is Python, this means that the classic Python tooling can be used with your favorite editor for linting (Pylint, Flake8, …), type checking (MyPy, PyRighgt, …), powerful language servers (PyRight, Python LSP, …) and code formatters (black, yapf, …).

This project is currently unstable and considered a proof of concept.

Example: a simple token contract

Let us demonstrate how to write a simple token contract in PyMich and show that it behaves identically between the CPython interpreter and the PyTezos Michelson virtual machine:

# contract.py

from dataclasses import dataclass
from typing import Dict
from stubs import *


def require(condition: bool, message: str) -> int:
    if not condition:
        raise Exception(message)

    return 0


@dataclass
class Contract:
    balances: Dict[address, int]
    total_supply: int
    admin: address

    def mint(self, to: address, amount: int):
        require(SENDER == self.admin, "Only admin can mint")

        self.total_supply = self.total_supply + amount

        if to in self.balances:
            self.balances[to] = self.balances[to] + amount
        else:
            self.balances[to] = amount

    def transfer(self, to: address, amount: int):
        require(amount > 0, "You need to transfer a positive amount of tokens")
        require(self.balances[SENDER] >= amount, "Insufficient sender balance")

        self.balances[SENDER] = self.balances[SENDER] - amount

        if to in self.balances:
            self.balances[to] = self.balances[to] + amount
        else:
            self.balances[to] = amount

Testing the contract in CPython

# contract_python_test.py

import unittest
from pytezos.michelson.micheline import MichelsonRuntimeError
import stubs
admin = "Mrs. Foo"
stubs.SENDER = admin

from contract import Contract


class TestContract(unittest.TestCase):
    def test_mint(self):
        from contract import Contract
        contract = Contract(admin=admin, balances={}, total_supply=0)
        amount = 10
        contract.mint(admin, amount)

        assert contract.balances[admin] == amount

        contract = Contract(admin="yolo", balances={}, total_supply=0)
        try:
            contract.mint(admin, amount)
            assert 0
        except Exception as e:
            assert e.args[0] == 'Only admin can mint'

    def test_transfer(self):
        amount_1 = 10
        contract = Contract(admin=admin, balances={admin: amount_1}, total_supply=amount_1)

        investor = "Mr. Bar"
        amount_2 = 4

        contract.transfer(investor, amount_2)

        assert contract.balances[admin] == amount_1 - amount_2
        assert contract.balances[investor] == amount_2

        try:
            contract.transfer(admin, -10)
            assert 0
        except Exception as e:
            assert e.args[0] == 'You need to transfer a positive amount of tokens'

        try:
            contract.transfer(admin, 100)
            assert 0
        except Exception as e:
            assert e.args[0] == 'Insufficient sender balance'

Testing the contract in the Pytezos Michelson REPL

Finally, we can write a similar test using the PyTezos Michelson VM:

# contract_michelson_test.py

import unittest
from compiler import Compiler
from compiler import VM
from pytezos.michelson.micheline import MichelsonRuntimeError

with open("contract.py") as f:
    source = f.read()


class TestContract(unittest.TestCase):
    def test_mint(self):
        micheline = Compiler(source).compile_contract()
        vm = VM()
        vm.load_contract(micheline)

        init_storage = vm.contract.storage.dummy()
        init_storage['admin'] = vm.context.sender

        new_storage = vm.contract.mint({"to": vm.context.sender, "amount": 10}).interpret(storage=init_storage, sender=vm.context.sender).storage
        self.assertEqual(new_storage['balances'], {vm.context.sender: 10})

        try:
            vm.contract.mint({"to": vm.context.sender, "amount": 10}).interpret(storage=init_storage).storage
            assert 0
        except MichelsonRuntimeError as e:
            self.assertEqual(e.format_stdout(), "FAILWITH: 'Only admin can mint'")

    def test_transfer(self):
        micheline = Compiler(source).compile_contract()
        vm = VM()
        vm.load_contract(micheline)

        init_storage = vm.contract.storage.dummy()
        init_storage['admin'] = vm.context.sender
        init_storage['balances'] = {vm.context.sender: 10}

        investor = "KT1EwUrkbmGxjiRvmEAa8HLGhjJeRocqVTFi"
        new_storage = vm.contract.transfer({"to": investor, "amount": 4}).interpret(storage=init_storage, sender=vm.context.sender).storage
        self.assertEqual(new_storage['balances'], {vm.context.sender: 6, investor: 4})

        try:
            vm.contract.transfer({"to": investor, "amount": -10}).interpret(storage=new_storage).storage
            assert 0
        except MichelsonRuntimeError as e:
            self.assertEqual(e.format_stdout(), "FAILWITH: 'You need to transfer a positive amount of tokens'")

        try:
            vm.contract.transfer({"to": investor, "amount": 10}).interpret(storage=new_storage, sender=vm.context.sender).storage
            assert 0
        except MichelsonRuntimeError as e:
            self.assertEqual(e.format_stdout(), "FAILWITH: 'Insufficient sender balance'")

As we can see, we’ve written the same tests for both the Python interpreter and the PyTezos VM. As expected, the contract behaves the same way.

Using existing Python tooling

Bellow are examples of autocomplete, linting and typechecking with Pyright in Emacs. Since I already had it setup to work with Python, it already works with PyMich !

Autocomplete:

./images/py-mich-autocomplete.png

Linting:

./images/py-mich-linting.png

Typechecking:

./images/py-mich-typechecking.png

Todo

  • [x] multi argument functions
  • [x] dictionnaries
  • [x] functions
  • [ ] lists
  • [ ] tuples
  • [ ] closures
  • [x] nested records
  • [ ] tuples

Pass 1: AST expansion

Class rewritting

We’d like to implement classes by rewritting them to classless Python first and compiling the new AST rather than compiling classes to Michelson directly. The idea is to rewritte the following:

class User:
   def __init__(a: int, b: str):
        self.a, self.b = a, b

    def method1(self, arg1: int, arg2: int) -> string:
        self.a = arg1 + arg2
        return "success"

    def method2(self, arg1: str, arg2: str) -> None:
        self.b = arg1 + arg2

user = User(1, "yo")
user.a = 10
user.method1(1, 2)
user.method2("yo", "lo")

As:

@dataclass 
class __User_self:
    a: int
    b: str

def __User___init__(a: int, b:str):
    return __User_self(a, b)

def __User_method1(self: __User_self, arg1: int, arg2: int) -> Tuple[__User_self, str]:
    self.a = arg1 + arg2
    return self, "success"

def __User_method2(self: __User_self, arg1: int, arg2: int) -> __User_self:
    self.b = arg1 + arg2
    return self

user = __User___init__(1, "yo")
user.a = 10
user = _User_method1(user, 1, 2)[0]
user = _User_method2(user, "yo", "lo")

Closures

Similarly, closures can be compiled without touching the Michelson generator by simply rewritting the Python to « closureless » code. We want to transform:

a = "foo"
b = 1
c = 2
def f(d: int) -> int
    return len(a) + b + d
d = f(2) + c

Into:

a = "foo"
b = 1
def (a: str, b: int, d: int) -> int
    return len(a) + b + d
d = f(a, b, 2) + c

This will ensure that the variables used from the closure are always at the same position on the stack relative to the function body.

About

experimenting compiling python to michelson using the ast module

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published