Note
Prefer to view this code in a Jupyter Notebook? View the repo here.
There are two methods for sending transactions using web3.py: ~web3.eth.Eth.send_transaction
and ~web3.eth.Eth.send_raw_transaction
. A brief guide:
- Want to sign a transaction offline or send pre-signed transactions?
- use
sign_transaction <eth_account.account.Account.sign_transaction>
+~web3.eth.Eth.send_raw_transaction
- use
- Are you primarily using the same account for all transactions and would you prefer to save a few lines of code?
- configure
~web3.middleware.construct_sign_and_send_raw_middleware
, then - use
~web3.eth.Eth.send_transaction
- configure
- Otherwise:
- load account via eth-account (
w3.eth.account.from_key(pk) <eth_account.account.Account.from_key>
), then - use
~web3.eth.Eth.send_transaction
- load account via eth-account (
Interacting with or deploying a contract?
- Option 1:
~web3.contract.ContractFunction.transact
uses~web3.eth.Eth.send_transaction
under the hood - Option 2:
~web3.contract.ContractFunction.build_transaction
+sign_transaction <eth_account.account.Account.sign_transaction>
+~web3.eth.Eth.send_raw_transaction
An example for each can be found below.
Many tutorials use eth-tester
(via EthereumTesterProvider) for convenience and speed of conveying ideas/building a proof of concept. Transactions sent by test accounts are auto-signed.
from web3 import Web3, EthereumTesterProvider
w3 = Web3(EthereumTesterProvider())
# eth-tester populates accounts with test ether:
acct1 = w3.eth.accounts[0]
some_address = "0x0000000000000000000000000000000000000000"
# when using one of its generated test accounts,
# eth-tester signs the tx (under the hood) before sending:
tx_hash = w3.eth.send_transaction({
"from": acct1,
"to": some_address,
"value": 123123123123123
})
tx = w3.eth.get_transaction(tx_hash)
assert tx["from"] == acct1
The ~web3.eth.Eth.send_transaction
method is convenient and to-the-point. If you want to continue using the pattern after graduating from eth-tester
, you can utilize web3.py middleware to sign transactions from a particular account:
from web3.middleware import construct_sign_and_send_raw_middleware
import os
# Note: Never commit your key in your code! Use env variables instead:
pk = os.environ.get('PRIVATE_KEY')
# Instantiate an Account object from your key:
acct2 = w3.eth.account.from_key(pk)
# For the sake of this example, fund the new account:
w3.eth.send_transaction({
"from": acct1,
"value": w3.to_wei(3, 'ether'),
"to": acct2.address
})
# Add acct2 as auto-signer:
w3.middleware_onion.add(construct_sign_and_send_raw_middleware(acct2))
# pk also works: w3.middleware_onion.add(construct_sign_and_send_raw_middleware(pk))
# Transactions from `acct2` will then be signed, under the hood, in the middleware:
tx_hash = w3.eth.send_transaction({
"from": acct2.address,
"value": 3333333333,
"to": some_address
})
tx = w3.eth.get_transaction(tx_hash)
assert tx["from"] == acct2.address
# Optionally, you can set a default signer as well:
# w3.eth.default_account = acct2.address
# Then, if you omit a "from" key, acct2 will be used.
if you don't opt for the middleware, you'll need to:
- build each transaction,
sign_transaction <eth_account.account.Account.sign_transaction>
, and- then use
~web3.eth.Eth.send_raw_transaction
.
# 1. Build a new tx
transaction = {
'from': acct2.address,
'to': some_address,
'value': 1000000000,
'nonce': w3.eth.get_transaction_count(acct2.address),
'gas': 200000,
'maxFeePerGas': 2000000000,
'maxPriorityFeePerGas': 1000000000,
}
# 2. Sign tx with a private key
signed = w3.eth.account.sign_transaction(transaction, pk)
# 3. Send the signed transaction
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
tx = w3.eth.get_transaction(tx_hash)
assert tx["from"] == acct2.address
The same concepts apply for contract interactions, at least under the hood.
Executing a function on a smart contract requires sending a transaction, which is typically done in one of two ways:
- executing the
~web3.contract.ContractFunction.transact
function, or ~web3.contract.ContractFunction.build_transaction
, then signing and sending the raw transaction.
#########################################
#### SMOL CONTRACT FOR THIS EXAMPLE: ####
#########################################
# // SPDX-License-Identifier: MIT
# pragma solidity 0.8.17;
#
# contract Billboard {
# string public message;
#
# constructor(string memory _message) {
# message = _message;
# }
#
# function writeBillboard(string memory _message) public {
# message = _message;
# }
# }
# After compiling the contract, initialize the contract factory:
init_bytecode = "60806040523480156200001157600080fd5b5060..."
abi = '[{"inputs": [{"internalType": "string","name": "_message",...'
Billboard = w3.eth.contract(bytecode=init_bytecode, abi=abi)
# Deploy a contract using `transact` + the signer middleware:
tx_hash = Billboard.constructor("gm").transact({"from": acct2.address})
receipt = w3.eth.get_transaction_receipt(tx_hash)
deployed_addr = receipt["contractAddress"]
# Reference the deployed contract:
billboard = w3.eth.contract(address=deployed_addr, abi=abi)
# Manually build and sign a transaction:
unsent_billboard_tx = billboard.functions.writeBillboard("gn").build_transaction({
"from": acct2.address,
"nonce": w3.eth.get_transaction_count(acct2.address),
})
signed_tx = w3.eth.account.sign_transaction(unsent_billboard_tx, private_key=acct2.key)
# Send the raw transaction:
assert billboard.functions.message().call() == "gm"
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
w3.eth.wait_for_transaction_receipt(tx_hash)
assert billboard.functions.message().call() == "gn"