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 definescontract_name
as well asnormalizer_class
, which we’ll define in the next chapterFA12MintTezos
Subclass of
TezosCall
and definesentrypoint
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#
- normalizer_class#
alias of
Normalizer
- save(*args, **kwargs)[source]#
Set
micheline
ifcontract_name
is set.
Provider#
- class djwebdapp_tezos.provider.TezosProvider(blockchain=None, wallet=None)[source]#
- deploy(transaction)[source]#
Deploy a given
Transaction
object.
- 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:
wallet –
Account
object to use, note that it must have asecret_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