A terminal tool for decoding Ethereum calldata. Paste any hex blob and get back a navigable tree of what it actually does — including everything nested inside it: multicalls, Safe batches, ERC-4337 user operations, router swap plans, all the way down.
Built with Bun, TypeScript, viem and OpenTUI.
bun install
bun startThat opens the TUI: paste calldata, hit enter, and use the arrow keys to walk the tree. Branches collapse and expand, the mouse wheel scrolls, and the optional target field adds token symbols and dollar values.
There's also a one-shot mode for scripts and pipes:
bun start 0x82ad56cb000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000008fb952916d96b4f4467d5bf4f33b6807803e357900000000000000000000000000000000000000000000000000000000017d784000000000000000000000000000000000000000000000000000000000…which prints:
aggregate3((address target, bool allowFailure, bytes callData)[] calls) [bundled ✓]
selector 0x82ad56cb · 324 bytes
└─ calls tuple[]
└─ [0] (address,bool,bytes)
├─ target address = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 · USDC (mainnet)
├─ allowFailure bool = false
└─ callData bytes = 0xa9059cbb…017d7840 · 68 bytes
↳ decoded:
transfer(address to, uint256 amount) [bundled ✓]
selector 0xa9059cbb · 68 bytes · to USDC · USD Coin (0x…3606eB48)
├─ to address = 0x8fb952916D96b4f4467D5BF4F33b6807803E3579
└─ amount uint256 = 25000000 · 25 USDC ($24.99)
A Multicall3 batch, opened up to reveal a USDC transfer — with the token resolved
on-chain and priced in dollars. Input is forgiving: the 0x prefix is optional, and
whitespace, newlines, quotes, and stray JSON commas are all stripped before decoding.
| key | action |
|---|---|
tab |
cycle focus: calldata input → target input → tree |
↑↓ / jk |
move selection |
enter / space |
collapse / expand the selected branch |
← / → |
collapse (or jump to parent) / expand |
e / c |
expand all / collapse all |
pgup/pgdn, home/end, g/G |
paging and jumps |
| mouse wheel / click | scroll the tree, select a row |
i |
back to the calldata input |
q / esc |
quit |
The target field takes 0xaddress:chainId (or just :chainId) and tells the decoder
which contract the calldata is sent to, which is what enables token symbols, decimals,
and USD values for the top-level call. Nested calls work their targets out on their own
from the surrounding data — a Safe's to, a multicall's target, a UserOp's sender.
| flag | effect |
|---|---|
--json |
print the decode tree as JSON |
--full |
never truncate long hex values |
--no-color |
plain output |
--no-enrich |
skip token metadata + USD lookups |
--address 0x… --chain 1 |
target contract: verified ABI (Sourcify/Etherscan) + token context |
For every call in the tree — top-level and nested — candidate signatures are tried in order and the best match wins, where "best" strongly favours candidates whose decoded arguments re-encode to exactly the input bytes:
- Verified ABI from Sourcify (and Etherscan, if
ETHERSCAN_API_KEYis set) when you pass--address - Bundled signatures for the contracts you actually run into: ERC-20/721/1155/4626, WETH, Uniswap V2/V3/V4/Universal Router, Permit2, Safe, ERC-4337 EntryPoints v0.6–v0.8, smart accounts, Multicall3, 1inch, 0x, Sushi, Odos, and friends — these carry parameter names, so the output reads well
- openchain.xyz (spam-filtered)
- 4byte.directory (oldest entry first — newer duplicates are usually squatters)
- Shape guessing — when nothing matches, parameter types are inferred from the raw ABI encoding itself
- Raw word dump as the honest last resort
Any bytes value that looks like calldata gets the same treatment recursively, with
guards against signature-database spam (non-word-aligned payloads only decode against
curated signatures). Data a signature didn't consume — wallet attribution suffixes,
appended junk — is surfaced as a (trailing data) node rather than silently dropped.
Decoding has been stress-tested against live traffic: ERC-4337 bundles, Safe batches, MetaMask delegation redemptions, CoW settlements, 114KB Seaport bulk orders, and every EVM swap source Relay.link routes through (Uniswap, PancakeSwap, KyberSwap, OpenOcean, Odos, 0x, Sushi…).
After decoding, the tree is annotated best-effort:
- Token metadata —
symbol/decimals/nameread on-chain over public RPCs, for call targets and anything named like a token (tokenIn,srcToken,asset…), including both ends of packed swap paths - USD values — spot prices from the open Relay.link API,
applied wherever an amount can be paired with a token, including native-value
transfers and
WRAP_ETHamounts - Anything that fails — dead RPC, unlisted token, unknown chain — just stays un-annotated
- ERC-4337 — packed v0.7 fields (
initCode,paymasterAndData,accountGasLimits,gasFees) are split into their real components; UserOpcallDatadescends through the account'sexecute/executeBatch - Safe —
execTransactionpayloads and packedmultiSendbatches - Uniswap Universal Router — command bytes become a named plan (
V3_SWAP_EXACT_IN,PERMIT2_PERMIT,WRAP_ETH…), V4 action bytes are named, and packed V3 paths render asWETH –0.05%→ USDC - Value hints — checksummed addresses with well-known labels,
max uint256, unix-timestamp deadlines
bun run lint # eslint — typescript-eslint + jsdoc required on every function
bun run typecheck # tsc --noEmit
bun run test # vitest, fully offline (the network is mocked)
bun run test:coverage # coverage report; 100% thresholds are enforcedUnit coverage is held at 100% lines / statements / branches / functions for the decoder library and tree model. The interactive view and process entry are covered by a ConPTY end-to-end test instead (scripts/drive-tui.py), which drives the real TUI: paste → decode → navigate → collapse → quit.
The scripts/ directory also has the stress-testing tools: relay-hunt.ts and
wild-hunt.ts pull complex live calldata (from Relay's API and from raw blocks),
partner-hunt.ts targets Relay's swap-source routers specifically, and
relay-eval.ts <manifest> [--verbose] decodes a sample set and reports how much of
each tree resolved.
CI runs lint + typecheck + coverage on every push (ci.yml), the ConPTY e2e on Windows runners (e2e.yml), and publishing a GitHub release builds standalone binaries for Linux/macOS/Windows (release.yml).
Bun ≥ 1.3 to run; Node.js ≥ 24 for the dev toolchain (vitest/eslint).
