djWebdApp Tezos#

Danger

Before you begin, make sure you have followed the setup instructions from Local blockchains.

Setup#

Smart contract#

We will need to instanciate a contract on this blockchain. We’ll use a simple example that looks like some FA12, in Pure Python (rather than “Smart” Python), thanks to Python to Michelson compiler by Thomas Binetruy-Pic, based on Python AST, Pytezos:

from dataclasses import dataclass
from pymich.michelson_types import Address, BigMap, Contract, Nat
from pymich.stdlib import SENDER


@dataclass
class FA12(Contract):
    tokens: BigMap[Address, Nat]
    total_supply: Nat
    owner: Address

    def mint(self, _to: Address, value: Nat):
        # for the sake of the examples, don't apply this security
        #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

    def transfer(self, _from: Address, _to: Address, value: Nat):
        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

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

We already compiled it, but you can change it and recompile it with the following command:

pip install pymich
cd src/djwebdapp_example_tezos/contracts
pymich FA12.py FA12.json

What matters is that the contract .json or .tz files have matching names and are both present in the contracts sub-directory of the Django app where corresponding models are going to live.

Pytezos Client#

Sandbox ids are predefined and hardcoded, you can find them in the tezos-init-sandboxed-client.sh script.

Let’s deploy our example contract using pytezos, first install pytezos and start a python shell with the ./manage.py shell command at the root of our repository:

pip install pytezos
./manage.py shell

Note

The above example also works in a normal Python shell started with the python command, but we need to be in the Django shell of the demo project to go through this tutorial anyway.

In the shell, do the following to have a pytezos client with a sandbox account:

# use local blockchain with sandbox account
from pytezos import pytezos
client = pytezos.using(
    key='edsk3gUfUPyBSfrS9CCgmCiQsTCHGkviBDusMxDJstFtojtc1zcpsh',
    shell='http://tzlocal:8732',
)

Check your client balance:

>> client.account()
{'balance': '3997440000000', 'delegate': 'tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx', 'counter': '0'}

With the tezos sandbox, we’ll use the default account which is already provisionned with some ethers.

Let’s deploy our example contract using Web3py, install it and start a Python shell with the ./manage.py shell command at the root of our repository:

Blockchain#

Now that we have deployed a contract, let’s setup djwebdapp for a local tezos node, also programatically in ./manage.py shell:

# First, we need to add a blockchain in the database
from djwebdapp.models import Blockchain
blockchain, _ = Blockchain.objects.get_or_create(
    name='Tezos Local',
    provider_class='djwebdapp_tezos.provider.TezosProvider',
    min_confirmations=2,  # two blocks to be safe from reorgs
)

# Add our node to the blockchain
blockchain.node_set.get_or_create(endpoint='http://tzlocal:8732')

Account#

Note

You may rotate Fernet keys used for encryption, please refer to djfernet documentation.

from djwebdapp.models import Account

# Given a blockchain and no address, djwebdapp will create a keypair
admin = Account.objects.create(blockchain=blockchain, name='Admin')

# Now, provision this wallet with some xtz from the default client wallet
client.transaction(
    destination=admin.address,
    amount=1_000_000,
).autofill().sign().inject(_async=False)

# Wait until the account receives the balance
while not admin.balance:
    admin.refresh_balance()

Models#

Custom#

Along with our smart contract, we’re creating some models to normalize all the data both ways: to deploy transactions, as well as to index them.

FA12Tezos

Subclass of TezosTransaction and defines contract_name as well as normalizer_class, which we’ll define in the next chapter

FA12MintTezos

Subclass of TezosCall and defines entrypoint

Note that both models define a get_args() method to return the arguments that the blockchain client should use when deploying.

from django.db import models

from djwebdapp_tezos.models import TezosCall, TezosContract


class FA12Tezos(TezosContract):
    contract_name = 'FA12'
    normalizer_class = 'FA12TezosNormalizer'
    token_name = models.CharField(max_length=200)
    token_symbol = models.CharField(max_length=10)

    def get_init_storage(self):
        return dict(
            tokens={},
            total_supply=0,
            owner=self.sender.address,
        )


