Creating a Python Ethereum Interface: Part 1

Written by pryce.turner | Published 2018/12/11
Tech Story Tags: ethereum | python | python-ethereum | python-ethereum-interface | ethereum-interface

TLDRvia the TL;DR App

Coming from a Python background, I was initially a bit discouraged to discover a lack of tutorials for interacting with the Ethereum blockchain using Python. However, after reading Nick Williams (excellent) guide and taking a good look at the rich feature set of Ethereum’s own web3.py, I was determined to write something of my own. I wanted a clean way to compile, deploy and interact with a contract from Python. I ended up writing a convenience interface that collects and abstracts a number of web3.py methods so it can be easily imported into different Python modules.

Let me preface this by saying a working knowledge of Python, Solidity, and the Ethereum blockchain are recommended. I also recommend you get your feet wet reading Nick’s aforementioned guide and having a poke around web3.py. Another great resource is this overview of the Python / Ethereum ecosystem summarizing the amazing work by Piper Merriam, Jason Carver and others into this space.

Before we get stuck in, there are a few requirements you’ll need to set up on your system first:

Please follow the links above for installation instructions. Also note that py-solc-x is a fork of the official repo updated for Solc v0.5.x. As with any software you put on your machine, don’t just take my word for it’s safety and efficacy! I encourage you to do your own due diligence as I don’t maintain the above packages.

I’d also like to preface this by saying I wrote everything on Ubuntu, if you run into issues on Windows or OS X I will do everything I can to help. H_owever,_ I do also have a lovely provisioning script if you’d like to spin up a fresh Ubuntu VM in Vagrant or WSL.

Setup

Let’s get stuck in. Go ahead and open up a terminal in a fresh directory and git clone the project.

The contract I’m using is the ubiquitous ‘Greeter’ contract with an inherited access control contract. However, the interface was designed to be contract agnostic, so feel free to follow along with your own project if you’re working on one. We’re going to use the python-wrapped solc compiler for compiling our contract, and web3.py for interacting with the blockchain. I developed this using ganache-cli — it should work in theory with any client, but I haven’t tested them out.

In a separate tab or terminal window, fire up ganache using theganache-cli command. You should see an output beginning similarly to what’s below:

Ganache CLI v6.1.8 (ganache-core: 2.2.1)

Available Accounts==================(0) 0xf52cef744ccdd52e66856057d820d2b6677af63c (~100 ETH)(1) 0x6be3a04dcce9d82bf36b71b20831553e6d9e154e (~100 ETH)(2) 0xd9b6e69094f7ed37eaa724b9c0141a269513fdcf (~100 ETH)(3) 0xc9ec9a79be055bbaf88dd2ebefd4d0d6230a30de (~100 ETH)...

Now, open up walkthrough.py and set up your imports at the top of the file with:

import osfrom web3 import Web3, HTTPProviderfrom interface import ContractInterface

This imports a few methods from the web3.py library along with the ContractInterface class from interface.py. Next, we’ll want to create our web3 instance using:

w3 = Web3(HTTPProvider('http://127.0.0.1:8545'))

Those are ganache’s default host and port. You can now interact with your node through python. Feel free to run the above commands in a python interperter, like:

>>> from web3 import Web3, HTTPProvider>>> w3 = Web3(HTTPProvider('http://127.0.0.1:8545'))>>> w3.eth.accounts[0] #should return:>>> 0xf52cef744ccdd52e66856057d820d2b6677af63c #account 0 from above

Nifty.

On the next line set up the path variable for where your contracts are stored, if you’re just using the included contracts that will look like:

contract_dir = os.path.abspath('./contracts/')

And finally, you’ll want to create an interface instance with:

greeter_interface = ContractInterface(w3, 'Greeter', contract_dir)

‘Greeter’ here is the name of the contract you wish to create an interface for. Since Greeter.sol inherits from Owned.sol, you’ll have access to all the methods within Owned from the Greeter interface.

Let’s run walkthrough.py briefly with the following to make sure we aren’t getting any errors on initialization:

python3 -i walkthrough.py>>> type(greeter_interface)

Should return <class 'interface.ContractInterface'>

Also, if you need a quick reference for any of the interface methods, you can run >>> help(ContractInterface) for docstring output.

Compile

Solc is (rightly so) pretty strict about compiling dependencies that aren’t passed explicitly. Everything was compiling fine when using Truffle, but Solc took some tinkering. If you’re importing contracts, make sure you use this specific format import "./contract.sol"; below your pragma. The compiler should take care of the rest. Go ahead and add the following to walkthrough.py:

greeter_interface.compile_source_files()

At this point, walkthrough should look like

# Put your imports hereimport osfrom interface import ContractInterfacefrom web3 import HTTPProvider, Web3

# Initialize your web3 objectw3 = Web3(HTTPProvider('http://127.0.0.1:8545'))

# Create a path object to your Solidity source filescontract_dir = os.path.abspath('./contracts/')

# Initialize your interfacegreeter_interface = ContractInterface(w3, 'Greeter', contract_dir)

# Compile contracts belowgreeter_interface.compile_source_files()

Let’s look at the compile_source_files() method in more detail:

deployment_list = []

for contract in os.listdir(self.contract_directory):deployment_list.append(os.path.join(self.contract_directory, contract))

self.all_compiled_contracts = compile_files(deployment_list)

print('Compiled contract keys:\n{}'.format('\n'.join(self.all_compiled_contracts.keys())))

This method just created a list of absolute paths to be passed to py-solc’s compile_files. The compiler output is then saved to an instance attribute for later use. If you now run walkthrough you should see the compiled contract keys print out.

Deploy

Let’s add the deployment method to walkthrough with the line:

greeter_interface.deploy_contract()

This method first checks that the contracts are compiled and re-compiles them if not with the following:

try:self.all_compiled_contracts is not Noneexcept AttributeError:print("Source files not compiled, compiling now and trying again...")self.compile_source_files()

Next, it will find the name of the contract we specified to deploy earlier within one of the compiler output keys. It then creates a deployment instance using that contract’s application binary interface (ABI) and bytecode (BIN):

for compiled_contract_key in self.all_compiled_contracts.keys():if self.contract_to_deploy in compiled_contract_key:deployment_compiled = self.all_compiled_contracts[compiled_contract_key]

deployment = self.web3.eth.contract(abi=deployment_compiled['abi'],bytecode=deployment_compiled['bin'])

This is our first time seeing the web3.py library in action. The web3.eth.contract is a contract class ready to be deployed on the blockchain. Next we’ll estimate the gas usage for deployment and deploy it if it’s below what was set during initialization.

deployment_estimate = deployment.constructor().estimateGas(transaction=deployment_params)

if deployment_estimate < self.max_deploy_gas:tx_hash = deployment.constructor().transact(transaction=deployment_params)

A few things are happening here. The constructor() method builds the deployment transaction given [deployment_params](http://deployment_estimate%20=%20deployment.constructor%28%29.estimateGas%28transaction=deployment_params%29%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20deployment_estimate%20%3C%20self.max_deploy_gas:%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tx_hash%20=%20deployment.constructor%28%29.transact%28transaction=deployment_params%29). This is a dictionary with a number of defaults that can be overloaded if desired. The max_deploy_gas is set as a default during __init__ and is really just a safety feature for unexpected deployment gas usage. If that passes, the contract is deployed using constructor().transact and the transaction hash (tx_hash) is returned.

tx_receipt = self.web3.eth.waitForTransactionReceipt(tx_hash)contract_address = tx_receipt['contractAddress']

These two lines wait for the transaction to be mined, return the receipt, and pull the contract’s address from it. We’re going to write that to file with the following:

vars = {'contract_address' : contract_address,'contract_abi' : deployment_compiled['abi']}

with open (self.deployment_vars_path, 'w') as write_file:json.dump(vars, write_file, indent=4)

This collects the deployment address and the contract’s ABI and saves them to a JSON file at the path specified in deployment_vars_path. We’ll see why in the next section.

Now if you run walkthrough you should see the output from compiling the contracts, some contract deployment output and a line telling you where those variables were saved.

Getting an Instance

Once we deploy a contract to the blockchain, it will persist there indefinitely (in theory at least). We don’t want to re-compile and re-deploy every time we want to interact with it. This is why we saved the contract’s address and ABI in the JSON file in the last section. Add the following to walkthrough:

greeter_interface.get_instance()

The first part of this method will open up the JSON, check that it has an address, and then check that there’s something deployed at that address with:

with open (self.deployment_vars_path, 'r') as read_file:vars = json.load(read_file)

try:self.contract_address = vars['contract_address']except ValueError("No address found in {}, please call 'deploy_contract' andtry again.".format(self.deployment_vars_path)):raise

contract_bytecode_length = len(self.web3.eth.getCode(self.contract_address).hex())

try:assert (contract_bytecode_length > 4), "Contract not deployedat {}.".format(self.contract_address)except AssertionError as e:print(e)raiseelse:print('Contract deployed at {}. This function returnsan instance object.'.format(self.contract_address))

Once those checks are done, we’ll build the contract instance again, using the address this time and return that instance.

self.contract_instance = self.web3.eth.contract(abi = vars['contract_abi'],address = vars['contract_address'])

return self.contract_instance

This deployed instance exposes your contract’s properties and methods as outlined in the documentation. For example,

>>>instance = greeter_interface.get_instance()>>>instance.functions.greet().call()>>>b'Hello'\x00\x00\x00\x00...

The greeting shows up like this because it’s set as the type bytes32 in Greeter.sol. We’ll explore why I went with bytes, along with dealing with events, and cleaning outputs in part 2 of this tutorial! However, for a lot of people, this contract instance will be enough to suit their needs.

Wrap Up

I wrote this little interface because I wanted a clean way to compile, deploy, and interact with smart contracts from Python. It’s been a big learning experience for me and the result of a lot of tinkering — I hope it’s helpful to some of you out there. However, it is fairly opinionated and certainly not as robust as I’d like it to be. It’s very much a work in progress and I’m open to criticisms and improvements. Also, again, if you need any help along the way, leave a comment and I’ll do my best!


Published by HackerNoon on 2018/12/11