txlens

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: MIT

README

txlens

ci release Go Reference Go Report Card license

Self-hosted, API-key-free pre-sign checks for Ethereum transactions.

Before a transaction is signed, txlens answers what will this actually do? It decodes the calldata (recursively, through multicalls, Safe wrappers and the Uniswap Universal Router), simulates the call against any JSON-RPC endpoint, lists the asset transfers and approval changes it would cause, decodes the revert reason if it fails, and raises a small set of high-signal risk flags. It also inspects mined transactions the same way.

$ txlens simulate --rpc $RPC --from 0xF977…aceC --to 0xdAC1…1ec7 --data 0xa9059cbb…

Calldata
  transfer(address,uint256)  [sourcify]
    _to address    = 0x28C6c06298d514Db089934071355E5743bf21d60
    _value uint256 = 1000000

Result: SUCCESS   gas estimate: 46479   trace gas used: 46097

Asset changes (from trace)
  0x28C6c06298d514Db089934071355E5743bf21d60  +1.0 USDT
  0xF977814e90dA44bFA03b6295A0616a897441aceC  -1.0 USDT

Single static binary, Go, one dependency (go-ethereum for ABI/RLP/crypto). Works against any endpoint; gets richer (call tree, asset changes, state diff) when the endpoint exposes debug_traceCall.

Why

  • Tenderly's simulation API is not on the free tier; Blocknative/Alchemy/etc. need keys and send your unsigned transactions to a third party. A wallet, an agent framework or a CI job should be able to run this check against its own node, or any public one.
  • Agents that send transactions need a guardrail that is a process, not a prompt: txlens simulate --json --fail-on-risk returns a structured report and a non-zero exit code when the transaction reverts or grants an unlimited approval.
  • cast decodes and calls, but does not tie calldata → trace → asset changes → approvals → warnings together, and has no selector-database / proxy-aware ABI resolution.

Install

go install github.com/meldrov/txlens/cmd/txlens@latest
# or
git clone https://github.com/meldrov/txlens && cd txlens && make build   # → bin/txlens

Go 1.26+. Prebuilt static binaries for Linux, macOS and Windows (amd64/arm64) are on the Releases page, with a checksums.txt to verify downloads.

Quickstart

All outputs below are real mainnet runs (2026-08-21) against https://eth.drpc.org, trimmed for width. https://eth.merkle.io also exposes debug_* without a key; publicnode, 1rpc, cloudflare-eth, flashbots and nodies do not (txlens then falls back to eth_call only and says so).

export TXLENS_RPC=https://eth.drpc.org    # or pass --rpc on every command
decode — what does this calldata say?

Universal Router input of a real swap (0x9fad0837…), ABI from Sourcify, commands and per-command inputs decoded, V3 path rendered:

$ txlens decode --to 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af --data 0x3593564c…

contract: 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af  (UniversalRouter, verified)

execute(bytes,bytes[],uint256)  [sourcify]
  commands bytes   = 0x0a00
                     ↳ PERMIT2_PERMIT, V3_SWAP_EXACT_IN
  inputs bytes[] (2 items)
    [0] PERMIT2_PERMIT bytes   = 0x0000000…0000000 (352 bytes)
        → PERMIT2_PERMIT(((address token, uint160 amount, uint48 expiration, uint48 nonce) details, address spender, uint256 sigDeadline) permitSingle, bytes signature)  [builtin]
            permitSingle ((address,uint160,uint48,uint48),address,uint256)
              details (address,uint160,uint48,uint48)
                token address     = 0xdAC17F958D2ee523a2206206994597C13D831ec7
                amount uint160    = 1461501637330902918203684832716283019655932542975
                expiration uint48 = 1789878407
                nonce uint48      = 512
              spender address     = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af
              sigDeadline uint256 = 1787286527
            signature bytes = 0x1364b6644f2f…c85c551b (65 bytes)
    [1] V3_SWAP_EXACT_IN bytes = 0x0000000…0000000 (256 bytes)
        → V3_SWAP_EXACT_IN(address recipient, uint256 amountIn, uint256 amountOutMin, bytes path, bool payerIsUser)  [builtin]
            recipient address    = 0x0000000000000000000000000000000000000001
                                   ↳ MSG_SENDER
            amountIn uint256     = 169052246
            amountOutMin uint256 = 605290434415167272469
            path bytes           = 0xdac17f958d2ee523a2206206994597c13d831ec7000bb89d70bae2944ffa477f37bae227fd981e6eb31982
                                   ↳ 0xdAC17F958D2ee523a2206206994597C13D831ec7 -(0.3%)-> 0x9d70baE2944Ffa477F37Bae227fd981E6eB31982
            payerIsUser bool     = true
                                   ↳ tokens pulled from msg.sender via Permit2
  deadline uint256 = 1787286527

SwapRouter02 exactInputSingle (struct parameter), tx 0x55d9f117…:

$ txlens decode --to 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 --data 0x04e45aaf…

contract: 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45  (SwapRouter02, verified)

exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))  [sourcify]
  params (address,address,uint24,address,uint256,uint256,uint160)
    tokenIn address           = 0x100acD9FcD8E0FF80A6595B66fdABe93184Aa100
    tokenOut address          = 0xdAC17F958D2ee523a2206206994597C13D831ec7
    fee uint24                = 100
    recipient address         = 0xe0eD57c18f9eEacd41A1113E41175237f7AB7Cb7
    amountIn uint256          = 8268337
    amountOutMinimum uint256  = 8793016
    sqrtPriceLimitX96 uint160 = 0

Offline, no --to: built-in signatures still cover the common standards.

$ txlens decode --offline --data 0xa9059cbb000…0f4240
transfer(address,uint256)  [builtin]
  to address     = 0x28C6c06298d514Db089934071355E5743bf21d60
  amount uint256 = 1000000
simulate — what will it do?

USDT transfer from the Binance hot wallet:

$ txlens simulate --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
    --to 0xdAC17F958D2ee523a2206206994597C13D831ec7 \
    --data 0xa9059cbb00000000000000000000000028c6c06298d514db089934071355e5743bf21d6000000000000000000000000000000000000000000000000000000000000f4240

Request
  from:   0xF977814e90dA44bFA03b6295A0616a897441aceC
  to:     0xdAC17F958D2ee523a2206206994597C13D831ec7
  value:  0 ETH
  data:   0xa9059cbb000000…0f4240 (68 bytes)
  block:  latest (chain 1)
  contract: 0xdAC17F958D2ee523a2206206994597C13D831ec7  (TetherToken, verified)

Calldata
  transfer(address,uint256)  [sourcify]
    _to address    = 0x28C6c06298d514Db089934071355E5743bf21d60
    _value uint256 = 1000000

Result: SUCCESS   gas estimate: 46479   trace gas used: 46097

Asset changes (from trace)
  0x28C6c06298d514Db089934071355E5743bf21d60  +1.0 USDT
  0xF977814e90dA44bFA03b6295A0616a897441aceC  -1.0 USDT
  transfers:
    0xF977…aceC → 0x28C6…1d60  1.0 USDT

Call trace (1 frames)
  CALL 0xdAC17F958D2ee523a2206206994597C13D831ec7  transfer(address,uint256)  gas=46097
    log Transfer(from=0xF977814e90dA44bFA03b6295A0616a897441aceC, to=0x28C6c06298d514Db089934071355E5743bf21d60, value=1000000)

Revert: transferring more USDC than the balance. The proxy (ZeppelinOS slot) is detected, the implementation ABI is merged, and the Error(string) comes out of the DELEGATECALL frame. Exit code 1.

$ txlens simulate --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
    --to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --data 0xa9059cbb…0c9f2c9cd04674edea40000000

  contract: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48  (FiatTokenProxy, verified)
    proxy (zeppelinos) → 0x43506849D7C04F9138D1A2050bbF3A0c054402dd  (FiatTokenV2_2, verified)

Calldata
  transfer(address,uint256)  [sourcify]
    to address    = 0x28C6c06298d514Db089934071355E5743bf21d60
    value uint256 = 1000000000000000000000000000000

Result: REVERTED   trace gas used: 36349
  reason: Error("ERC20: transfer amount exceeds balance")

Asset changes (from trace): none

