paint-brush
Ethereum Smart Contracts in Python: a comprehensive(ish) guideby@enjeyw
38,753 reads
38,753 reads

Ethereum Smart Contracts in Python: a comprehensive(ish) guide

by Nick WilliamsApril 6th, 2018
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

It’s one thing to get a basic smart contract up on Ethereum — just google “ERC20 Token Tutorial” you’ll find plenty of information on how to do it. Interacting with a contract programmatically is another thing entirely, and if you’re a Python coder, then tutorials are scarce.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Ethereum Smart Contracts in Python: a comprehensive(ish) guide
Nick Williams HackerNoon profile picture

It’s one thing to get a basic smart contract up on Ethereum — just google “ERC20 Token Tutorial” you’ll find plenty of information on how to do it. Interacting with a contract programmatically is another thing entirely, and if you’re a Python coder, then tutorials are scarce.

One, by my count, and it’s soooo 2017.

Fortunately for us, Version 4 of Web3.py has just been released, which means it’s now easier than ever to run a python script and watch as magical things happen on the blockchain. Spooky.

A big shout out Piper Merriam, Jason Carver and all the others who’ve worked so on hard on Web3.py to make life easy for the rest of us — at Sempo we’re using Ethereum to make Disaster Response more transparent, and it’s only really possible thanks to Web3.py.

Getting Set Up

First we get set up, making sure we have the relevant python libraries installed and so-forth.

Python libraries everywhere, but what are they for?

There are plenty of python libraries related to Ethereum out there, but there are two that come up a lot when people talk about Ethereum: Web3.py and Pyethereum. At first glance it’s not obvious which one you should use for what.

Pyethereum

A Python implementation of the Ethereum Virtual Machine (EVM). The EVM, in turn is the part of the Ethereum protocol that actually runs the code in Smart Contracts and determines their outputs. So if you wanted to run an Ethereum node in Python, Pyethereum is a good place to start.

Even if you’re perfectly happy running your Smart Contracts without running your own node, Pyethereum is still a good library to have around— it contains a bunch of functions that do useful things like calculate a user’s address from their private key and so on.

Web3.py

A library for actually interacting with the Ethereum Blockchain. We’re talking about things like transferring Ether between accounts, publishing Smart Contracts and triggering functions attached existing Smart Contracts. It’s inspired by the popular javascript library Web3.js, and it’ll be the main library we use in this tutorial.

Ok, less talk, more do!

At first I tried working with a version Python 3.5, but at runtime I ran into issues, apparently caused by Python’s type hinting. Creating a Virtual Environment based off Python 3.6 solved this, so I’d recommend doing the same.

Go ahead and pip-install web3 (make sure you get version 4) .

Unless you enjoy spending real money for the sake of it, you’ll need a wallet on an Ethereum TestNet such as Ropsten with plenty of Ether to play with. An easy way to do this is download the Metamask extension for Chrome, and create a new account from there.

Make sure you also select the ‘Ropsten Test Net’ on the left

Even if you have an existing wallet with real Ether in it, I’d highly recommend creating a new one for development purposes. We’re going to be doing some relatively reckless things with private keys, so it can’t be a problem if they accidentally become public (public-private keys?)

Getting test Ether for your newly created wallet is easy: simply go to faucet.metamask.io and click on ‘request 1 ether from faucet’. This should be plenty for what we’ll be doing.

