Crafting a Custom Ethereum Transaction Decoder: From Raw Hex to Human-Readable Data with Python
Ever looked at a raw Ethereum transaction and wondered what all that hex gibberish actually means? Let's build a decoder that turns opaque calldata into something you can actually read.
Why Decode Transactions?
Every time someone interacts with a smart contract, they send calldata — hex-encoded function signatures and parameters. Understanding this data is crucial for:
- Auditing contract interactions
- Building block explorers
- Monitoring whale wallets
- Debugging failed transactions
The Building Blocks
We'll use just two Python libraries: web3.py for chain interaction and eth-abi for decoding. Here's the core approach:
from web3 import Web3
from eth_abi import decode
import requests
# Connect to Ethereum
w3 = Web3(Web3.HTTPProvider('https://eth.llamarpc.com'))
# Get a transaction
tx_hash = '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060'
tx = w3.eth.get_transaction(tx_hash)
# Extract function selector (first 4 bytes of input)
input_data = tx['input']
function_selector = '0x' + input_data.hex()[:8]
print(f"Function Selector: {function_selector}")
The Magic: ABI-Based Decoding
Function selectors alone aren't enough — we need the contract's ABI. Here's a smart trick: query the Etherscan API for verified contract ABIs, cache them, then decode on the fly.
import json
from eth_abi import decode
def get_contract_abi(contract_address):
"""Fetch verified ABI from Etherscan with caching."""
url = f"https://api.etherscan.io/api"
params = {
'module': 'contract',
'action': 'getabi',
'address': contract_address,
'apikey': 'YOUR_API_KEY'
}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
if data['status'] == '1':
return json.loads(data['result'])
return None
def decode_transaction_input(tx_input, abi):
"""Decode calldata using contract ABI."""
selector = tx_input[:4]
for item in abi:
if item.get('type') != 'function':
continue
# Compute function selector
signature = f"{item['name']}({','.join(i['type'] for i in item['inputs'])})"
computed_selector = w3.keccak(text=signature)[:4]
if selector == computed_selector:
# Decode parameters
param_types = [i['type'] for i in item['inputs']]
param_names = [i['name'] for i in item['inputs']]
decoded = decode(param_types, tx_input[4:])
return {
'function': item['name'],
'params': dict(zip(param_names, decoded))
}
return {'function': 'unknown', 'params': {}}
Putting It All Together
Here's a complete script that decodes any transaction to a verified contract:
def decode_any_transaction(tx_hash):
tx = w3.eth.get_transaction(tx_hash)
if tx['input'] == b'' or tx['input'] == '0x':
return {
'type': 'ETH transfer',
'value': w3.from_wei(tx['value'], 'ether'),
'to': tx['to']
}
abi = get_contract_abi(tx['to'])
if not abi:
return {'type': 'contract call (unverified)', 'raw_input': tx['input'].hex()}
decoded = decode_transaction_input(tx['input'], abi)
return {
'type': 'contract interaction',
'contract': tx['to'],
**decoded
}
# Example usage
result = decode_any_transaction('0x1234...')
print(json.dumps(result, indent=2, default=str))
Going Further
This approach scales well. You can extend it with:
- Event log decoding — parse emitted events the same way
- Proxy contract resolution — follow EIP-1967 to find implementation ABIs
- Multi-chain support — works on any EVM chain with a compatible explorer API
The Code Is Open Source
All the code from this tutorial is available on GitHub: github.com/Byaigo
Support open-source development:
ETH: 0x18da907cb9d981bc798acb87ac27b03a2dc3cbb7
Happy decoding! 🚀
Top comments (0)