Call trace (2 frames)
  CALL 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48  transfer(address,uint256)  gas=36349  ✗ execution reverted: Error("ERC20: transfer amount exceeds balance")
    DELEGATECALL 0x43506849D7C04F9138D1A2050bbF3A0c054402dd  transfer(address,uint256)  gas=7361  ✗ execution reverted: Error("ERC20: transfer amount exceeds balance")

The same over-balance transfer on USDT (Solidity 0.4, assert-style) fails with an INVALID opcode and no data; txlens reports that instead of guessing:

Result: REVERTED   trace gas used: 600000000
  reason: reverted without data (invalid opcode: INVALID)

Custom errors resolve through the target's ABI. Re-running a mined Universal Router transaction from its signed raw bytes after its deadline:

$ txlens simulate --raw 0x02f90413…

Result: REVERTED   trace gas used: 36740
  reason: TransactionDeadlinePassed()
    error TransactionDeadlinePassed() resolved via sourcify
  data:   0x5bf6f916

Unlimited approval → warning, and exit code 3 with --fail-on-risk:

$ txlens simulate --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
    --to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
    --data 0x095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
    --fail-on-risk

Calldata
  approve(address,uint256)  [sourcify]
    spender address = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
    value uint256   = 115792089237316195423570985008687907853269984665640564039457584007913129639935

Result: SUCCESS   gas estimate: 56711   trace gas used: 55906
  return: 0x0000000000000000000000000000000000000000000000000000000000000001

Asset changes (from trace)
  approvals:
    USDC: 0xF977…aceC approves 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 for UNLIMITED (⚠)

Call trace (2 frames)
  CALL 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48  approve(address,uint256)  gas=55906
    DELEGATECALL 0x43506849D7C04F9138D1A2050bbF3A0c054402dd  approve(address,uint256)  gas=26673
      log Approval(owner=0xF977…aceC, spender=0x68b3…Fc45, value=1157920892373161954235709850086879078532699846656405640394575840079131296399
35)

WARNINGS
  [warning] unlimited-approval: USDC (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48): approve 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 for UNLIMITED — the spender can move the owner's entire balance at any time
$ echo $?
3

State overrides: fund an address that has nothing and wrap ETH.

$ txlens simulate --from 0x000000000000000000000000000000000000dEaD \
    --to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --value "1 ether" --data 0xd0e30db0 \
    --override-balance 0x000000000000000000000000000000000000dEaD="10 ether"

Result: SUCCESS   gas estimate: 28284   trace gas used: 27938

Asset changes (from trace)
  0x000000000000000000000000000000000000dEaD  -1.0 ETH, +1.0 WETH
  0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2  +1.0 ETH
  transfers:
    0x0000…dEaD → 0xC02a…6Cc2  1.0 ETH
    0x0000…0000 → 0x0000…dEaD  1.0 WETH

--state-diff adds the prestateTracer diff (storage slots, balances, nonces):

State diff (prestateTracer)
  0xF977814e90dA44bFA03b6295A0616a897441aceC
    nonce: 20302 → 20303
  0xdAC17F958D2ee523a2206206994597C13D831ec7
    storage[0x0be16d71…405f36]: 0x…0041faf2403f74d7 → 0x…0041faf240303297
    storage[0x78b35599…1436e0]: 0x…00041a22d373ed6d → 0x…00041a22d3832fad

Endpoint without the debug namespace (here ethereum-rpc.publicnode.com):

Result: SUCCESS   gas estimate: 46479

Call trace: unavailable — provider lacks debug_traceCall (the method debug_traceCall does not exist/is not available); asset changes and the call tree need an endpoint with the debug namespace
tx — what did a mined transaction do?
$ txlens tx 0x9fad0837a86f1e8c59e0c1fca03b8a03e35705808fbffda6a7bd639721540192

Transaction 0x9fad0837a86f1e8c59e0c1fca03b8a03e35705808fbffda6a7bd639721540192
  status: SUCCESS
  block:  25801109 (index 339)
  from:   0x56d2f51A6e15B6d1CfE89698Dc000c41dbD4F0A9
  to:     0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af
  value:  0 ETH
  nonce:  1043   type: 2
  gas:    157805 used / 211439 limit   @ 0.093064677 gwei   fee 0.000014686071353985 ETH
  contract: 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af  (UniversalRouter, verified)