class FA12TezosMint(TezosCall):
    entrypoint = 'mint'
    target_contract = models.ForeignKey(
        'FA12Tezos',
        on_delete=models.CASCADE,
    )
    mint_account = models.ForeignKey(
        'djwebdapp.Account',
        on_delete=models.CASCADE,
    )
    mint_amount = models.PositiveIntegerField()

    def __str__(self):
        return f'mint({self.mint_account.address}, {self.mint_amount})'

    def get_args(self):
        return dict(
            _to=self.mint_account.address,
            value=self.mint_amount,
        )


class FA12TezosBalance(models.Model):
    fa12 = models.ForeignKey(
        'FA12Tezos',
        on_delete=models.CASCADE,
    )
    account = models.ForeignKey(
        'djwebdapp.Account',
        on_delete=models.CASCADE,
    )
    balance = models.PositiveIntegerField(default=0)

    class Meta:
        unique_together = (
            ('fa12', 'account'),
        )

    def __str__(self):
        return f'{self.account} balance: {self.balance}'

Contract deployment#

Time to see the beauty of all this, to deploy this smart contract, and make a bunch of mint calls through Django models!

from djwebdapp_example_tezos.models import FA12Tezos, FA12TezosMint

# create a normalized contract object
contract = FA12Tezos.objects.create(
    blockchain=blockchain,
    sender=admin,
    token_name='Your Token',
    token_symbol='YT',
)

# make a couple of mint calls
first_mint = FA12TezosMint.objects.create(
    target_contract=contract,
    sender=admin,
    mint_account=admin,
    mint_amount=100,
)
second_mint = FA12TezosMint.objects.create(
    target_contract=contract,
    sender=admin,
    mint_account=admin,
    mint_amount=200,
)

# let's deploy each transaction one by one
# run ./manage.py spool in a bash loop instead of doing this in python
import time
while blockchain.provider.spool():
    blockchain.wait()
    blockchain.provider.index()

contract.refresh_from_db()
print(contract.level, contract.hash, contract.address)

first_mint.refresh_from_db()
print(first_mint.level, first_mint.hash)

second_mint.refresh_from_db()
print(second_mint.level, second_mint.hash)

# Given all these transactions are sent from the same account, each was done in
# a different block to prevent noonce issues
assert contract.level < first_mint.level < second_mint.level

Indexing and normalization#

Indexing is the process of parsing data from the blockchain, normalization is the process of transforming incomming data into structured relational data.

To map incomming blockchain data into models, we’ll define a Normalizer for that contract, in a normalizers.py file in the same app, and define a method par contract function that will be called by the indexer to normalize the data into your models:

from djwebdapp.models import Account
from djwebdapp.normalizers import Normalizer

from djwebdapp_example_tezos.models import FA12Tezos, FA12TezosMint, FA12TezosBalance


class FA12TezosNormalizer(Normalizer):
    def mint(self, call, contract):
        account, _ = Account.objects.get_or_create(
            address=call.args['_to'],
        )
        call, _ = FA12TezosMint.objects.update_or_create(
            tezostransaction_ptr_id=call.id,
            defaults=dict(
                target_contract=contract,
                mint_account=account,
                mint_amount=call.args['value'],
            )
        )
        balance, _ = FA12TezosBalance.objects.get_or_create(
            account=account,
            fa12=contract,
        )
        balance.balance += call.mint_amount
        balance.save()

First, let’s call a smart contract function from outside djwebdapp, it’s the call that we are going to index and normalize, then, run the indexer and the normalizer.

# let's mint some sweet tokens
client_mint = client.contract(contract.address).mint(
    'tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx',
    300,
).send(min_confirmations=2)

# should this really be needed?
blockchain.wait_blocks()

blockchain.provider.index()
indexed = blockchain.provider.transaction_class.objects.get(hash=client_mint.hash())
assert indexed.args, dict(
    _to='tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx',
    value=300,
)

blockchain.provider.normalize()

