Skip to main content

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

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 structure

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

We 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. (Though you could get around this by 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 API key.

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

For the purposes of this tutorial, you don't need to use environmental variables, but it's a good practice to do so so that sensitive 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')
api_key = os.getenv('INFURA_API_KEY')
infura_url = 'https://goerli.infura.io/v3/{}'.format(api_key)
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 straightforward: 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 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 the Sepolia 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.

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')
api_key = os.getenv('INFURA_API_KEY')
infura_url = 'https://goerli.infura.io/v3/{}'.format(api_key)
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
Was this helpful?
Connect MetaMask to provide feedback
What is this?
This is a trial feedback system that uses Verax to record your feedback as onchain attestations on Linea Mainnet. When you vote, submit a transaction in your wallet.