🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

88 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

chainscope

A smart-money indexer for Uniswap V3 on Ethereum.
Ingest β†’ decode β†’ per-wallet PnL β†’ a read API and Telegram alerts β€” reorg-safe, exactly-once, and flat on disk. Built two ways behind one seam: an in-process channel and a distributed Redpanda log.

status language store chain license

docker compose up -d Β Β·Β  API on :8080 Β Β·Β  Grafana on :3000


What is chainscope

chainscope follows Uniswap V3 on Ethereum mainnet and answers one question in near real time: which wallets are winning. It ingests swaps and liquidity events, computes each wallet's realised PnL on a FIFO cost basis, serves it over a read API, and pushes Telegram alerts when smart money moves.

The point is not another price feed. It is being correct under the two failure modes that usually get hand-waved β€” chain reorganisations and process crashes β€” so every derived number can be proven right after a block is orphaned or the process is killed mid-write.

  • Exactly-once β€” a write and the cursor that names it commit in one transaction.
  • Reorg-safe β€” orphaned blocks and everything derived from them (PnL, candles, the leaderboard) are walked backwards, not left stale.
  • Flat on disk β€” raw events roll off past finality; candles and wallet aggregates are the permanent record they burn into.

Who it's for

Audience What they get
Analysts / traders A live smart-money leaderboard and per-wallet PnL scorecards, wash-trade filtered.
Alert consumers Telegram pings on watchlist moves, coordinated cluster buys, and fresh pools clearing a scorecard.
Integrators A read API β€” pools, swaps, OHLCV candles, wallet trades, /metrics β€” with keyset pagination.
Operators One-command Docker stack, Prometheus metrics, and a provisioned Grafana dashboard.
Developers A full Rust workspace β€” chain source, pipeline, API, alerter β€” with exactly-once and reorg recovery implemented two ways to fork and study.

The pipeline

Every stage publishes to a transport seam and reads from it; none calls the next directly. One block of work flows through, in order:

producer ──[BlockUnit]──▢ transformer ──[RowBatch]──▢ writer ──▢ PnL Β· candles Β· retention
  fetch                     decode                    exactly-once fold
  • producer β€” walks the chain one block at a time from the stored cursor; resumes rather than repeats after a restart. Typed RPC errors (Transient, RangeTooLarge, BlockNotFound, Fatal) decide retry vs. stop.
  • transformer β€” decodes swaps and liquidity events into partitioned raw tables.
  • writer β€” commits each batch and the cursor in one transaction: exactly-once by construction. PnL, candle folds and pool discovery extend that same transaction.
  • retention β€” folds candles, rolls them up (1mβ†’1hβ†’1d), and drops raw partitions past a finality floor, keeping the footprint flat.

Two architectures, one seam

chainscope's thesis is that it implements exactly-once and reorg recovery two ways, behind the same seam, switchable with one line of config.

  • Phase 1 β€” in-process channel (M1–M4). All stages run in one process over a bounded in-memory channel. Exactly-once is a single Postgres transaction; a crash leaves "all rows + cursor" or "neither", never a half-state.
  • Phase 2 β€” Redpanda log (M5). The same stages split into separate processes reading a Kafka-compatible topic. Exactly-once becomes idempotent consumers keyed on offset, and a reorg is a compensating revert event on the log rather than a rollback inside one transaction.

Both satisfy the same behavioural tests. Switching is one setting in chainscope.toml β€” no stage is touched:

[pipeline]
transport = "channel"    # phase 1, one process
# transport = "redpanda"   # phase 2, distributed; brokers under docker compose

The seam (crates/core/src/transport.rs) is the only place either transport is named β€” enforced by tests/seam_is_not_leaking.rs, which fails the build if any other file reaches for a channel or a Kafka client directly.

How it works

Three surfaces, all from one indexed store:

  • Read β€” the API answers pools, swaps, OHLCV candles, wallet scorecards and the leaderboard, keyset-paginated with an opaque cursor and a hot-stats cache.
  • Alert β€” the alerter polls the same database and pushes to Telegram on a watchlist move, a cluster of watched wallets buying the same pool, or a new pool clearing the scorecard threshold. It is a separate process so a hung outbound call can never backpressure ingestion.
  • Observe β€” Prometheus scrapes the API's /metrics; Grafana renders ingest lag, the block heads, and the raw-vs-aggregate disk footprint M9 keeps flat.

Run it

Everything runs under Docker. docker compose up builds the three binaries and brings up the full stack β€” Postgres, indexer, API, alerter, Prometheus, Grafana.

# 1. configure β€” set RPC endpoint(s) and, for alerts, a Telegram bot token
cp .env.example .env

# 2. one command up
docker compose up -d --build
docker compose ps                 # postgres healthy, then indexer/api/alerter up

# 3. prove it end to end
./ops/smoke.sh                    # brings up, gates every health check, asserts the surface

ops/smoke.sh is the exit criterion: it waits for each service's health check, then asserts /status and /metrics answer, Prometheus is actually scraping the API (up == 1), and Grafana is serving β€” one green line or a named failure. To drive a binary from the host instead, run just the database (migrations apply on startup):

docker compose up -d postgres
cargo run --bin chainscope-indexer

Endpoints