Finally, because we’re going to be working with the Ropsten TestNet without hosting our own node, we need a provider that we can connect the Blockchain through. Infura.io works well for this, so go and create a free account over there. Note down your provider url for the Ropsten TestNet (looks something like https://ropsten.infura.io/FE2Gfedcm3tfed3).

Deploying a Smart Contract

Using Python to deploy a Smart Contract without running your own node is pretty difficult, so we’re going to cheat on this step. For many Smart Contract use-cases, you only need to do it once anyway.

As I mentioned earlier, there’s a million guides on how to deploy an ERC20 contract, so we’re going to deploy a little different (and conveniently shorter).

Q: Who loves sharing their opinions on the internet?

Everyone?

Good answer. The following Smart Contract, which I’ve named ‘Soap Box’ allows anyone to broadcast any opinion they want to the blockchain, where it’ll be viewable for the rest of eternity (give or take).

There’s a catch though: Only addresses that have paid the requisite 0.02 Ether fee will be able to broadcast their opinion. Doesn’t sound very fair, but there you have it.

Remix, Ethereum’s online code editor is excellent, so create a new file over there and paste in the following code. It’s written in Solidity (the programming language of Smart Contracts). It doesn’t matter if the code doesn’t make too much sense — we’ll be going through the relevant parts in more detail later, but at the end of the day this is a Python tutorial.


pragma solidity ^0.4.0;contract SoapBox {



// Our 'dict' of addresses that are approved to share opinionsmapping (address => bool) approvedSoapboxer;string opinion;

// Our event to announce an opinion on the blockchain  
event OpinionBroadcast(address \_soapboxer, string \_opinion);



// This is a constructor function, so its name has to match the contractfunction SoapBox() public {}

// Because this function is 'payable' it will be called when ether is sent to the contract address.  
function() public payable{  
    // msg is a special variable that contains information about the transaction  
    if (msg.value > 20000000000000000) {    
        //if the value sent greater than 0.02 ether (in Wei)  
        // then add the sender's address to approvedSoapboxer   
        approvedSoapboxer\[msg.sender\] =  true;  
    }  
}  
  
  
// Our read-only function that checks whether the specified address is approved to post opinions.  
function isApproved(address \_soapboxer) public view returns (bool approved) {  
    return approvedSoapboxer\[\_soapboxer\];  
}   
  
// Read-only function that returns the current opinion  
function getCurrentOpinion() public view returns(string) {  
    return opinion;  
}


//Our function that modifies the state on the blockchainfunction broadcastOpinion(string _opinion) public returns (bool success) {

    // Looking up the address of the sender will return false if the sender isn't approved  
    if (approvedSoapboxer\[msg.sender\]) {  
          
        opinion = \_opinion;  
        emit OpinionBroadcast(msg.sender, opinion);  
        return true;  
          
    } else {  
        return false;  
    }  
      
}

}

Here’s where Metamask becomes really useful: If you click on the ‘run’ tab on the top right of the remix window and select ‘Injected Web3’ under the ‘Environment’ dropdown, the ‘Account’ dropdown should populate with address of the account you created earlier in MetaMask. If not, just refresh the browser.

Next click on ‘Create’. Metamask should bring up a popup asking you to confirm the transaction. If not, just open the Metamask extension and do it there:

You’ll receive a message at the bottom of the Remix console letting you know that the creation of the contract is pending. Click on the link to view its status on Etherscan. If you refresh and the ‘To’ field is populated with the Contract address, the contract has successfully deployed.

Once you’ve noted down the contract address, it’s time for us to start interacting with the contract via Web3.py

In my mind are four (and a half) ways you can interact with Ethereum Smart Contracts. The last two (and a half) often get lumped together, but the differences are important. We’ve already seen the first one — deploying a Smart Contract on the Blockchain. Now we’ll cover the rest on python:

  • Sending Ether to a Contract: Self explanatory really — sending ether from a wallet to the address of the smart contract. Hopefully in exchange for something useful
  • Calling a function: Executing a read-only function of a Smart Contract to get some information (such as an address’s balance)
  • Transacting with a function: Executing a function of a Smart Contract that makes changes to the state of the blockchain.
  • Viewing Events: Viewing information published onto the blockchain because of previous transactions with functions.

Send Ether to a Contract

Some (but not all) Smart Contracts include a ‘payable’ function. These functions are triggered if you send Ether to the contract’s address. A classic use case for this is an ICO: send ether to a contract and return you’ll be issued with tokens.

