ethereum-track
track
Crystal CLI that lists the ETH and ERC-20 transactions (with amounts) for an Ethereum address.
$ crystal build src/track.cr -o bin/track
$ bin/track 0x033958c4d407160Ac2a2ffcA6621E4669DA9E6A3
Address : 0x033958c4d407160Ac2a2ffcA6621E4669DA9E6A3
Source : Explorer https://eth.blockscout.com/api
Balance : 0.0000021191270058 ETH
== NATIVE ETH (45 entries) ==
in native 0.043 ETH 0x8bda…3fbe -> 0x0339…e6a3 2026-05-16
self native 0 ETH 0x0339…e6a3 -> 0x0339…e6a3 2026-05-16
out native 0.068 ETH 0x0339…e6a3 -> 0x1481…db70 2026-06-12
...
== ERC-20 (N entries) ==
in erc20 1 DFRD 0x3c47…3e42 -> 0x0339…e6a3 2026-09-10
in erc20 0.998893 USDT 0xacdb…6d07 -> 0x0339…e6a3 2026-05-16
...
Subcommands
bin/track snapshot ADDRESS [--token=…] [--amount=N] [--rpc=URL] [--out=DIR]
bin/track merkle [--snapshot=FILE] [--out=DIR]
snapshot probes every distinct out-recipient of an address for its balance of a token at a fixed block and writes snapshots/<block>.json (--out changes the directory). Holders are excluded from eligible; so are contract recipients, which are dropped entirely — a token sent to a contract cannot be claimed and is burned. EOA-only is the default (pass --eoas-only=false to keep contract recipients, which is only useful for inspection). Note candidates holds every probed recipient (including holders), so it is larger than eligible.
merkle turns the eligible recipients into a sorted merkle tree and writes the root plus per-recipient proofs (merkle/<root>.json). See AIRDROP.md for the full flow, including the DFRDAirdrop claim contract and forge test verification.
Resolvers
| Resolver | What it lists | Requirement |
|---|---|---|
blockscout |
Native ETH + ERC-20 transfers | None (default, rate-limited) |
etherscan |
Native ETH + ERC-20 transfers | Free API key (EXPLORER_KEY) |
rpc |
Native (via trace_filter) + ERC-20 (via eth_getLogs) |
Archive node with trace_filter |
Resolvers are selected with a resolver:ADDRESS prefix:
bin/track 0x0339…
bin/track etherscan:0x0339… # requires EXPLORER_KEY
bin/track rpc:0x0339… --rpc https://my-archive.node
Blockscout
The public instance (https://eth.blockscout.com/api) needs no key but is throttled to roughly 10 requests/minute. Requests are spaced out and retried; tune with --interval=SEC. Pass --progress to watch the pages land — one dot per request is the only signal a throttled run gives.
Some networks block direct access to the explorer host (Cloudflare-gated egress). Route requests through an HTTP proxy with --proxy URL (or the EXPLORER_PROXY env var). A working option is https://r.jina.ai:
EXPLORER_PROXY=https://r.jina.ai bin/track 0x0339…
Etherscan
Create a free API key at https://etherscan.io and export it:
export EXPLORER_KEY=yourkey
bin/track etherscan:0x0339…
JSON-RPC
Useful when you already run a node or have a keyed/archive RPC. The --from-block / --to-block flags limit the scan (defaults: whole chain), --no-traces / --no-logs disable the liability of a feature that the node does not support. trace_filter is required for native ETH balances on this path; most free public endpoints reject it, so expect a warning.
bin/track rpc:0x0339… --rpc https://eth-mainnet.g.alchemy.com/v2/KEY
An unbounded scan is very slow. This resolver has no index to query, so without --from-block/--to-block it walks the entire chain in 2000-block eth_getLogs chunks — tens of thousands of calls, and on a public node easily the best part of an hour before the first row is printed. The CLI estimates this up front and says so:
$ bin/track rpc:0x0339… --progress
warn: no block bounds on the rpc resolver; scanning the whole chain
warn: head is 26064847, so expect about 26066 eth_getLogs calls (~13033 chunks x 2 topics)
warn: bound it with --from-block=N --to-block=N, or pass --no-logs; watch progress with --progress
It warns rather than refusing, so a deliberate full sweep still works. Bound the range whenever you can, and add --progress so the silence is visible:
bin/track rpc:0x0339… --from-block=26060000 --to-block=26064847 --progress
--trace (or TRACE=1) echoes the parsed arguments before anything else runs, which separates "flags parsed wrong" from "still scanning":
$ bin/track rpc:0x0339… --depth 0 --trace
trace: argv ["rpc:0x033958c4d407160Ac2a2ffcA6621E4669DA9E6A3"]
trace: resolver="rpc" address="0x033958c4d407160Ac2a2ffcA6621E4669DA9E6A3"
trace: depth=0 max_addresses=500 full_addresses=false
trace: from_block=0 to_block=nil no_traces=false no_logs=false
trace: rpc_url=nil rpc_key=none explorer_key=none proxy=nil interval=6.0
trace: progress=false
Note: eth_getLogs filters on the log emitter (the token contract), not on participants, so the RPC resolver scans token contracts where the address was a from/to index. Full-history discovery without an explorer/archive index is impractical on free RPC tiers (Alchemy free caps ranges at 10 blocks).
Options
Usage: track [RESOLVER:]ADDRESS
--rpc=URL RPC endpoint (rpc resolver)
--from-block=N First block to scan (rpc resolver)
--to-block=N Last block to scan (rpc resolver)
--no-traces Skip native ETH transfers (rpc resolver)
--no-logs Skip ERC-20 transfer logs (rpc resolver)
--interval=SEC Seconds between explorer API calls (default 6)
--progress Print a dot per completed request on stderr
--no-progress Do not print progress (default)
--proxy=URL Route explorer requests through an HTTP proxy
-h, --help Show this help
Progress
Every resolver fetches the whole history before it prints anything, and the explorer resolvers deliberately sleep between pages (6s by default), so a healthy run is silent for a long time. --progress makes it visibly alive: a start line naming the scan, then one dot per completed request on stderr, so stdout — and any pipe or redirect — keeps only the report.
$ bin/track 0x0339… --progress
note: fetching 0x0339…E6A3 via blockscout
....... note: level 1, 8 addresses fetched
...........
note: 18 pages completed
Address : 0x0339…
Dots go to stderr specifically so bin/track … > out.txt stays clean; the existing note:/warn: lines are routed through the same reporter so they never land in the middle of a dot run. In a terminal the dot stream is followed by a running counter; when stderr is redirected you get plain dots. On the rpc resolver each dot is one JSON-RPC call — an eth_getLogs scan issues many per address, so expect a steady stream. --no-progress (the default) restores the old quiet behaviour.
Environment: RESOLVER, RPC_URL, RPC_API_KEY, EXPLORER_KEY, EXPLORER_PROXY.
Layout
src/track.cr— CLI, output formatting,snapshot/merklesubcommandssrc/explorer.cr— Etherscan/Blockscout explorer API client (pagination, throttling)src/ethereum_rpc.cr— minimal JSON-RPC client (BigInt-aware)src/transactions.cr— transfer model + listing logicsrc/keccak.cr— pure-Crystal Keccak-256 (for merkle generation)src/merkle.cr— sorted merkle tree + proofssrc/snapshot.cr— airdrop recipient snapshot + holder probescontracts/—DFRDAirdrop.solclaim contract + helperstest/— Foundry suite (cross-validates the merkle tree on-chain)spec/— unit tests (crystal spec)
Tests
crystal spec
forge test
Built with the Crystal stdlib only (no shards).
ethereum-track
- 0
- 0
- 0
- 0
- 0
- about 3 hours ago
- September 23, 2026
Sun, 27 Sep 2026 03:35:28 GMT