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 asnormalizer_class
, which we’ll define in the next chapterFA12MintEthereum
Subclass of
EthereumCall
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_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
andbytecode
ifcontract_name
is set.
Provider#
- class djwebdapp_ethereum.provider.EthereumProvider(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.
- index_level(level)[source]#
Index a given block level.
Left to implement in new provider subclasses.
- transaction_class#
alias of
EthereumTransaction