SwapChan
Never holds your keys · your wallet signs
blocktools CLI

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.