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/merkle subcommands
  • src/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 logic
  • src/keccak.cr — pure-Crystal Keccak-256 (for merkle generation)
  • src/merkle.cr — sorted merkle tree + proofs
  • src/snapshot.cr — airdrop recipient snapshot + holder probes
  • contracts/ — DFRDAirdrop.sol claim contract + helpers
  • test/ — 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).

Repository

ethereum-track

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 3 hours ago
  • September 23, 2026
License

Links
Synced at

Sun, 27 Sep 2026 03:35:28 GMT

Languages