First we’ll start off with our imports and create a new web3 object that’s connected to the Ropsten TestNet via Infura.io.


import timefrom web3 import Web3, HTTPProvider

contract_address = [YOUR CONTRACT ADDRESS]wallet_private_key = [YOUR TEST WALLET PRIVATE KEY]wallet_address = [YOUR WALLET ADDRESS]w3 = Web3(HTTPProvider([YOUR INFURA URL]))

w3.eth.enable_unaudited_features()

You can find your wallet private key from the menu next to your Account Name in Metamask. Because we’re using some features of Web3.py that haven’t been fully audited for security, we need to call w3.eth.enable_unaudited_features() to acknowledge that we’re aware that bad things might happen_._ I told you we were doing some risky things with private keys!

Now we’ll write a function that sends ether from our wallet to the contract:

def send_ether_to_contract(amount_in_ether):

amount\_in\_wei = w3.toWei(amount\_in\_ether,**'ether'**);  

nonce = w3.eth.getTransactionCount(wallet\_address)  

txn\_dict = {  
        **'to'**: contract\_address,  
        **'value'**: amount\_in\_wei,  
        **'gas'**: 2000000,  
        **'gasPrice'**: w3.toWei(**'40'**, **'gwei'**),  
        **'nonce'**: nonce,  
        **'chainId'**: 3  
}  

signed\_txn = w3.eth.account.signTransaction(txn\_dict, wallet\_private\_key)  

txn\_hash = w3.eth.sendRawTransaction(signed\_txn.rawTransaction)  

txn\_receipt = **None**    count = 0  
**while** txn\_receipt **is None and** (count < 30):  

    txn\_receipt = w3.eth.getTransactionReceipt(txn\_hash)  

    print(txn\_receipt)  

    time.sleep(10)  


**if** txn\_receipt **is None**:  
    **return** {**'status'**: **'failed'**, **'error'**: **'timeout'**}  

**return** {**'status'**: **'added'**, **'txn\_receipt'**: txn\_receipt}

First let’s go over the transaction dictionary txn_dict— it contains most of the information required to define the transaction we send to the Smart Contraction.

  • To: Where we’re sending the ether to (in this case the smart contract)
  • Value: How much we’re sending in Wei
  • Gas: Gas is a measure of the computational effort that goes into executing a transaction on Ethereum. In this case we’re specifying an upper limit on how much gas we’re willing to go through to execute this transaction.*
  • Gas Price: How much we’re willing to pay (in Wei) per unit of Gas.
  • Nonce: This is an address nonce rather than the more commonly referred to Proof of Work. It’s simply a count of how many previous transactions the sending address has made, and is used to prevent double spending.
  • Chain ID: Each Ethereum Network has its own chain ID — the main net has an ID of 1, while Ropsten’s is 3. You can find a longer list here.

*A quick note on the gas limit: There’s functions that allow you to estimate how much gas a transaction will use. However, I find the best way to choose the limit is to work out how much your willing to pay in ether before you’d just rather have the transaction fail, and go with that.

Once we’ve defined the important parts of our transaction, we’ll sign it using our wallet’s Private Key. Then it’s ready to be sent to the network, which we’ll do with the sendRawTransaction method.

Our transaction won’t actually be completed until a miner decides to include it in a block. In general, how much you offer to pay for each unit Gas (remember our gas price parameter) determines how quickly a node will decide to include your transaction in a block (if at all).

https://ethgasstation.info/ is good place to work out how long you’ll be waiting for your transaction to be included in a block.

This time delay means that transactions are asynchronous. When we call the sendRawTransaction, we’re immediately given transaction’s unique hash. You can use this hash at any time to query whether your transaction has been included in a block or not. We’ll know that the transaction has be added to the blockchain if and only if we’re able to get a transaction receipt (because all good purchases come with receipts right?). That’s why we create the loop to regularly check whether we’ve got a receipt:

