djWebdApp Ethereum#

Danger

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

Setup#

Smart contract#

In this tutorial, 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/contracts
solc --abi --overwrite --output-dir . --bin FA12.sol

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

Web3 client#

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

Blockchain#

Now that we’re ready to stimulate the blockchain, 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',
    # on geth local blockchain, blocks are mined at every transaction, so we
    # don't need to wait for confirmation blocks
    min_confirmations=0,
)

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

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 eths from the default client wallet
client.eth.send_transaction(dict(
    to=admin.address,
    value=client.to_wei(4_000_000, 'ether'),
))

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

FA12Ethereum

Subclass of EthereumTransaction and defines k:py:attr:~djwebdapp_ethereum.models.EthereumContract.contract_file_name as well as normalizer_class, which we’ll define in the next chapter

FA12MintEthereum

Subclass of EthereumCall 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_ethereum.models import EthereumCall, EthereumContract


class FA12Ethereum(EthereumContract):
    contract_name = 'FA12'
    normalizer_class = 'FA12EthereumNormalizer'
    token_name = models.CharField(max_length=200)
    token_symbol = models.CharField(max_length=10)

    def get_args(self):
        return (self.token_name, self.token_symbol)


class FA12EthereumMint(EthereumCall):
    entrypoint = 'mint'
    target_contract = models.ForeignKey(
        'FA12Ethereum',
        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 (self.mint_account.address, self.mint_amount)


class FA12EthereumBalance(models.Model):
    fa12 = models.ForeignKey(
        'FA12Ethereum',
        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_ethereum.models import FA12Ethereum, FA12EthereumMint

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

# make a couple of mint calls
first_mint = FA12EthereumMint.objects.create(
    target_contract=contract,
    sender=admin,
    mint_account=admin,
    mint_amount=100,
)
second_mint = FA12EthereumMint.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():
    time.sleep(.1)

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_ethereum.models import FA12Ethereum, FA12EthereumMint, FA12EthereumBalance


class FA12EthereumNormalizer(Normalizer):
    def mint(self, call, contract):
        account, _ = Account.objects.get_or_create(
            address=call.args['account'],
        )
        call, _ = FA12EthereumMint.objects.update_or_create(
            ethereumtransaction_ptr_id=call.id,
            defaults=dict(
                target_contract=contract,
                mint_account=account,
                mint_amount=call.args['amount'],
            )
        )
        balance, _ = FA12EthereumBalance.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.

Contract = client.eth.contract(
    abi=contract.abi,
    address=contract.address,
)

tx = Contract.functions.mint(client.eth.default_account, 300).transact()
receipt = client.eth.wait_for_transaction_receipt(tx)
txhash = receipt['transactionHash'].hex()

blockchain.provider.index()
indexed = blockchain.provider.transaction_class.objects.get(hash=txhash)
assert indexed.args, 2

blockchain.provider.normalize()

normalized = contract.fa12ethereummint_set.get(hash=txhash)
assert normalized.mint_amount == 300

Wallets#

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.to_checksum_address(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.to_wei(4_000_000, '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 == 4_000_000
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()

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_ethereum.models.EthereumCall(*args, **kwargs)[source]#

Base model class for Ethereum contract function calls.

exception DoesNotExist#
exception MultipleObjectsReturned#
class djwebdapp_ethereum.models.EthereumContract(*args, **kwargs)[source]#

Base model class for Ethereum Contracts.

contract_name#

Name of the contract files, they are expected to be found in the contracts sub-directory of the Django App that holds the model that is inheriting from this class (your app)

exception DoesNotExist#
exception MultipleObjectsReturned#
normalizer_class#

alias of Normalizer

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

Sets abi and bytecode if contract_name is set.

class djwebdapp_ethereum.models.EthereumTransaction(*args, **kwargs)[source]#

Base model for Ethereum transactions.

abi#

Smart contract ABI code.

bytecode#

Smart contract bytecode.

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

Sets has_code if bytecode is set.

Provider#

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

Deploy a given Transaction object.

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.

index_level(level)[source]#

Index a given block level.

Left to implement in new provider subclasses.

transaction_class#

alias of EthereumTransaction