Ga naar hoofdinhoud

Web3.py: How to send EIP-1559 transactions (ETH & ERC-20 tokens)

Hello friends! 👋

In this tutorial we’re going to take a look at how to use Web3.py to send EIP-1559 transactions, containing either ETH or any verified ERC-20 token. As usual, if you only need the script’s code, feel free to scroll down to the end of the article.

Script’s structure

The script will be split within two methods, one for sending ETH and another for sending ERC-20 tokens.

I recommend only using one method per script run, since the nonce can be erroneously calculated when two transactions are sent in a short amount of time. A fix for this can be, of course, manually incrementing the nonce for the second transaction.

Prerequisites

For this tutorial to work, you will need to have installed:

  • web3.py (can be installed by running - pip install web3)
  • dotenv (optional, but recommended for better security - pip install python-dotenv)

Setting up our project

Let’s start by importing the required libraries and creating our .env file, which will contain our account’s private key (you can use a test wallet without any real funds on it) and the Infura project ID.

If you need a little more help on creating a .env file, you can check out this article.

For this tutorial’s purposes you can also not use environmental variables, but it’s a good practice to use them, so the sensible information is not easily accessible within the code.

from web3 import Web3
import json
import os
from dotenv import load_dotenv

load_dotenv()
private_key = os.getenv('SIGNER_PRIVATE_KEY')
project_id = os.getenv('INFURA_PROJECT_ID')
infura_url = 'https://goerli.infura.io/v3/{}'.format(project_id)
web3 = Web3(Web3.HTTPProvider(infura_url))

Sending ETH

Sending ETH is a simpler process than sending ERC-20 tokens, because it doesn’t require interacting with any smart contracts. This means that the whole operation is pretty straight forward, we’re going to create a transaction object, then sign and send the transaction to the blockchain:

def send_ETH(from_address, to_address, amount):
tx = {
'type': '0x2',
'nonce': web3.eth.getTransactionCount(from_address),
'from': from_address,
'to': to_address,
'value': web3.toWei(0.01, 'ether'),
'maxFeePerGas': web3.toWei('250', 'gwei'),
'maxPriorityFeePerGas': web3.toWei('3', 'gwei'),
'chainId': 5
}
gas = web3.eth.estimateGas(tx) # gas limit
tx['gas'] = gas
signed_tx = web3.eth.account.signTransaction(tx, private_key)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)

if tx_receipt['status'] == 1:
print('ETH transferred successfully! Hash: {}'.format(str(web3.toHex(tx_hash))))
else:
print('There was an error transferring the ETH')

As you can see above, for signing the transaction we’ve used our test account’s private key, within the eth.signTransaction method. Then, we can send the transaction by calling eth.send_raw_transaction.

Finally, once the transaction’s receipt is ready, we can determine if everything went smoothly by checking the status, and then print out the transaction’s hash.

Sending ERC-20 tokens

For this tutorial, let’s try and send some UNI tokens. What makes sending ERC-20 tokens different than sending plain ETH, is the fact that our script will need the contract’s ABI. The ABI can be easily found on etherscan, by searching for the contract’s address and going to the ‘Contract’ section.

So let’s begin by finding the ABI for UNI, on Goerli testnet, by going to the token’s etherscan page, under the Contract tab. Then, we can create an abi.json file in our project’s directory and store the copied ABI there.

# This code lies in the __main__ function, scroll down to see the
# whole script if needed

# address for UNI token
contract_address = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'

with open("./abi.json", 'r') as f:
contract_abi = json.load(f)

contract = web3.eth.contract(address=contract_address, abi=contract_abi)
send_ERC20(from_address, to_address, 100000, contract) # amount is in wei

After fetching the ABI from the local abi.json file, we can create our contract object and pass all these parameters to the send_ERC20 function, which is very similar to the send_ETH function shown above.

def send_ERC20(from_address, to_address, amount, contract):
tx = contract.functions.transfer(to_address, amount).buildTransaction({
'from': from_address,
'nonce': web3.eth.getTransactionCount(from_address),
'maxFeePerGas': web3.toWei('250', 'gwei'),
'maxPriorityFeePerGas': web3.toWei('3', 'gwei'),
'value': 0,
'chainId': 5
})
gas = web3.eth.estimateGas(tx)
tx['gas'] = gas
signed_tx = web3.eth.account.signTransaction(tx, private_key)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)

if tx_receipt['status'] == 1:
print('Tokens transferred successfully! Hash: {}'.format(str(web3.toHex(tx_hash))))
else:
print('There was an error transferring the tokens')

As you can see, the only difference is that our code uses the contract’s transfer function to build the transaction, otherwise it’s the same process as for sending plain ETH.

And that’s it! Make sure to declare your ‘from’ and ‘to’ addresses in the main function and you should be good to go. Thanks for sticking around to the end of the article! 😊

Complete code overview

from web3 import Web3
import json
import os
from dotenv import load_dotenv

load_dotenv()
private_key = os.getenv('SIGNER_PRIVATE_KEY')
project_id = os.getenv('INFURA_PROJECT_ID')
infura_url = 'https://goerli.infura.io/v3/{}'.format(project_id)
web3 = Web3(Web3.HTTPProvider(infura_url))

def send_ETH(from_address, to_address, amount):
tx = {
'type': '0x2',
'nonce': web3.eth.getTransactionCount(from_address),
'from': from_address,
'to': to_address,
'value': web3.toWei(0.01, 'ether'),
'maxFeePerGas': web3.toWei('250', 'gwei'),
'maxPriorityFeePerGas': web3.toWei('3', 'gwei'),
'chainId': 5
}
gas = web3.eth.estimateGas(tx)
tx['gas'] = gas
signed_tx = web3.eth.account.signTransaction(tx, private_key)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)

if tx_receipt['status'] == 1:
print('ETH transferred successfully! Hash: {}'.format(str(web3.toHex(tx_hash))))
else:
print('There was an error transferring the ETH')

def send_ERC20(from_address, to_address, amount, contract):
tx = contract.functions.transfer(to_address, amount).buildTransaction({
'from': from_address,
'nonce': web3.eth.getTransactionCount(from_address),
'maxFeePerGas': web3.toWei('250', 'gwei'),
'maxPriorityFeePerGas': web3.toWei('3', 'gwei'),
'value': 0,
'chainId': 5
})
gas = web3.eth.estimateGas(tx)
tx['gas'] = gas
signed_tx = web3.eth.account.signTransaction(tx, private_key)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)

if tx_receipt['status'] == 1:
print('Tokens transferred successfully! Hash: {}'.format(str(web3.toHex(tx_hash))))
else:
print('There was an error transferring the tokens')

if __name__ == '__main__':

from_address = '0x66D...'
to_address = '0x895...'

### SEND ETH ###################

send_ETH(from_address, to_address, 10000) # amount is in wei

### SEND ERC-20 ################

# UNI token address on Goerli:
contract_address = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'

with open("./abi.json", 'r') as f:
contract_abi = json.load(f)

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

send_ERC20(from_address, to_address, 100000, contract) # amount is in wei