djWebdApp Ethereum#

Indexing contracts#

Example contract#

We will need to instanciate a contract on this blockchain. We’ll use a simple example smart contract in solidity that looks like some FA12:

pragma solidity ^0.8.0;


contract FA12 {
    mapping(address => uint256) private _balances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function mint(address account, uint256 amount) public virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _totalSupply += amount;
        _balances[account] += amount;
    }
}

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

cd src/djwebdapp_example/ethereum
solc --abi --overwrite --output-dir . --bin FA12.sol

Example contract deployment#

Danger

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

With the Ethereum 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:

pip install web3
./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, make sure your default account is provisionned properly:

from web3 import Web3

# use local blockchain with default account
client = Web3(Web3.HTTPProvider('http://ethlocal:8545'))
client.eth.default_account = client.eth.accounts[0]

# enable support for geth --dev sandbox
from web3.middleware import geth_poa_middleware
client.middleware_onion.inject(geth_poa_middleware, layer=0)

Check your client balance:

>>> client.eth.default_account
'0xD1562e5128FC95311E46129a9f445402278e7751'
>>> client.eth.get_balance(w3.eth.default_account)
115792089237316195423570985008687907853269984665640564039457577993160770347781

Deploy a smart contract#

First, load the smart contract source code:

bytecode = open('src/djwebdapp_example/ethereum/FA12.bin', 'r').read()
abi = open('src/djwebdapp_example/ethereum/FA12.abi', 'r').read()

Let’s deploy our smart contract and call the mint() entrypoint by pasting the following in our python shell started above, which you need to start if you haven’t already to run the following commands:

# actually deploy the contract:
contract = client.eth.contract(abi=abi, bytecode=bytecode)
contract_hash = contract.constructor('Your New Token', 'YNT').transact()
receipt = client.eth.wait_for_transaction_receipt(contract_hash)
address = receipt.contractAddress

# let's mint some sweet YNTs
contract = client.eth.contract(abi=abi, address=address)
hash = contract.functions.mint(client.eth.default_account, 1000).transact()
receipt = client.eth.wait_for_transaction_receipt(hash)

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='Ethereum Local',
    provider_class='djwebdapp_ethereum.provider.EthereumProvider',
)

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

Indexing a contract#

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

# Then, insert a smart contract with our address
from djwebdapp_ethereum.models import EthereumTransaction
contract = EthereumTransaction.objects.create(
    blockchain=blockchain,
    # used to index method calls
    address=address,
    # used to translate function calls
    abi=abi,
    # used to fill the contract metadata
    hash=contract_hash.hex(),
)
assert contract.kind == 'contract'

# 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['amount'] == 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 EthereumTransactions, 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_ethereum.models.EthereumTransaction’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_ethereum.models import EthereumTransaction
from djwebdapp_example.balance_update import balance_update
from djwebdapp_example.models import FA12


@receiver(signals.post_save, sender=EthereumTransaction)
def mint_normalize_ethereum(sender, instance, **kwargs):
    if instance.function != 'mint':
        # not a mint call? bail out!
        return

    try:
        fa12 = instance.contract.fa12
    except EthereumTransaction.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['account'],
    )

    # create or update the normalized Mint object for this call
    fa12.mint_set.update_or_create(
        ethereum_call=instance,
        defaults=dict(
            address=beneficiary,
            value=instance.args['amount'],
        )
    )

    # 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_ethereum(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.ethereum_contract:
        # not an ethereum contract
        return

    for call in instance.ethereum_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 Ethereum contract
from djwebdapp_example.models import FA12
fa12 = FA12.objects.create(ethereum_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#

# create a keyfile, we could have created it with geth account new too
new_account = client.eth.account.create()
keyfile = new_account.encrypt('')

# decode private key and get address
address = Web3.toChecksumAddress(keyfile['address'])
private_key = blockchain.provider.client.eth.account.decrypt(keyfile, '')

# send some ether from the seed account
client.eth.send_transaction(dict(
    to=address,
    value=client.toWei(1337, 'ether'),
))

# wait until the blockchain validates the transfer
import time
while not client.eth.get_balance(address):
    time.sleep(.1)

# import the freshly created wallet by secret key
import binascii
from djwebdapp.models import Account
bootstrap = Account.objects.create(
    secret_key=binascii.b2a_base64(private_key).decode(),
    address=address,
    blockchain=blockchain,
)

# balance was automatically fetched
assert bootstrap.balance == 1337
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()

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 contract#

from djwebdapp_ethereum.models import EthereumTransaction

# Create a smart contract to deploy
contract = EthereumTransaction.objects.create(
    sender=bootstrap,
    state='deploy',
    max_fails=2,  # try twice before aborting, to speed up tests!
    bytecode=bytecode,
    abi=abi,
    args=['Your New Token', 'YNT'],
)

# Create a call that should deploy afterwards on that contract
mint = EthereumTransaction.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()

# Geth --dev incremented the block level transactionnaly,
# but you'd need that to wait a block on a real network as such
blockchain.wait(contract.level + 1)

# Now spool will deploy the mint call!
assert blockchain.provider.spool() == mint

# And has nothing else to do
assert not blockchain.provider.spool()