Calldata
  execute(bytes,bytes[],uint256)  [sourcify]
    commands bytes   = 0x0a00
                       ↳ PERMIT2_PERMIT, V3_SWAP_EXACT_IN
    … (as in the decode example above)

Logs (4)
  [389] 0x0000…8BA3  Permit(owner=0x56d2…F0A9, token=0xdAC1…1ec7, spender=0x66a9…8Af, amount=1461501637330902918203684832716283019655932542975, expiration=1789878407, nonce=512)
  [390] 0x9d70…1982  Transfer(from=0xfAf4…8384, to=0x56d2…F0A9, value=608332094889615349216)
  [391] 0xdAC1…1ec7  Transfer(from=0x56d2…F0A9, to=0xfAf4…8384, value=169052246)
  [392] 0xfAf4…8384  Swap(sender=0x66a9…8Af, recipient=0x56d2…F0A9, amount0=-608332094889615349216, amount1=169052246, sqrtPriceX96=41715199519853159017144, liquidity=547256676836576503, tick=-289154)

Asset changes (from trace)
  0x56d2f51A6e15B6d1CfE89698Dc000c41dbD4F0A9  +608.332094889615349216 AYNI, -169.052246 USDT
  0xfAf41F3761EB08374639955BDE44CBbF3dcC8384  -608.332094889615349216 AYNI, +169.052246 USDT
  transfers:
    0xfAf4…8384 → 0x56d2…F0A9  608.332094889615349216 AYNI
    0x56d2…F0A9 → 0xfAf4…8384  169.052246 USDT

Call trace (11 frames)
  CALL 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af  execute(bytes,bytes[],uint256)  gas=157805
    CALL 0x000000000022D473030F116dDEE9F6B43aC78BA3  permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)  gas=13686
      log Permit(owner=0x56d2…F0A9, token=0xdAC1…1ec7, spender=0x66a9…8Af, …)
      STATICCALL 0x0000000000000000000000000000000000000001  0x72ec0f95?  gas=3000
    CALL 0xfAf41F3761EB08374639955BDE44CBbF3dcC8384  swap(address,bool,int256,uint160,bytes)  gas=105977
      log Swap(…)
      CALL 0x9d70baE2944Ffa477F37Bae227fd981E6eB31982  transfer(address,uint256)  gas=22733
        DELEGATECALL 0x4A13A71d29b4e678497efD4D230c5e324CB9e5c7  transfer(address,uint256)  gas=17837
          log Transfer(from=0xfAf4…8384, to=0x56d2…F0A9, value=608332094889615349216)
      STATICCALL 0xdAC17F958D2ee523a2206206994597C13D831ec7  balanceOf(address)  gas=5031
      CALL 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af  uniswapV3SwapCallback(int256,int256,bytes)  gas=26253
        CALL 0x000000000022D473030F116dDEE9F6B43aC78BA3  transferFrom(address,address,uint160,address)  gas=23737
          CALL 0xdAC17F958D2ee523a2206206994597C13D831ec7  transferFrom(address,address,uint256)  gas=22530
            log Transfer(from=0x56d2…F0A9, to=0xfAf4…8384, value=169052246)
      STATICCALL 0xdAC17F958D2ee523a2206206994597C13D831ec7  balanceOf(address)  gas=1031

WARNINGS
  [info] permit-signature: calldata carries permit-style signature(s): PERMIT2_PERMIT — an off-chain signature grants a token allowance; check signer, spender, amount and deadline

txlens simulate --tx-hash 0x9fad08… re-runs the same transaction at its parent block and produces the same asset changes (with a note that intra-block ordering is not reproduced).

Commands and flags

Common flags (every subcommand): --rpc URL (or TXLENS_RPC), --chain-id N (when no RPC; default 1), --abi file.json (repeatable; bare ABI arrays and Foundry/Hardhat artifacts), --json, --no-cache, --cache-dir DIR, --offline (no Sourcify/selector-DB), --timeout 60s.

txlens decode
txlens decode --data 0x… [--to 0xADDR] [--depth 5]

--to enables Sourcify lookup (and, with --rpc, on-chain proxy detection). Without it only local ABIs, built-ins and the selector databases are used.

