Contract data (EVM): selectors, calldata, logs, returndata, reverts, deploy addresses
Tiny offline stdin→stdout filters: sig-hash, abi-decode, abi-encode, create-address. No network client, no project, no config, no built-in chain list…
⚠️ DANGEROUS: use at your own risk. Code that handles keys and signs transactions should always be read and audited by you before you trust it with real funds. Used incorrectly, it can destroy your money.
Tiny offline stdin→stdout filters: sig-hash, abi-decode, abi-encode, create-address.
No network client, no project, no config, no built-in chain list and no signature database:
you fetch the JSON-RPC response yourself (any EVM chain, any RPC) and pipe it in.
Every tested example below runs on raw Ethereum mainnet JSON-RPC responses recorded in
fixtures/evm-mainnet/; the exact request (curl, URL, UTC time, block) for each
file is in fixtures/evm-mainnet/PROVENANCE.md. The live curl steps
are shown for your own RPC ($RPC) and are UNTESTED here: CI never touches the network.
(b) An unknown selector, confirmed with sig-hash
A revert came back as bare 0x5bf6f916 (fixture 04e-custom-error-eth_call.json, request
"04e" in PROVENANCE.md: eth_call of Universal Router execute(bytes,bytes[],uint256) with
deadline 1). sig-hash has no database, so you supply candidates, here the errors typed from the
router's source (fixtures/evm/candidate-errors.txt):
sig-hash --lookup "$(jq -r .error.data recipes/fixtures/evm-mainnet/04e-custom-error-eth_call.json)" --sigs recipes/fixtures/evm/candidate-errors.txt
Forward direction, to check a single guess:
sig-hash 'error TransactionDeadlinePassed()'
Live (UNTESTED here): curl -s -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x…","data":"0x…"},"latest"]}' "$RPC" | jq -r .error.data
(a) Transaction input → arguments
01a-erc20-transfer-tx.json is the eth_getTransactionByHash response for a USDC transfer (request "01a" in
PROVENANCE.md). Pull input out with jq and give abi-decode the signature:
jq -r .result.input recipes/fixtures/evm-mainnet/01a-erc20-transfer-tx.json | abi-decode --sig 'transfer(address,uint256)'
The whole response works too (result.input is found on its own). Without a matching signature it
answers no_match with the selector (exit 0; exit 2 is malformed input) and never guesses.
abi-decode --sig 'approve(address,uint256)' < recipes/fixtures/evm-mainnet/01a-erc20-transfer-tx.json
Live (UNTESTED here): curl -s -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionByHash","params":["0x…"]}' "$RPC" | jq -r .result.input | abi-decode --sig 'transfer(address,uint256)'
(c) Logs: eth_getLogs Transfer events
01c-usdc-transfer-getLogs.json (request "01c") is an eth_getLogs result; each log becomes one JSON line, with
indexed topics decoded:
abi-decode --sig 'event Transfer(address indexed from, address indexed to, uint256 value)' < recipes/fixtures/evm-mainnet/01c-usdc-transfer-getLogs.json
(d) Return data: balanceOf
03c-usdc-balanceOf.json (request "03c") is an eth_call result. Give the output types (USDC uses six decimals,
see 03a):
abi-decode --types uint256 < recipes/fixtures/evm-mainnet/03c-usdc-balanceOf.json
(e) Reverts: Error(string) and a custom error
Error(string) and Panic(uint256) are decoded with no signature. 04d (request "04d") is a
transferFrom without allowance:
jq .error recipes/fixtures/evm-mainnet/04d-error-string-eth_call.json | abi-decode
A custom error needs its declaration (from section (b)):
jq .error recipes/fixtures/evm-mainnet/04e-custom-error-eth_call.json | abi-decode --abi recipes/fixtures/evm/candidate-errors.txt
(g) Arguments → calldata, and back
abi-encode is the inverse of abi-decode. The Solidity ABI spec example baz(uint32,bool) with
69, true gives selector 0xcdcd77c0:
abi-encode 'baz(uint32,bool)' 69 true | jq -c '{signature,selector}'
Re-encoding the decoded arguments of 01a gives its input byte for byte:
abi-encode --raw 'transfer(address,uint256)' 0x264bd8291fae1d75db2c5f573b07faa6715997b5 5050510000 | abi-decode --sig 'transfer(address,uint256)'
Live (UNTESTED here): D=$(abi-encode --raw 'balanceOf(address)' 0x…); curl -s -X POST -H 'Content-Type: application/json' --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_call\",\"params\":[{\"to\":\"0x…\",\"data\":\"$D\"},\"latest\"]}" "$RPC" | abi-decode --sig 'balanceOf(address) returns (uint256)' --output
(f) Contract addresses: CREATE and CREATE2
CREATE: the address is keccak256(rlp([deployer, nonce])). 05a (request "05a") is a deployment tx;
create-address reads from and nonce from it, and the result matches contractAddress in its receipt 05b:
create-address < recipes/fixtures/evm-mainnet/05a-create-tx.json
CREATE2 (EIP-1014): keccak256(0xff ++ deployer ++ salt ++ keccak256(init_code)). 05c/05d (requests
"05c", "05d") create the Uniswap V3 USDT pool with token0 0x3246…8edb, token1 USDT, fee 100. The factory
salt is keccak256(abi.encode(token0, token1, fee)) and the init-code hash is the factory's fixed
POOL_INIT_CODE_HASH from Uniswap's PoolAddress.sol (not fetched). The result matches the pool address in
the PoolCreated log of 05d:
abi-encode --raw '(address,address,uint24)' 0x3246806c924fab68dd6db5d39b516ade72618edb 0xdac17f958d2ee523a2206206994597c13d831ec7 100 | sig-hash --keccak | jq -c '{deployer: "0x1F98431c8aD98523631AE4a59f267346ea31F984", salt: .keccak256, init_code_hash: "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"}' | create-address | jq -c '{scheme,address}'
jq -r '.result.logs[0].data' recipes/fixtures/evm-mainnet/05d-create2-pool-receipt.json | abi-decode --types '(int24,address)' | jq -r '.args[1].value' | tr A-F a-f
Live (UNTESTED here): curl -s -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionByHash","params":["0x…"]}' "$RPC" | create-address
(h) What an ABI implements, and selector collisions
recipes/fixtures/evm/erc20-plus-collision.txt is the EIP-20 function list plus gasprice_bit_ether(int128),
whose selector is also 0x23b872dd (transferFrom). abi-inspect reports the collision and that ERC-20 is complete:
abi-inspect recipes/fixtures/evm/erc20-plus-collision.txt | jq -c .selector_collisions
abi-inspect recipes/fixtures/evm/erc20-plus-collision.txt | jq -c '.interfaces[0]'
A compiler artifact ({"abi": [...]}) or a bare ABI array works the same. Live (UNTESTED here): an explorer's
verified-ABI download, e.g. curl -s "$EXPLORER_ABI_URL" | jq -r .result | abi-inspect.
(i) What is deployed: eth_getCode
06 (request "06") is the runtime code of a small mainnet contract. bytecode-inspect reads the response as-is;
the Solidity metadata trailer says solc 0.8.20, and the dispatcher's PUSH4/EQ constants are listed as
selector candidates (a heuristic: confirm each with sig-hash --lookup and a signature file you trust):
bytecode-inspect < recipes/fixtures/evm-mainnet/06-getCode-small.json | jq -c '{kind, size, exceeds_eip170, solc: .metadata.solc, selector_candidates}'
0x8da5cb5b is owner():
sig-hash 'owner()'
An EIP-1167 minimal proxy (the reference code from the EIP) gives its implementation:
bytecode-inspect 0x363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 | jq -c '{kind, implementation}'
Live (UNTESTED here): curl -s -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0x…","latest"]}' "$RPC" | bytecode-inspect
(j) What a typed-data signature commits to: EIP-712
eip712-mail-signed.json is the Mail example from the EIP-712 specification with its published signature.
typed-data-verify prints the domain separator, message hash and digest (compare them with what a wallet
shows) and recovers the signer:
typed-data-verify recipes/fixtures/evm/eip712-mail-signed.json | jq -c '{primary_type, chain_id, recovered, status}'
A different expected signer is a mismatch (exit 1):
typed-data-verify --signer 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB recipes/fixtures/evm/eip712-mail-signed.json
An eth_signTypedData_v4 request ({"params": [address, typedData]}) can be piped in as-is with --sig;
its address becomes the expected signer. Live (UNTESTED here): capture the request a dapp sends and the
signature the wallet returns, then typed-data-verify --sig 0x… request.json.