normalized = contract.fa12tezosmint_set.get(hash=client_mint.hash())
assert normalized.mint_amount == 300

# normalized transaction is a subclass of indexed transaction
assert normalized.tezostransaction_ptr == indexed

Wallets#

Importing a wallet#

# import a bootstrap wallet by secret key
from djwebdapp.models import Account
from pytezos import Key

# Use update_or_create, in case we already have this address!
import binascii
bootstrap, _ = Account.objects.update_or_create(
    blockchain=blockchain,
    address='tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx',
    defaults=dict(
        secret_key=binascii.b2a_base64(Key.from_encoded_key(
            'edsk3gUfUPyBSfrS9CCgmCiQsTCHGkviBDusMxDJstFtojtc1zcpsh'
        ).secret_exponent).decode(),
    ),
)

# balance was automatically fetched
assert bootstrap.balance > 0
old_balance = bootstrap.balance

Creating a wallet#

# create a new wallet on that blockchain, secret key auto generates
new_wallet = Account.objects.create(blockchain=blockchain)
assert new_wallet.balance == 0

Transfering coins#

# Use the transaction model with an amount argument to transfer coins
from djwebdapp.models import Transaction
transaction = Transaction.objects.create(
    name='Provision 1.000.000 coins',
    amount=1_000_000,
    sender=bootstrap,
    receiver=new_wallet,
    blockchain=blockchain,
)

# Deploy the transaction now
transaction.deploy()

# Wait for confirmation blocks
transaction.blockchain.wait()

# Index the transaction
blockchain.provider.index()

# And refresh balance
new_wallet.refresh_balance()

Refreshing balances#

# we can also refresh balances of all accounts with this method
# you would rather have ./manage.py refresh_balances in a cron or something
new_wallet.refresh_balance()
assert new_wallet.balance == 1_000_000
assert new_wallet.provider.get_balance() == new_wallet.balance

API#

Models#

class djwebdapp_tezos.models.TezosCall(id, created_at, updated_at, name, description, blockchain, level, hash, counter, nonce, number, gasprice, gas, last_fail, max_fails, has_code, metadata, normalized, state, error, history, amount, args, address, function, sender, receiver, kind, index, transaction_ptr, contract, micheline, caller)[source]#
exception DoesNotExist#
exception MultipleObjectsReturned#
class djwebdapp_tezos.models.TezosContract(id, created_at, updated_at, name, description, blockchain, level, hash, counter, nonce, number, gasprice, gas, last_fail, max_fails, has_code, metadata, normalized, state, error, history, amount, args, address, function, sender, receiver, kind, index, transaction_ptr, contract, micheline, caller)[source]#
exception DoesNotExist#
exception MultipleObjectsReturned#
get_args()[source]#

Return the arguments of the transaction.

normalizer_class#

alias of Normalizer

save(*args, **kwargs)[source]#

Set micheline if contract_name is set.

class djwebdapp_tezos.models.TezosTransaction(*args, **kwargs)[source]#

Base class for tezos transactions.

micheline#

Smart contract micheline JSON code.

exception DoesNotExist#
exception MultipleObjectsReturned#
save(*args, **kwargs)[source]#

Set has_code if micheline.

Provider#

class djwebdapp_tezos.provider.TezosProvider(blockchain=None, wallet=None)[source]#
deploy(transaction)[source]#

Deploy a given Transaction object.

download(target)[source]#

Import transactions from tzkt for a contract

generate_secret_key()[source]#

Generate a secret key.

Raises NotImplemented in base Provider class.

get_balance(address=None)[source]#

Query the blockchain and return the balance of an address.

address#

Address to get the balance of, use the current client address by default.

get_client(**kwargs)[source]#

Return the Python client that provider encapsulates.

Parameters:

walletAccount object to use, note that it must have a secret_key.

property head#

Return the current block number.

Raises NotImplemented in base Provider class.

index_level(level)[source]#

Index a given block level.

Left to implement in new provider subclasses.

transaction_class#

alias of TezosTransaction

transfer(transaction)[source]#

Execute a transfer transaction.