txlens simulate
txlens simulate --from 0x… --to 0x… [--value WEI|"0.1 ether"|"5 gwei"] [--data 0x…] [--gas N]
                [--block latest|N] [--override-balance ADDR=WEI]… [--override-code ADDR=0x…]…
                [--state-diff] [--no-trace] [--fail-on-risk]
txlens simulate --tx-hash 0x…     # mined tx, replayed at its parent block (fee fields dropped)
txlens simulate --raw 0x…         # signed raw tx (any type); sender recovered from the signature

Steps: eth_estimateGaseth_call (state overrides attached when given) → if the endpoint answers debug_traceCall (probed once): callTracer (withLog) for the call tree and asset changes, prestateTracer (diffMode) with --state-diff.

Exit codes: 0 success, 1 the call reverts, 2 usage / RPC error, 3 warning-level risk flags with --fail-on-risk.

txlens tx
txlens tx 0xHASH [--no-trace]

Receipt, decoded input, decoded logs, debug_traceTransaction call tree and asset changes. Without the debug namespace, asset changes come from receipt logs (native value flows inside the call tree are then invisible) and the revert reason of a failed transaction is recovered by replaying eth_call at the parent block. Exit code 1 if the transaction reverted.

JSON output

--json prints one object. schema is 1 and is bumped on incompatible changes. Abbreviated shapes (full structs in internal/render/types.go):

// txlens decode --json
{
  "schema": 1,
  "contract": {"address","name","verified","has_code","proxy":{"kind","implementation","implementation_name","implementation_verified"}},
  "call": Call
}

// Call (recursive)
{
  "to": "0x…",                      // target the calldata was decoded against (checksummed)
  "selector": "0xa9059cbb",
  "name": "transfer", "signature": "transfer(address,uint256)",
  "source": "local|sourcify|builtin|openchain|4byte",
  "exact": true,                    // re-encoding the decoded params reproduces the calldata
  "params": [Param], "alternatives": ["otherSig(...)"], "raw": "0x…", "error": "unknown selector"
}
// Param
{"name","type","value",             // value: checksummed address / decimal integer / 0x-hex / "true" / string
 "components":[Param],              // tuple fields or array elements
 "decoded": Call,                   // nested calldata
 "note": "MSG_SENDER"}              // human annotation (UR commands, V3 path, Safe operation…)

// txlens simulate --json
{
  "schema": 1,
  "request": {"from","to","value","data","gas","block","chain_id","tx_hash"},
  "contract": {...}, "decoded": Call,
  "gas_estimate": 46479, "gas_estimate_error": "…",
  "success": true, "return_data": "0x…",
  "revert": {"kind":"none|error|panic|custom|raw","message","panic_code","panic_name","selector","name","signature","source","params":[Param],"raw","alternatives"},
  "trace": {"available":true,"reason":"…","tracer":"callTracer","gas_used":46097,"frame_count":1,
            "root": {"type","depth","from","to","value","gas","gas_used","selector","function","source","decoded":Call,"input","output","error","revert":{…},
                     "logs":[{"address","name","signature","source","params":[Param],"topics":[],"data"}],"calls":[…]}},
  "assets": {"source":"trace|logs",
             "transfers":[{"kind":"native|erc20|erc721|erc1155","token","symbol","from","to","amount","token_id","formatted","origin"}],
             "approvals":[{"kind","token","symbol","owner","spender","amount","token_id","for_all","approved","unlimited","formatted"}],
             "net":[{"address","deltas":[{"kind","token","symbol","token_id","amount","formatted"}]}]},
  "state_diff": {"pre":{addr:{balance,nonce,code,storage}}, "post":{…}},   // with --state-diff
  "risks": [{"code","severity":"warning|info","message"}],
  "notes": ["…"],
  "exit_code": 0
}

// txlens tx --json
{
  "schema": 1, "hash","block","index","from","to","contract_created","nonce","type","value",
  "gas_limit","gas_used","effective_gas_price","fee","status":"success|reverted|pending",
  "contract", "decoded", "logs":[LogView], "revert", "trace", "assets", "risks", "notes"
}

Amounts (amount, value, deltas) are decimal strings in base units; formatted applies token decimals when known.

How ABI resolution works