txn\_receipt = **None**    count = 0

**while** txn\_receipt **is None and** (count < 30):  

    txn\_receipt = w3.eth.getTransactionReceipt(txn\_hash)  

    print(txn\_receipt)  

    time.sleep(10)

It’s worth noting that a transaction can be added to the blockchain and still fail for any number of reasons, such as not having enough Gas.

So that’s the Python code for sending ether to the contract. Let’s quickly review the payable function that we wrote in Solidity:





function() public payable{if (msg.value >= 20000000000000000) {approvedSoapboxer[msg.sender] = true;}}

Msg is a special variable in Smart Contracts that contains information about the transaction that was sent to the Smart Contract. In this case, we’re using msg.value, which gives the amount of Ether that was sent in the transaction (in Wei rather than raw Ether). Likewise, msg.sender gives the address of the wallet that made the transaction — if enough Ether has been sent, we’ll add this to the dictionary of approved accounts.

Go ahead an run the send_ether_to_contract function. Hopefully you’ll get a receipt back. You can also check whether the transaction went through by looking up your wallet address on the Ropsten Network section of Etherscan. We’ll be able to get a bit more information in Python in the next section.

Calling a function

We’ve just sent some amount of Ether to our Smart Contract, so it makes sense that we want to check whether the our wallet address has now been approved to share opinions. For this we’ve defined the following function in our Smart Contract:



function isApproved(address _soapboxer) public view returns (bool approved) {return approvedSoapboxer[_soapboxer];}

Compared to python, there’s a lot of extra stuff plastered around this function such as declaring type (address and bool). At its core though, this function simply takes an address (the _soapboxer parameter), looks up the corresponding approval boolean in what is effectively (but not quite) a hash table/python dict and returns that value.

When you call a smart contract function, the Ethereum node will calculate the result, and return it to you. Here’s where things get a little bit complex: calls are read-only, meaning they don’t make any changes to the blockchain. If the above function contained a line of code to record number time an address had been checked for approval:

approvedCheckedCount[_soapboxer] = approvedCheckedCount[_soapboxer] + 1

Then when the function was called, the node would calculate the new value of approvedCheckedCount, but discard it once a result had been returned.

In exchange for being read-only, function calls don’t cost you any ether to run, so you can happily check whether an account as been approved without worrying about costs.

Let’s jump back up to the top of our python file and add some more set-up code.

import contract_abi

contract = w3.eth.contract(address = contract_address, abi = contract_abi.abi)

You’ll need to create another python file named contract_abi. This will contain a big JSON string of information that Python needs to interact with the functions we defined in our Smart Contract, called the Application Binary Interface (ABI). You can find the ABI’s JSON string for your smart contract in Remix:

  • Click on the ‘Compile’ tab
  • Click on ‘Details’ — a modal should appear with a lot of information
  • Scroll Down to the ABI section and click on the ‘Copy to clipboard’ Icon.

Paste the copied string into your ‘contract_abi.py’ file, which should look something like this:





abi = """[{A BIG LIST OF ABI INFO SPREAD ACROSS MULTIPLE DICTS}]"""

The other line we added to our main python file now takes this ABI JSON string and uses it to set up a contract object. If you explore contract, you’ll notice that it contains a functions attribute that includes the three functions we created in our Smart Contract.

Now we’ll create a python function that calls the Smart Contract’s isApproved function to check whether a specified address is approved to share opinions.

def check_whether_address_is_approved(address):

**return** contract.functions.isApproved(address).call()

Well that was short.

You can now use this to check wether your wallet address is approved. If you ran the send_ether_to_contract function earlier and sent a sufficient amount of Ether, hopefully you’ll get back ‘true’.

Transacting with a function

We’re up to the final major interaction with our Smart Contract — broadcasting an opinion. Once again, let’s review our Solidity Code:


function broadcastOpinion(string _opinion) public returns (bool success) {if (approvedSoapboxer[msg.sender]) {

        opinion = \_opinion;  
        emit OpinionBroadcast(msg.sender, opinion);  
        return true;

    } else {  
        return false;  
    }  
}

Nothing too new here: we take the incoming _opinion parameter and use it to set global variable opinion. (Which can intern by queried by a getter function, if you so desire). There’s one line that’s a bit different:

emit OpinionBroadcast(msg.sender, opinion)


We’ll cover that shortly.When you interact with a Smart Contract’s function via a transaction, any changes the function makes to the state of Smart Contract are published on the blockchain. In exchange for this privilege, you’ll have to pay the miners some (hopefully small) amount of Ether. Python time:

def broadcast_an_opinion(covfefe):

nonce = w3.eth.getTransactionCount(wallet\_address)  

txn\_dict = contract.functions.broadcastOpinion(covfefe).buildTransaction({  
    **'chainId'**: 3,  
    **'gas'**: 140000,  
    **'gasPrice'**: w3.toWei(**'40'**, **'gwei'**),  
    **'nonce'**: nonce,  
})  

signed\_txn = w3.eth.account.signTransaction(txn\_dict, private\_key=wallet\_private\_key)  

result = w3.eth.sendRawTransaction(signed\_txn.rawTransaction)  

tx\_receipt = w3.eth.getTransactionReceipt(result)  

count = 0  
**while** tx\_receipt **is None and** (count < 30):  

    time.sleep(10)  

    tx\_receipt = w3.eth.getTransactionReceipt(result)  

    print(tx\_receipt)  


**if** tx\_receipt **is None**:  
    **return** {**'status'**: **'failed'**, **'error'**: **'timeout'**}  

processed\_receipt = contract.events.OpinionBroadcast().processReceipt(tx\_receipt)  

print(processed\_receipt)  
  
output = **"Address {} broadcasted the opinion: {}"**\\  
    .format(processed\_receipt\[0\].args.\_soapboxer, processed\_receipt\[0\].args.\_opinion)  
print(output)  

**return** {**'status'**: **'added'**, **'processed\_receipt'**: processed\_receipt}

This is effectively the same process as the one used when sending Ether to the smart contract. We’ll create and sign a transaction and then send it to the network. Once again, the transaction is asynchronous, which means that regardless of what the function is told to return in the Solidity code, what you’ll actually get back is always the transaction’s hash.

Given that transactions don’t return any useful information in their own right, we need something else. This leads us to our last (half) way of interacting with Smart Contracts.

Events

I call Events a “half” way of interacting with Smart Contracts because technically they’re emitted by a transaction. Events are a Smart Contract’s way of recording things on a blockchain in an easy to read form — they’re basically just a set of values that can be looked up using the receipt of a particular transaction. We defined one at the very top our smart contract:

event OpinionBroadcast(address _soapboxer, string _opinion);

Then, when we use the broadcastOpinion function, we use it to emit information to the blockchain.

Once the transaction has been added to a block, you can then use the transaction hash to query the blockchain for the specific values emitted by the OpinionBroadcast event. That’s the last bit of our python code in the function broadcast_an_opinion. You’ll notice that the information we asked to be emitted by the event is stored in the ‘args’ attribute.

This event is very much public. In fact, anyone can easily use Etherscan or similar to view a log of all the events emitted by your Smart Contract.

Etherscan automatically detects ‘Transfer’ events a lists them all. Nifty

If you’ve made it this far, you’ve earned the right to broadcast an opinion. Go ahead and run broadcast_an_opinion with your opinion of choice.

If everything has run smoothly, you should soon get back a processed receipt, and a printout from the OpinionBroadcast event that has been put onto the blockchain.

Nice.

Here’s the complete python code:


import timefrom web3 import Web3, HTTPProvider

