Last active
May 7, 2025 14:25
-
-
Save wolovim/4c4a9bd66a60c5f98e25d652930c84c2 to your computer and use it in GitHub Desktop.
web3.py EIP-7702 worked example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
COUNTER_ADDRESS = "0x5bC6068195306fC5148f125D7dFcF525185813E6" | |
COUNTER_ABI = """[ | |
{ | |
"stateMutability": "nonpayable", | |
"type": "function", | |
"name": "set_number", | |
"inputs": [ | |
{ | |
"name": "new_number", | |
"type": "uint256" | |
} | |
], | |
"outputs": [] | |
}, | |
{ | |
"stateMutability": "nonpayable", | |
"type": "function", | |
"name": "lock", | |
"inputs": [], | |
"outputs": [] | |
}, | |
{ | |
"stateMutability": "nonpayable", | |
"type": "function", | |
"name": "unlock", | |
"inputs": [], | |
"outputs": [] | |
}, | |
{ | |
"stateMutability": "nonpayable", | |
"type": "function", | |
"name": "get_number", | |
"inputs": [], | |
"outputs": [ | |
{ | |
"name": "", | |
"type": "uint256" | |
} | |
] | |
}, | |
{ | |
"stateMutability": "view", | |
"type": "function", | |
"name": "number", | |
"inputs": [], | |
"outputs": [ | |
{ | |
"name": "", | |
"type": "uint256" | |
} | |
] | |
}, | |
{ | |
"stateMutability": "view", | |
"type": "function", | |
"name": "locked", | |
"inputs": [], | |
"outputs": [ | |
{ | |
"name": "", | |
"type": "bool" | |
} | |
] | |
}, | |
{ | |
"stateMutability": "nonpayable", | |
"type": "constructor", | |
"inputs": [ | |
{ | |
"name": "initial_number", | |
"type": "uint256" | |
} | |
], | |
"outputs": [] | |
} | |
]""" | |
MULTICALL_ADDRESS = "0xca11bde05977b3631167028862be2a173976ca11" | |
MULTICALL_ABI = '[{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes[]","name":"returnData","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3[]","name":"calls","type":"tuple[]"}],"name":"aggregate3","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3Value[]","name":"calls","type":"tuple[]"}],"name":"aggregate3Value","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"blockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getBasefee","outputs":[{"internalType":"uint256","name":"basefee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"getBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBlockNumber","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"chainid","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockCoinbase","outputs":[{"internalType":"address","name":"coinbase","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockDifficulty","outputs":[{"internalType":"uint256","name":"difficulty","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockGasLimit","outputs":[{"internalType":"uint256","name":"gaslimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockTimestamp","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getEthBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryAggregate","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryBlockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"}]' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 1. import dependencies | |
from eth_account import Account | |
from dotenv import load_dotenv | |
import os | |
from web3 import Web3, HTTPProvider | |
from artifacts import MULTICALL_ABI, MULTICALL_ADDRESS, COUNTER_ADDRESS, COUNTER_ABI | |
import json | |
load_dotenv() | |
MULTICALL_ADDRESS = Web3.to_checksum_address(MULTICALL_ADDRESS) | |
MULTICALL_ABI = json.loads(MULTICALL_ABI) | |
COUNTER_ADDRESS = Web3.to_checksum_address(COUNTER_ADDRESS) | |
COUNTER_ABI = json.loads(COUNTER_ABI) | |
HOODI_CHAIN_ID = 560048 | |
# 2. instantiate w3 | |
w3 = Web3(HTTPProvider("https://ethereum-hoodi-rpc.publicnode.com")) | |
# 3. check the starting state of the contract | |
counter_contract = w3.eth.contract(address=COUNTER_ADDRESS, abi=COUNTER_ABI) | |
locked = counter_contract.functions.locked().call() | |
number = counter_contract.functions.number().call() | |
print(f"Counter state: locked={locked}, number={number}") | |
# 4. load EOA account (with Hoodi testnet ETH) from private key | |
acct = Account.from_key(os.getenv("TEST_PK")) | |
acct_code = w3.eth.get_code(acct.address) | |
nonce = w3.eth.get_transaction_count(acct.address) | |
print(f"Account code before: {acct_code.hex()}") | |
print(f"Account nonce before: {nonce}") | |
# 5. build an authorization utilizing the multicall contract | |
auth = { | |
"chainId": HOODI_CHAIN_ID, | |
"address": MULTICALL_ADDRESS, | |
"nonce": nonce + 1, | |
} | |
# 6. sign the auth with the EOA | |
signed_auth = acct.sign_authorization(auth) | |
# 7. build and encode the data to provide to aggregate function | |
def calculate_multicall_data(): | |
multicall_contract = w3.eth.contract(address=MULTICALL_ADDRESS, abi=MULTICALL_ABI) | |
counter_contract = w3.eth.contract(address=COUNTER_ADDRESS, abi=COUNTER_ABI) | |
lock_calldata = counter_contract.encode_abi("lock") | |
unlock_calldata = counter_contract.encode_abi("unlock") | |
new_number = 6 | |
set_number_calldata = counter_contract.encode_abi("set_number", args=[new_number]) | |
# Structure the calls for the aggregate function | |
# aggregate((address target, bytes callData)[]) | |
calls = [ | |
(COUNTER_ADDRESS, unlock_calldata), | |
(COUNTER_ADDRESS, set_number_calldata), | |
(COUNTER_ADDRESS, lock_calldata), | |
] | |
# Encode the outer call data for the aggregate function | |
return multicall_contract.encode_abi("aggregate", args=[calls]) | |
# 8. build the type 4 transaction | |
tx = { | |
"chainId": HOODI_CHAIN_ID, | |
"nonce": nonce, | |
"gas": 1_000_000, | |
"maxFeePerGas": 10**10, | |
"maxPriorityFeePerGas": 10**9, | |
"to": acct.address, | |
"authorizationList": [signed_auth], | |
"data": calculate_multicall_data(), | |
} | |
# 9. sign and send the transaction | |
signed_tx = acct.sign_transaction(tx) | |
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) | |
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) | |
# 10. verify the results | |
counter_contract = w3.eth.contract(address=COUNTER_ADDRESS, abi=COUNTER_ABI) | |
current_number = counter_contract.functions.number().call() | |
is_locked = counter_contract.functions.locked().call() | |
print(f"Counter state after: locked={is_locked}, number={current_number}") | |
new_acct_code = w3.eth.get_code(acct.address) | |
new_nonce = w3.eth.get_transaction_count(acct.address) | |
print(f"Account code after: {new_acct_code.hex()}") | |
print(f"Account nonce after: {new_nonce}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment