package

github.com/khalidjshaikh/ethereum-track

main / published Sep 27, 2026 / repository

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

ResolverWhat it listsRequirement
blockscoutNative ETH + ERC-20 transfersNone (default, rate-limited)
etherscanNative ETH + ERC-20 transfersFree API key (EXPLORER_KEY)
rpcNative (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).

API

  • Cli
  • EthereumRpc

    Minimal Ethereum JSON-RPC client backed by the Crystal stdlib.

  • ExplorerApi

    Reads transaction history from an Etherscan-style explorer API.

  • HistoryResult

    Result of a history scan: native transfers, ERC-20 transfers, present balance, and any warnings raised along the way (e.g.

  • HolderStatus

    Balance-probe outcome for a candidate recipient.

  • Keccak

    Pure-Crystal Keccak-256 (the Ethereum hash, NOT SHA3-256: domain byte is 0x01 instead of 0x06).

  • Merkle

    Sorted merkle tree over airdrop leaves, compatible with the scheme found in OpenZeppelin's MerkleProof and forge-std:

  • Progress

    Live heartbeat for long network-bound runs.

  • SnapEntry
  • Snapshot

    Builds the frozen airdrop snapshot for an address: 1.

  • SnapshotIssue
  • TokenInfo
  • Transactions
  • Transfer

    A single movement of funds involving the target address.

  • Walk

    Breadth-first walk of the fund graph, following OUT-transfers only: level 1 is every distinct address the root paid, level 2 everything those paid, and so on.

  • WalkNode

    An address reached by the walk, together with the history pulled for it.

  • WalkResult
  • WalkStats

    In/out totals for one walked address, used to render a one-line summary.