For each selector, candidates are gathered in this order and the first one whose decoding re-encodes to exactly the calldata wins (falling back to the first that unpacks at all):

  1. Local --abi files.
  2. Sourcify v2GET /v2/contract/{chainId}/{address}?fields=abi,compilation,proxyResolution for the target. With --rpc, proxies are resolved on-chain first: EIP-1967 implementation slot, EIP-1967 beacon slot (→ implementation()), ZeppelinOS legacy slot, EIP-1167 minimal-proxy bytecode, and EIP-897 implementation() on the proxy itself; Sourcify's proxyResolution is the fallback. The implementation's ABI is merged into the target's.
  3. Built-in signatures: ERC-20/721/1155, WETH, Multicall/Multicall3, Safe, Universal Router, Uniswap V2/V3 routers, Permit2, common proxy functions, the standard token events, and OpenZeppelin 5 / Permit2 / Universal Router custom errors.
  4. openchain (/signature-database/v1/lookup?function=…&filter=true), then 4byte.directory only if openchain has nothing (4byte entries sorted by id, oldest first — far fewer spam collisions). Signatures whose real selector does not match are discarded. Candidates from these sources are marked inexact in text output when the re-encoding check fails, and alternatives lists the other matching signatures.

Events use the same chain (contract ABI → built-ins → openchain/4byte). For signature-only events the indexed layout is unknown, so txlens tries layouts (preferring indexed address parameters) until topics and data decode exactly. Custom errors: contract ABI of the callee and of the deepest reverting frame → built-ins → openchain (which indexes error selectors in the function namespace).

Everything network-derived is cached under $XDG_CACHE_HOME/txlens or ~/.cache/txlens (Sourcify hits forever, misses 6 h; selector lookups 30 d, misses 24 h; token metadata 30 d). --no-cache bypasses it, --offline disables network lookups.

How asset changes are derived

With a callTracer tree (debug_traceCall / debug_traceTransaction with withLog: true):

  • Native: value of every CALL, CALLCODE, CREATE/CREATE2 and SELFDESTRUCT frame (not DELEGATECALL/STATICCALL), from from to to.
  • ERC-20: Transfer(address,address,uint256) with 3 topics (value in data), Approval with 3 topics; WETH-style Deposit/Withdrawal as mint/burn.
  • ERC-721: Transfer with 4 topics (tokenId indexed), Approval with 4 topics, ApprovalForAll.
  • ERC-1155: TransferSingle, TransferBatch.
  • Frames that reverted — and everything beneath them — are skipped, since their effects were rolled back.

Transfers are netted per address and asset; the zero address (mint/burn counterparty) is omitted from the net view. Tokens are enriched with symbol()/decimals()/name() via eth_call (bytes32 symbols such as MKR are handled), cached.

Without a trace (tx on a non-debug endpoint) the same classification runs on receipt logs, plus the transaction's own value.

Risk flags