Surface Where
Ingest status + lag GET /status
Indexed / new pools GET /pools, GET /pools/:address, GET /pools/new
Pool swaps / candles GET /pools/:address/swaps, .../candles?resolution=1m|1h|1d
Wallet PnL scorecard GET /wallets/:address
Wallet realised trades GET /wallets/:address/trades
Smart-money leaderboard GET /leaderboard
Prometheus metrics / health GET /metrics, GET /healthz
Grafana dashboard http://localhost:3000 (admin/admin) β€” lag, heads, footprint
Prometheus http://localhost:9090

Architecture

The read API and alerter are read-only consumers of the store the indexer owns:

   ChainSource (JSON-RPC)          ← the only thing that knows Ethereum exists
         β”‚  blocks + logs
         β–Ό
   indexer pipeline               ← fetch β†’ decode β†’ PnL β†’ retention, exactly-once
         β”‚  writes
         β–Ό
   Postgres (partitioned)          ← raw events roll off; candles + PnL are permanent
         β”‚  read-only
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ά api        ← pools, swaps, candles, scorecards, /metrics
         └────────────▢ alerter    ← Telegram: moves, cluster buys, new pools

Only crates/eth-source knows Ethereum exists β€” cargo tree -p chainscope-core shows no chain library at all, the boundary enforced by the compiler rather than by discipline.

chainscope/
β”œβ”€ crates/
β”‚  β”œβ”€ core/          # chain-agnostic domain types, cursor, the transport seam
β”‚  └─ eth-source/    # the only crate that knows Ethereum: ChainSource + decoders
β”œβ”€ bins/
β”‚  β”œβ”€ indexer/       # ingestion pipeline: fetch β†’ decode β†’ PnL β†’ retention
β”‚  β”œβ”€ api/           # read API (axum): keyset pagination, hot-stats cache, /metrics
β”‚  └─ alerter/       # Telegram alerts: watchlist moves, cluster buys, new pools
β”œβ”€ migrations/       # embedded, applied on startup
β”œβ”€ ops/              # Dockerfile helpers, prometheus.yml, grafana provisioning, smoke.sh
└─ scripts/          # redpanda topic setup (phase 2)

Testing

chainscope is verified at levels that each catch what the one below can't β€” many are behavioural, asserting an invariant rather than a fixed output:

Level Where What it proves
Unit cargo test (per crate) math, decoding, cursor and config logic
Crash resumability tests/crash_resumability.rs kill at any point, resume gap-free β€” 50 randomised trials
Reorg recovery tests/reorg_* orphaned blocks and their derived rows walk backwards
Seam isolation tests/seam_is_not_leaking.rs no stage names a transport directly
Store integration tests/*_db.rs writer, PnL, candles, retention, API against real Postgres
Log transport tests/kafka_* idempotent consumers, offsets, broadcast under a storm
End-to-end ops/smoke.sh one command up, stack healthy, dashboard live

The load-bearing test is crash_resumability: it runs the real producer and writer against a synthetic chain, aborts at a randomised point, restarts from the cursor, and asserts blocks form a gap-free run with no duplicates β€” and a companion test deliberately breaks atomicity to prove the invariant can actually fail.

Project status

Every milestone is complete β€” the stack comes up with one command, reaches the mainnet tip, and renders live on Grafana.

Milestone What it added
βœ… M1 Crash-safe ingestion: single-transaction exactly-once, cursor resume, supervised shutdown
βœ… M2 Event decoding: swaps + liquidity events into partitioned raw tables
βœ… M3 Throughput: concurrent backfill, batching, range bisection under RPC limits
βœ… M4 Reorg recovery: fork detection, rollback of orphaned blocks and everything derived
βœ… M5 Phase-2 transport: Redpanda log, separate processes, idempotent consumers + revert events
βœ… M6 Per-wallet PnL: FIFO cost basis, lot-consumption ledger for exact reorg reversal, wash flagging
βœ… M7 Read API: keyset pagination, hot-stats cache, leaderboard materialised view
βœ… M8 Alerts + sniffer: Telegram watchlist moves, cluster buys, new-pool scorecards
βœ… M9 Retention: live candle fold, 1mβ†’1hβ†’1d downsampling, partition pruning past finality
βœ… M10 Ops: Prometheus /metrics, provisioned Grafana, Dockerfiles, one-command full-stack compose

Configuration

chainscope.toml is committed and holds everything shareable β€” chain id, pool list, tuning knobs. .env is not committed and holds the secrets: the database URL and RPC endpoints. Any value is overridable from the environment as CHAINSCOPE_<SECTION>__<KEY> (the environment always wins), and DATABASE_URL / RUST_LOG keep their conventional names.

Everything is validated before a socket opens β€” addresses must be 20 bytes of hex, the pool list non-empty and duplicate-free, unknown keys are errors β€” and a failure names the field and echoes the bad value rather than starting mis-configured.

RPC note (measured 2026-07-23): free endpoints differ wildly in history depth. rpc.flashbots.net is the best keyless option; most others cap eth_getLogs range or want a token past ~128 blocks. Deep backfill (M3) needs a paid archive endpoint; the network tests derive a recent settled block from the tip rather than pinning historical block numbers, so they test the code, not a billing tier.

Toolchain

  • Rust (stable) for the whole workspace β€” indexer, API, alerter, core, eth-source.
  • Postgres 16 as the store; migrations are embedded and applied on startup.
  • Docker + Docker Compose for the full stack.
  • Redpanda (Kafka-compatible) for the phase-2 log transport.
  • Prometheus + Grafana for metrics and the dashboard.

License

MIT

About

A Uniswap V3 indexer on Ethereum mainnet, written in Rust. Exactly-once writes, reorg-safe, resumable.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages