djWebdApp Tezos#
Indexing contracts#
Example 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, and an original idea by your favorite open source zealot:
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):
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
pymich FA12.py FA12.json
Local tezos blockchain#
Danger
Before you begin, make sure you have followed the setup instructions from Local blockchains.
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'}
Deploy a smart contract#
First, load the smart contract source code:
# load contract source code
import json
source = json.load(open('src/djwebdapp_example/tezos/FA12.json'))
# initial storage in micheline :P
storage = {
'prim': 'Pair',
'args': [
[],
{'int': '0'},
{'string': 'tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx'}
]}
Let’s deploy our smart contract and call the mint()
entrypoint by pasting the
following in our pytezos python shell started above, which you need to start if
you haven’t already to run the following commands:
# originate contract source code with given storage, wait 1 confirmation block
opg = client.origination(
dict(code=source, storage=storage)
).send(min_confirmations=1)
# get originated contract address
from pytezos.operation.result import OperationResult
res = OperationResult.from_operation_group(opg.opg_result)
address = res[0].originated_contracts[0]
# let's mint some sweet tokens
client.contract(address).mint(
'tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx',
1000,
).send(min_confirmations=2)
This should store the deployed contract address in the address variable, copy it or leave the shell open because you need it to index the contract in the next section.
Setting up a blockchain network#
Now that we have deployed a contract, let’s setup djwebdapp
for a local
ethereum 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',
)
# Add our node to the blockchain
blockchain.node_set.get_or_create(endpoint='http://tzlocal:8732')
Indexing a contract#
Now that we have deployed a contract, and setup djwebdapp
for a local tezos
node, let’s index a contract, also programatically in ./manage.py shell
:
# Insert a smart contract with our address
from djwebdapp_tezos.models import TezosTransaction
contract = TezosTransaction.objects.create(
blockchain=blockchain,
address=address,
)
# Transaction kind was setup automatically
assert contract.kind == 'contract'
# Unspecified, micheline was downloaded automatically
assert contract.micheline
# But calls have not yet been synchronized
assert not contract.call_set.count()
# Let's index the blockchain, you could also run ./manage.py index
blockchain.provider.index()
# Refresh our contract model object
contract.refresh_from_db()
# Gas cost was indexed
assert contract.gas
# Mint call was indexed
call = contract.call_set.first()
assert call.function == 'mint'
assert call.args['value'] == 1000
Normalizing incomming data: Models#
We have created example models in the src/djwebdapp_example
directory:
"""
Example models to demonstrate features of djwebdapp.
"""
from django.db import models
class FA12(models.Model):
"""
Model representing an FA12 contract on at least one blockchain.
.. py:attribute:: name
The name of the FA12 token, ie.: "Your New Token"
.. py:attribute:: symbol
Symbol of the FA12 token, ie.: "YNT"
.. note:: You wouldn't need to have relations to contracts on every
blockchain, but we have them here so that we are later able to
demonstrate inter-blockchain mirroring.
"""
tezos_contract = models.OneToOneField(
'djwebdapp_tezos.TezosTransaction',
on_delete=models.CASCADE,
null=True,
blank=True,
)
ethereum_contract = models.OneToOneField(
'djwebdapp_ethereum.EthereumTransaction',
on_delete=models.CASCADE,
null=True,
blank=True,
)
name = models.CharField(
max_length=200,
null=True,
blank=True,
)
symbol = models.CharField(
max_length=10,
null=True,
blank=True,
)
class Mint(models.Model):
"""
Model representing a mint() call on an FA12 contract.
.. py:attribute:: address
Recipient address for the mint.
.. py:attribute:: value
Amount of tokens minted.
.. note:: You wouldn't need to have relations to contract calls on every
blockchain, but we have them here so that we are later able to
demonstrate inter-blockchain mirroring.
"""
fa12 = models.ForeignKey(
'FA12',
on_delete=models.CASCADE,
)
tezos_call = models.OneToOneField(
'djwebdapp_tezos.TezosTransaction',
on_delete=models.CASCADE,
null=True,
blank=True,
)
ethereum_call = models.OneToOneField(
'djwebdapp_ethereum.EthereumTransaction',
on_delete=models.CASCADE,
null=True,
blank=True,
)
address = models.ForeignKey(
'djwebdapp.Account',
on_delete=models.CASCADE,
)
value = models.PositiveIntegerField()
def __str__(self):
return f'mint({self.address.address}, {self.value})'
class Balance(models.Model):
"""
Model representing the balance of an address on an FA12 token.
"""
fa12 = models.ForeignKey(
'FA12',
on_delete=models.CASCADE,
)
address = models.ForeignKey(
'djwebdapp.Account',
on_delete=models.CASCADE,
related_name='fa12_balance_set',
)
balance = models.PositiveIntegerField()
class Meta:
unique_together = (
('fa12', 'address'),
)
def __str__(self):
return f'{self.address} balance: {self.balance}'
# importing other documentation code snippets concerning signals here
from .ethereum.mint_normalize import mint_normalize_ethereum # noqa
from .tezos.mint_normalize import mint_normalize_tezos # noqa
Note
You wouldn’t have to declare ForeignKeys to other Transaction classes than TezosTransactions, but we’ll learn to do inter-blockchain mirroring later in this tutorial, so that’s why we have relations to both.
And declared a function to update the balance of an FA12 contract:
from django.db.models import Q, Sum
def balance_update(fa12, address):
"""
Account balance calculator.
We completely recalculate the balance here so that we are able to keep
correct results even after a blockchain reorg.
Note that the blockchain implementation is completely out of the way here:
we're dealing with normalized models!
"""
# calculate a balance total
total = fa12.mint_set.exclude(
# exclude calls deleted by reorg!!
Q(tezos_call__state='deleted') | Q(ethereum_call__state='deleted')
).filter(
address=address,
).aggregate(
total=Sum('value')
)['total']
# you would have to add burn() and transfer() method support here if you
# had them in your smart contract
# set the balance for an address
address.fa12_balance_set.update_or_create(
fa12=fa12,
defaults=dict(
balance=total,
),
)
Normalizing incomming data: Signals#
Finally, to connect the dots, we are first going to connect a custom callback
to djwebdapp_tezos.models.TezosTransaction
’s post_save
signal to create
normalized Mint
objects for every mint()
call we index:
from django.db.models import signals
from django.dispatch import receiver
from djwebdapp_tezos.models import TezosTransaction
from djwebdapp_example.balance_update import balance_update
from djwebdapp_example.models import FA12
@receiver(signals.post_save, sender=TezosTransaction)
def mint_normalize_tezos(sender, instance, **kwargs):
if instance.function != 'mint':
# not a mint call? bail out!
return
try:
fa12 = instance.contract.fa12
except TezosTransaction.fa12.RelatedObjectDoesNotExist:
# no FA12 normalized object for this contract? bail out!
return
# figure out the beneficiary Account based on the mint call arg _to
beneficiary = instance.blockchain.account_set.get(
address=instance.args['_to'],
)
# create or update the normalized Mint object for this call
fa12.mint_set.update_or_create(
tezos_call=instance,
defaults=dict(
address=beneficiary,
value=instance.args['value'],
)
)
# we're fully recalculating the balance here in case of a blockchain reorg
# to ensure the balance is always current
balance_update(fa12, beneficiary)
@receiver(signals.post_save, sender=FA12)
def fa12_create_tezos(sender, instance, created, **kwargs):
"""
Trigger post_save on every call that the contract already has, if any.
"""
if not created:
# we're setup already
return
if not instance.tezos_contract:
# not a tezos contract
return
for call in instance.tezos_contract.call_set.all():
call.save()
We are now ready to normalize the smart contract we have indexed:
# Create a normalized FA12 model for this Tezos contract
from djwebdapp_example.models import FA12
fa12 = FA12.objects.create(tezos_contract=contract)
# reverse relation works as usual
assert contract.fa12
# mint calls were normalized
assert contract.fa12.mint_set.count() == 1
# balance was calculated
from djwebdapp_example.models import Balance
assert Balance.objects.first().balance == 1000
Vault#
Setup#
Make sure you have installed djwebdapp with the [vault]
dependencies (or
[all]
).
Note
You may rotate Fernet keys used for encryption, please refer to djfernet documentation.
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 coins',
amount=1_000,
sender=bootstrap,
receiver=new_wallet,
blockchain=blockchain,
)
# Deploy the transaction now
transaction.deploy()
# Wait one block level
transaction.blockchain.wait(transaction.level + 1)
blockchain.provider.index()
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
assert new_wallet.provider.get_balance() == new_wallet.balance
Deploy a smart contract#
from djwebdapp_tezos.models import TezosTransaction
# Create a smart contract to deploy
contract = TezosTransaction.objects.create(
sender=bootstrap,
state='deploy',
max_fails=2, # try twice before aborting, to speed up tests!
micheline=source,
args=storage,
)
# Create a call that should deploy afterwards on that contract
mint = TezosTransaction.objects.create(
sender=bootstrap,
state='deploy',
max_fails=2,
contract=contract,
function='mint',
args=(
new_wallet.address,
1000,
),
)
# Spool will first deploy the contract
assert blockchain.provider.spool() == contract
# Get deployment level
contract.refresh_from_db()
# But won't deploy the mint call on the same block
assert not blockchain.provider.spool()
# Waiting for next block ...
blockchain.wait(contract.level + 1)
# Index to fetch contract address
blockchain.provider.index()
# Now spool will deploy the mint call!
assert blockchain.provider.spool() == mint
mint.refresh_from_db()
blockchain.wait(mint.level + 1)
blockchain.provider.index()