Part 7: Interacting With a Contract Instance¶
Python Objects for Contract Instances¶
A contract instance is a bytecode on the blockchain at a specific address. Populus and Web3 give you a Python object with Python methods, which correspond to this bytecode. You interact with this local Python object, but behind the scenes these Python interactions are sent to a the bytecode on the blockchain.
To find and interact with a contract on the blockchain, this local contract object needs an address and the ABI (application binary interface).
Reminder: the contract instance is compiled, a bytecode, so the EVM (ethereum virtual machine) needs the ABI in order to call this bytecode. The ABI (application binary interface) is essentially a JSON file with detailed description of the functions and their arguments, which tells how to call them. It’s part of the compiler output.
Populus does not ask you for the address and the ABI of the projects’ contracts: it already
has the address in the registrar file at registrar.json
,
and the ABI in build/contracts.json
However, if you want to interact with contract instances from other projects, or even deployed by others, you will need to create a Web3 contract yourself and manually provide the address and the ABI. See Web3 Contracts
Warning
When you call a contract that you didn’t compile and didn’t deploy yourself, you should be 100% sure that it’s trusted, that the author is trusted, and only only after you got a version of the Solidity contract’s source, and verified that the compilation of this source is identical to the bytecode on the blockchain.
Call an Instance Function¶
A call
is a contract instance invocation that doesn’t change
state. Since the state is not changed, there is no need to create a transaction, to mine the transaction into a block,
and to propagate it and sync to the entire blockchain. The call
runs only on the one node you are connected to,
and the node reverts everything when the call
is finished - and saves you the expensive gas.
Calls are useful to query an existing contract state, without any changes, when a local synced node can just hand you this info. It’s also useful as a “dry-run” for transactions: you run a ‘’call’‘, make sure everything is working, then send the real transaction.
To access a conract function with call
, in the same way you have done with the tests,
use contract_obj.call().foo(arg1,arg2...)
where foo
is the contract function. Then call()
returns an object that exposed the contract instance functions
in Python.
To see an example, edit the script:
$ nano scripts/donator.py
And add a few lines, as follows:
from populus.project import Project
p = Project(project_dir="/home/mary/projects/donations/")
with p.get_chain('horton') as chain:
donator, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator')
print("Donator address on horton is {address}".format(address=donator.address))
if deploy_tx_hash is None:
print("The contract is already deployed on the chain")
else:
print("Deploy Transaction {tx}".format(tx=deploy_tx_hash))
# Get contract state with calls
donationsCount = donator.call().donationsCount()
donationsTotal = donator.call().donationsTotal()
# Client side
ONE_ETH_IN_WEI = 10**18 # 1 ETH == 1,000,000,000,000,000,000 Wei
total_ether = donationsTotal/ONE_ETH_IN_WEI
avg_donation = donationsTotal/donationsCount if donationsCount > 0 else 0
status_msg = (
"Total of {:,.2f} Ether accepted in {:,} donations, "
"an avergage of {:,.2f} Wei per donation."
)
print (status_msg.format(total_ether, donationsCount, avg_donation))
Pretty much similar to what we did so far: The script starts with the Project
object,
the main entry point to the Populus API. The project object provides a chain
object (as long as
this chain is defined in the project-scope or user-scope configs),
and once you have the chain
you can get the contract instance on that chain.
Then we get the donationsCount
and the donationsTotal
with call
. Populus, via Web3, calls
the running geth node, and geth grabs and return these two state variables
from the contract’s storage. Even if we had used geth as a node to mainnet
, a sync node can get this info
localy.
These are the same public variables that you declared in the Donator
Solidity source:
contract Donator {
uint public donationsTotal;
uint public donationsUsd;
uint public donationsCount;
uint public defaultUsdRate;
...
}
Finally, we can do some client side processing.
Run the script:
$ python scripts/donator.py
Donator address on horton is 0xb8d9d2afbe18fd6ac43042164ece9691eb9288ed
The contract is already deployed on the chain
Total of 0.00 Ether accepted in 0 donations, an avergage of 0.00 Wei per donation.
Note that we don’t need an expensive state variable for “average”, in the contract, nor a function to calculate average. The contract just keeps only what can’t be done elsewhere, to save gas. Moreover, code on deployed contracts can’t be changed, so offloading code to the client gives you a lot of flexibility (and, again, gas, if you need a fix and re-deploy).
Send a Transaction to an Instance Function¶
To change the state of the instance, ether balance and the state variables, you need to send a transaction.
Once the transaction is picked by a miner, included in a block and accepted by the blockchain, every node on the blockchain will run and update the state of your contract. This process obviously costs real money, the gas.
With Populus and Web3 you send transactions with the transact
function. For every contract instance object,
transact()
exposes the contract’s instance functions. Behind the scenes, Populus takes your Pythonic call and,
via Web3, convert it to the transactions’ data
payload, then sends the transaction to geth.
When geth get the transaction, it sends it to the blockchain. Populus will return the transaction hash.
and you will have to wait until it’s mined and accepted in a block. Typically 1-2 seconds with a local chain,
but will take more time on testnet
and mainnet
(you will watch new blocks with filters
and events
,later on that).
We will add a transaction to the script:
$ nano scripts/donator.py
Update the script:
import random
from populus.project import Project
p = Project(project_dir="/home/mary/projects/donations/")
with p.get_chain('horton') as chain:
donator, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator')
print("Donator address on horton is {address}".format(address=donator.address))
if deploy_tx_hash is None:
print("The contract is already deployed on the chain")
else:
print("Deploy Transaction {tx}".format(tx=deploy_tx_hash))
# Get contract state with calls
donationsCount = donator.call().donationsCount()
donationsTotal = donator.call().donationsTotal()
# Client side
ONE_ETH_IN_WEI = 10**18 # 1 ETH == 1,000,000,000,000,000,000 Wei
total_ether = donationsTotal/ONE_ETH_IN_WEI
avg_donation = donationsTotal/donationsCount if donationsCount > 0 else 0
status_msg = (
"Total of {:,.2f} Ether accepted in {:,} donations, "
"an avergage of {:,.2f} Wei per donation."
)
print (status_msg.format(total_ether, donationsCount, avg_donation))
# Donate
donation = ONE_ETH_IN_WEI * random.randint(1,10)
effective_eth_usd_rate = 5
transaction = {'value':donation, 'from':chain.web3.eth.coinbase}
tx_hash = donator.transact(transaction).donate(effective_eth_usd_rate)
print ("Thank you for the donation! Tx hash {tx}".format(tx=tx_hash))
The transaction is a simple Python dictionary:
transaction = {'value':donation, 'from':chain.web3.eth.coinbase}
The value
is obviously the amount you send in Wei, and the from
is the account that sends the transaction.
Note
You can include any of the ethereum allowed items in a transaction except data
which is
created auto by converting the Python call to an EVM call. Web3 also set ‘gas’ and ‘gasPrice’ for you
based on estimates if you didn’t provide any. The ‘to’ field, the instance address, is already known to Populus
for project-deployed contracts. See transaction parameters
Coinbase Account
Until now you didn’t provide any account, because in the tests the tester
chain magically creates and unlocks
ad-hoc accounts. With a persistent chain you have to explictly provide the account.
Luckily, when Populus created the local horton
chain it also created a default wallet file, a password file that unlocks the wallet,
and included the --unlock
and --password
arguments for geth in the run script, run_chain.sh
. When you run
horton
with chains/horton/./run_chain.sh
the account is already unlocked.
All you have to do is to say that you want this account as the transaction account:
'from':chain.web3.eth.coinbase
The coinbase
(also called etherbase
) is the default account that geth will use. You can have as many accounts
as you want, and set one of them as a coinbase. If you didn’t add an account for horton
, then the chain has only
one account, the one that Populus created, and it’s automatically assigned as the coinbase.
Note
The wallet files are saved in the chain’s keystore
directory. For more see the tutorial on Wallets and
Accounts. For a more in-depth discussion see geth accounts managment
Finally, the script sends the transaction with transact
:
tx_hash = donator.transact(transaction).donate(effective_eth_usd_rate)
Ok. Run the script, after you make sure that horton
is running:
$ python scripts/donator.py
Donator address on horton is 0xb8d9d2afbe18fd6ac43042164ece9691eb9288ed
The contract is already deployed on the chain
Total of 0.00 Ether accepted in 0 donations, an avergage of 0.00 Ether per donation.
Thank you for the donation! Tx hash 0xbe9d182a508ec3a7efc3ada8cfb134647b39feec4a7eb018ef91cc38e216ddbc
Worked. The transaction was sent, yet we still don’t see it. Run again:
$ python scripts/donator.py
Donator address on horton is 0xb8d9d2afbe18fd6ac43042164ece9691eb9288ed
The contract is already deployed on the chain
Total of 3.00 Ether accepted in 1 donations, an avergage of 3,000,000,000,000,000,000.00 Wei per donation.
Thank you for the donation! Tx hash 0xf6d40adfedf1882e7543c4ef96803bd790127afdc67e40a4c7d91d29884ad182
First donation accepted! Run again:
$ python scripts/donator.py
Donator address on horton is 0xb8d9d2afbe18fd6ac43042164ece9691eb9288ed
The contract is already deployed on the chain
Total of 4.00 Ether accepted in 2 donations, an avergage of 2,000,000,000,000,000,000.00 Wei per donation.
Thank you for the donation! Tx hash 0x21bd87b9db76b54a48c5a12a4bf7930a0e45480f5af5d0745cb2e8b4a438c5af
And they just keep coming.
If you looked at your geth chain terminal windown, you could see how geth picks the transaction and mine it:
INFO [10-20|01:48:32] 🔨 mined potential block number=3918 hash=d36ecd…e724c1
INFO [10-20|01:48:32] Commit new mining work number=3919 txs=0 uncles=0 elapsed=1.084ms
INFO [10-20|01:48:40] Submitted transaction fullhash=0xbe9d182a508ec3a7efc3ada8cfb134647b39feec4a7eb018ef91cc38e216ddbc recipient=0xb8d9d2afbe18fd6ac43042164ece9691eb9288ed
INFO [10-20|01:49:05] Successfully sealed new block number=3919 hash=4e36eb…01e41f
INFO [10-20|01:49:05] 🔨 mined potential block number=3919 hash=4e36eb…01e41f
INFO [10-20|01:49:05] Commit new mining work number=3920 txs=1 uncles=0 elapsed=735.282µs
INFO [10-20|01:49:21] Successfully sealed new block
Check the persistancy of the instance again. Stop the horton
chain, press Ctrl+C in it’s terminal window,
and then re-run it with chains/horton/./run_chain.sh
.
Run the script again:
$ python scripts/donator.py
Donator address on horton is 0xb8d9d2afbe18fd6ac43042164ece9691eb9288ed
The contract is already deployed on the chain
Total of 7.00 Ether accepted in 3 donations, an avergage of 2,333,333,333,333,333,504.00 Wei per donation.
Thank you for the donation! Tx hash 0x8a595949271f17a2a57a8b2f37f409fb1ee809c209bcbcf513706afdee922323
Oh, it’s so easy to donate when a genesis block allocates you billion something.
The contract instance is persistent, and the state is saved. With horton
, a local chain, it’s saved to your hard-drive.
On mainent
and testnet
, to the entire blockchain nodes network.
Note
You may have noticed that we didn’t call the fallback
function. Currently there is no builtin way to call
the fallback
from Populus. You can simply send a transaction to the contract instance’s address,
without any explicit function call. On transaction w/o a function call the EVM will call the fallback
.
Even better, write another named function that you can call and test
from Populus, and let the fallback
do one thing - call this function.
Programatically Access to a Contract Instance¶
The script is very simple, but it gives a glimpse how to use Populus as bridge between your Python application
and the Ethereum Blockchain. As an excercise, update the script so it prompts for donation amount, or work with
the Donator
instance on the morty local chain.
This is another point that you’ll appreciate Populus: not only it helps to manage, develop and test blockchain assets (Solidity sources, compiled data, deployments etc), but it also exposes your blockchain assets as Python objects that you can later use natively in any of your Python projects. For more see #TODO Populus API.
Interim Summary¶
- You interacted with an Ethereum persistent contract instance on a local chain
- You used
call
to invoke the instance (no state change) - You sent transactions to the instance (state changed)
- You used the
Project
object as an entry point to Populus’ API for a simple Python script - And, boy, you just donated a very generous amount of Wei.