All notable changes to indexer-rs. Format: Keep a Changelog.
- Backfill writes now go through PG
COPY FROM STDIN, not per-row INSERT. Buffer N blocks (default 100, configurable viaINDEXER_WRITE_BATCH_SIZE, max 1000) then flush blocks/transactions/logs in a single transaction with the cursor bump toMAX(height). The tail loop'singest_onekeeps the per-rowON CONFLICT DO NOTHINGpath for at-tip retries. Spec §5 invariant 2 (cursor never lands ahead of data) holds: cursor advance is inside the same transaction as the COPY streams; partial-batch flush on cancel keeps the cursor in sync.
- CoinBlast orphan adoption — direct-deployed curves (e.g. CBLAST Genesis pre-factory) are now probed via
eth_call(token()/curveSupply()/graduationSrxThreshold()) on first sighting and adopted intocb_tokensautomatically. Topic-collision contracts get cached inknown_non_curvesso each address is probed at most once per worker lifetime. ChainProvider::call(to, data)—eth_callwrapper returning raw bytes; callers decode viaalloy_sol_types.GET /address/:addr/txs?limitandGET /address/:addr/transfers?limit&standard— paginated address activity, mirrors TS port.bin/indexer.rs: tail loop wired (gRPCStreamEventsparallel with backfill, SingleFlight gate, reconnect on Lagged / StreamEnded). Analytics flusher wired (CLICKHOUSE_URL env opt-in). Both honour sharedCancellationTokenfor graceful shutdown.
Initial code-complete release covering spec Phases 0 → 9.
- Phase 0 — Cargo workspace, 9 placeholder crates + 2 binaries, CI pipeline.
- Phase 1 —
domaincrate (BlockHeight/TxIndex/LogIndex/EpochNumbernewtypes;WeiwrappingU256withsqlx::Typeimpls throughBigDecimal;Block/Transaction/Log/TokenTransferstructs).dbcrate (0001_initial.sqlmirroring the TS drizzle schema byte-for-byte across 10 tables;PgPoolwrapper; per-table CRUD helpers generic oversqlx::PgExecutor). - Phase 2 —
chaincrate (ChainProviderwrapping alloyRootProvider<Http<reqwest>>;GrpcClientwrapping the generated tonic stub;RestClientfor native/tx/<hash>JSON;retry_with_backoffwith deterministic jitter, no rand dep).build.rsinvokestonic-prost-buildon the canonicalsentrix.proto. - Phase 3 —
synccrate.block_writerwrites block + txs + logs + cursor advance in onesqlx::Transaction(atomic, idempotent).cursorwraps_meta[last_synced_height].single_flightgate (AtomicBool + Mutex stash) collapses tip bursts to one chain.backfillwalkscursor+1..=tip-safe_lag.reorgprobes atcursor-reorg_probe_depth, walks back to divergence on hash mismatch, cascade-deletes downstream + resets cursor.tailconsumes tonicStreamEvents; on Lagged exits so the orchestrator re-runs JSON-RPC backfill before resubscribe. - Phase 4 —
coinblastcrate.events.rsdeclaresCurveCreated/Buy/Sell/Graduatedviaalloy-sol-typessol!macro; topic0 + decoder generated from ABI.worker.rsruns chunked backfill (4000-block chunks, 5-block safe_lag).handlers.rswritescb_tokensinsert +cb_tradesinsert + aggregate bump in single SQL transactions; idempotent viacb_tradesUNIQUE(tx_hash, log_index). - Phase 5 —
apicrate. Axum routes mirror TS Fastify port byte-for-byte: snake_case keys, bigint stringified,from_addr → fromrename, log topics flattened with nulls dropped.GET /health,GET /blocks?limit&before,GET /blocks/:height,GET /tx/:hash.ApiError → IntoResponsewith{ error: ... }body. - Phase 6 — GraphQL via
async-graphql+async-graphql-axum.POST /graphqlandGET /graphql/playground(GraphiQL UI). Query:block(height),blocks(first, before),transaction(hash). Same wire shape as REST. - Phase 7 —
cachecrate.fred-9RedisPoolwrapper.CacheClient::{get,set,invalidate}with serde-JSON values, three TTL tiers (Chain=60s, Address=5min, Detail=1h), key namespace.CircuitBreaker(atomic counter +parking_lot::Mutex<Option<Instant>>) trips after threshold consecutive failures, half-opens afteropen_forwindow. - Phase 8 —
analyticscrate. ClickHouseRawTxRow(Decimal as String to avoid Decimal256 experimental flag).AnalyticsHandle(mpsc unbounded) +run_flusher(15s tick + drain on cancel; final flush before exit per spec §10.5). - Phase 9 —
bin/api.rs+bin/indexer.rswired withfigmentenv config, graceful shutdown on SIGTERM/Ctrl-C. Multi-stageDockerfile(rust:1.90-bookworm builder → debian-slim runtime, non-root uid 10001).docker-compose.yml(postgres:17-alpine + indexer + api with healthchecks).
- Rust 1.90 (workspace
rust-version = "1.90", transitive deps require ≥1.86 viaruint/icu/home). - Edition 2024.
cargo fmt --check+cargo build --workspace --locked+cargo nextest run --workspace --locked --no-tests=pass+cargo clippy --workspace --all-targets --locked -- -D warnings+cargo deny check(advisories + bans + licenses + sources).
- Push-on-commit hookup from sync layer to analytics handle (flusher infra is wired; sync side TBD).
- REST iter 2 long tail (
/stats/dailymaterialised view,/whale/*, etherscan-compat?module=, coinblast-specific routes). - Testcontainers wiring for golden-ingest + reorg-sim integration tests (currently
#[ignore]'d).