contract_address = [YOUR CONTRACT ADDRESS]wallet_private_key = [YOUR TEST WALLET PRIVATE KEY]wallet_address = [YOUR WALLET ADDRESS]w3 = Web3(HTTPProvider([YOUR INFURA URL]))

w3.eth.enable_unaudited_features()

contract = w3.eth.contract(address = contract_address, abi = contract_abi.abi)

def send_ether_to_contract(amount_in_ether):

amount\_in\_wei = w3.toWei(amount\_in\_ether,**'ether'**);  

nonce = w3.eth.getTransactionCount(wallet\_address)  

txn\_dict = {  
        **'to'**: contract\_address,  
        **'value'**: amount\_in\_wei,  
        **'gas'**: 2000000,  
        **'gasPrice'**: w3.toWei(**'40'**, **'gwei'**),  
        **'nonce'**: nonce,  
        **'chainId'**: 3  
}  

signed\_txn = w3.eth.account.signTransaction(txn\_dict, wallet\_private\_key)  

txn\_hash = w3.eth.sendRawTransaction(signed\_txn.rawTransaction)  

txn\_receipt = **None**    count = 0  
**while** txn\_receipt **is None and** (count < 30):  

    txn\_receipt = w3.eth.getTransactionReceipt(txn\_hash)  

    print(txn\_receipt)  

    time.sleep(10)  


**if** txn\_receipt **is None**:  
    **return** {**'status'**: **'failed'**, **'error'**: **'timeout'**}  

**return** {**'status'**: **'added'**, **'txn\_receipt'**: txn\_receipt}  

def check_whether_address_is_approved(address):

**return** contract.functions.isApproved(address).call()  

def broadcast_an_opinion(covfefe):

nonce = w3.eth.getTransactionCount(wallet\_address)  

txn\_dict = contract.functions.broadcastOpinion(covfefe).buildTransaction({  
    **'chainId'**: 3,  
    **'gas'**: 140000,  
    **'gasPrice'**: w3.toWei(**'40'**, **'gwei'**),  
    **'nonce'**: nonce,  
})  

signed\_txn = w3.eth.account.signTransaction(txn\_dict, private\_key=wallet\_private\_key)  

result = w3.eth.sendRawTransaction(signed\_txn.rawTransaction)  

tx\_receipt = w3.eth.getTransactionReceipt(result)  

count = 0  
**while** tx\_receipt **is None and** (count < 30):  

    time.sleep(10)  

    tx\_receipt = w3.eth.getTransactionReceipt(result)  

    print(tx\_receipt)  


**if** tx\_receipt **is None**:  
    **return** {**'status'**: **'failed'**, **'error'**: **'timeout'**}  

processed\_receipt = contract.events.OpinionBroadcast().processReceipt(tx\_receipt)  

print(processed\_receipt)  

output = **"Address {} broadcasted the opinion: {}"**\\  
    .format(processed\_receipt\[0\].args.\_soapboxer, processed\_receipt\[0\].args.\_opinion)  
print(output)  

**return** {**'status'**: **'added'**, **'processed\_receipt'**: processed\_receipt}  

if __name__ == "__main__":

send\_ether\_to\_contract(0.03)  

is\_approved = check\_whether\_address\_is\_approved(wallet\_address)  
  
print(is\_approved)  

broadcast\_an\_opinion(**'Despite the Constant Negative Press'**)

Wrapping up

So that about covers it. As I mentioned, we’re not quite at the point yet where it’s easy to actually deploy Smart Contracts using python, but everything else is there. At Sempo, we’re using all of the technology that I’ve covered above to make Disaster Response more transparent.

If you have any thoughts or suggestions, please leave them in the comments and I’ll get back to you ASAP!

EDIT: Thanks for Sebastian Dirman for pointing out that w3.toWei(value, ‘ether’) is a far better way to convert between Ether and Wei — simply multiplying your amount in ether by 1000000000000000000 can result in type errors!