code severity raised when why it matters
unlimited-approval warning ERC-20 Approval ≥ 2^96−1 (covers MaxUint256, Permit2's uint160 max, uint96 max) spender can drain the whole balance at any later time
approval-for-all warning ApprovalForAll(…, true) operator may move every token of the collection
approval-to-eoa warning approval whose spender has no code approvals are granted to contracts; an EOA spender is a classic drainer pattern
calldata-to-eoa warning to has no code but calldata is non-empty the call "succeeds" and does nothing — wrong address or undeployed contract
value-to-reverting-call warning native value attached and the call reverts nothing moves, gas is burnt; usually a construction bug
unverified-contract info to has code but no Sourcify ABI decoding relies on selector databases; read the trace, not the names
permit-signature info permit-style function or a PERMIT2_* Universal Router command in the calldata an off-chain signature grants allowance; verify signer/spender/amount/deadline
delegatecall-unexpected info to delegatecalls an address that is not its detected implementation callee runs with the target's storage and balance

--fail-on-risk turns warning-level flags into exit code 3. The set is deliberately small; it is meant as a guardrail, not a scanner.

Limitations

  • No prices / USD values, no cross-chain context; one endpoint, one chain.
  • Call tree, asset changes and state diff need debug_traceCall / debug_traceTransaction. Without it you get eth_call, gas estimate, decoded calldata and revert reason; tx falls back to receipt logs.
  • Selector databases collide and contain junk; txlens ranks by re-encoding fidelity and source trust but can still name a function wrong. Prefer a verified ABI (--to + Sourcify) or --abi.
  • Re-simulation (--tx-hash, --raw) runs at the parent block's end state; transactions earlier in the same block are not replayed, so results can differ from the mined outcome. Fee fields are dropped.
  • Universal Router command table is the v2 layout (v1.x NFT commands kept where byte values do not collide); V4 actions are named but not decoded further.
  • Internal (trace-level) value flows of a tx are only visible with the debug namespace; fee-on-transfer and rebasing tokens are reported by their events, not by balance diff.
  • Token metadata is fetched at latest, not at the simulated block.
  • Public endpoints rate-limit; txlens retries transient errors a few times and caches aggressively, but large traces against free endpoints can still fail.

Roadmap

  • MCP server exposing decode / simulate as tools for agent pre-sign checks.
  • Library API (pkg/txlens) with the same report structs.
  • EIP-712 typed-data explainer (Permit, Permit2, Safe, Seaport, …).
  • Batch / CI mode: many transactions from a file, aggregated exit code.
  • Safe transaction service integration: fetch a pending multisig tx by hash and simulate it as the Safe.
  • Balance-diff based asset changes (prestateTracer) to catch non-standard tokens.

Development

make build            # bin/txlens with version ldflags
make test             # offline unit tests (httptest fakes for Sourcify/openchain/4byte)
make test-integration # TXLENS_INTEGRATION=1, hits TXLENS_RPC (default https://eth.drpc.org)
make lint             # gofmt + go vet

Layout: cmd/txlens (stdlib flag dispatcher), internal/abiresolve (local/Sourcify/selector-DB/proxy, cache), internal/decode (calldata → tree, wrappers), internal/sim (RPC, overrides, tracers), internal/assets (events/value flows → asset changes, metadata), internal/revert, internal/risk, internal/render (report structs, text, JSON). examples/ has real callTracer fixtures and commands.

Contributing

Issues and PRs welcome. Keep the dependency list at go-ethereum + stdlib, run make lint test before pushing, and add a fixture or an httptest-backed test for anything that talks to the network.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
txlens command
Command txlens decodes, simulates and inspects Ethereum transactions against any JSON-RPC endpoint.
Command txlens decodes, simulates and inspects Ethereum transactions against any JSON-RPC endpoint.
internal
abiresolve
Package abiresolve locates ABI fragments for contracts and selectors.
Package abiresolve locates ABI fragments for contracts and selectors.
assets
Package assets derives asset transfers and approval changes from call traces and logs, nets them per address, and enriches tokens with metadata.
Package assets derives asset transfers and approval changes from call traces and logs, nets them per address, and enriches tokens with metadata.
decode
Package decode turns calldata into a tree of named parameters, recursing into nested calldata carried by multicalls, Safe transactions, the Uniswap Universal Router and any bytes argument that looks like a call.
Package decode turns calldata into a tree of named parameters, recursing into nested calldata carried by multicalls, Safe transactions, the Uniswap Universal Router and any bytes argument that looks like a call.
render
Package render holds the report structures produced by the txlens subcommands and renders them as text or JSON.
Package render holds the report structures produced by the txlens subcommands and renders them as text or JSON.
revert
Package revert decodes EVM revert payloads: Error(string), Panic(uint256), custom errors resolved through an ABI lookup, and raw data.
Package revert decodes EVM revert payloads: Error(string), Panic(uint256), custom errors resolved through an ABI lookup, and raw data.
risk
Package risk evaluates a small, high-signal set of pre-sign warnings.
Package risk evaluates a small, high-signal set of pre-sign warnings.
sim
Package sim talks to a JSON-RPC endpoint: eth_call / eth_estimateGas with state overrides, debug_traceCall / debug_traceTransaction with the callTracer and prestateTracer, and the handful of read calls the rest of txlens needs.
Package sim talks to a JSON-RPC endpoint: eth_call / eth_estimateGas with state overrides, debug_traceCall / debug_traceTransaction with the callTracer and prestateTracer, and the handful of read calls the rest of txlens needs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL