ERC-20 token transfer with web3.py
This is a tutorial how to transfer ERC-20 token in Python with web3-ethereum-defi package.
You need
A private key in hexadecimal format
ETH or other EVM native token for gas fees
A Python virtual environment with web3-ethereum-defi installed
Understanding how to operate command line and command line applications
Set up
Import your private key to an environment variable in UNIX shell:
export PRIVATE_KEY="0x1111111111111"
Set up your JSON-RPC connecgtion:
export JSON_RPC_URL="https://"
Transfer script
Then create the following script:
"""Manual transfer script.
- For a hardcoded token, asks to address and amount where to transfer tokens.
- Waits for the transaction to complete
"""
import datetime
import os
import sys
from decimal import Decimal
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import HTTPProvider, Web3
from web3.middleware import construct_sign_and_send_raw_middleware
from eth_defi.abi import get_deployed_contract
from eth_defi.token import fetch_erc20_details
from eth_defi.txmonitor import wait_transactions_to_complete
# What is the token we are transferring.
# Replace with your own token address.
ERC_20_TOKEN_ADDRESS = "0x0aC7B3733cBeE5D87A80fbf331f4A0bD01f17386"
# Connect to JSON-RPC node
json_rpc_url = os.environ["JSON_RPC_URL"]
web3 = Web3(HTTPProvider(json_rpc_url))
print(f"Connected to blockchain, chain id is {web3.eth.chain_id}. the latest block is {web3.eth.block_number:,}")
# Read and setup a local private key
private_key = os.environ.get("PRIVATE_KEY")
assert private_key is not None, "You must set PRIVATE_KEY environment variable"
assert private_key.startswith("0x"), "Private key must start with 0x hex prefix"
account: LocalAccount = Account.from_key(private_key)
web3.middleware_onion.add(construct_sign_and_send_raw_middleware(account))
# Show users the current status of token and his address
erc_20 = get_deployed_contract(web3, "ERC20MockDecimals.json", ERC_20_TOKEN_ADDRESS)
token_details = fetch_erc20_details(web3, ERC_20_TOKEN_ADDRESS)
print(f"Token details are {token_details}")
balance = erc_20.functions.balanceOf(account.address).call()
eth_balance = web3.eth.getBalance(account.address)
print(f"Your balance is: {token_details.convert_to_decimals(balance)} {token_details.symbol}")
print(f"Your have {eth_balance/(10**18)} ETH for gas fees")
# Ask for transfer details
decimal_amount = input("How many tokens to transfer? ")
to_address = input("Give destination Ethereum address? ")
# Some input validation
try:
decimal_amount = Decimal(decimal_amount)
except ValueError as e:
raise AssertionError(f"Not a good decimal amount: {decimal_amount}") from e
assert web3.isChecksumAddress(to_address), f"Not a valid address: {to_address}"
# Fat-fingering check
print(f"Confirm transfering {decimal_amount} {token_details.symbol} to {to_address}")
confirm = input("Ok [y/n]?")
if not confirm.lower().startswith("y"):
print("Aborted")
sys.exit(1)
# Convert a human-readable number to fixed decimal with 18 decimal places
raw_amount = token_details.convert_to_raw(decimal_amount)
tx_hash = erc_20.functions.transfer(to_address, raw_amount).transact({"from": account.address})
# This will raise an exception if we do not confirm within the timeout
print(f"Broadcasted transaction {tx_hash.hex()}, now waiting 5 minutes for mining")
wait_transactions_to_complete(web3, [tx_hash], max_timeout=datetime.timedelta(minutes=5))
print("All ok!")
Running
Run the script:
python scripts/erc20-manual-transfer.py
Example output
Connected to blockchain, chain id is 1. the latest block is 14,627,918
Token details are <XXX (XXX) at 0x0aC7B3733cBeE5D87A80fbf331f4A0bD01f17386>
Your balance is: 369999999 XXX
Your have : 0.2679961495972585 ETH for gas fees
How many tokens to transfer? 1
Give destination Ethereum address? 0x6449299d1d268c4008b4fB992afd04AB5fAec4E6
Confirm transfering 1 XXX to 0x6449299d1d268c4008b4fB992afd04AB5fAec4E6
Ok [y/n]?y
Broadcasted transaction 0xfed8c07b1da1d4348d3ea0ec678f30082fc8e944ada4b0f6510b5a7c05ceb910, now waiting 5 minutes for mining
All ok!