Problem
Every Supra dApp that needs on-chain events in a database ends up rewriting the same loop: poll the RPC, page through results, store rows, remember where it stopped. The SDK turns that loop into a reusable, typed library so a team only writes the event types and the storage logic.
My role: Personal open-source project. Designed the API, implemented the crate and published it to crates.io.
Constraints
- Supra exposes events over an HTTP RPC (
/rpc/v3/events/{address}::{event}) with header-based pagination, so indexing is poll-based. - An indexer must resume where it left off after a restart without re-indexing from genesis.
- Each consumer has its own event schema and tables, so storage cannot be hard-coded.
Architecture
IndexerConfig
node_url, contract_address, events, starting block height, fetch interval
SupraIndexer::start_all
Spawns one tokio task per registered event collector
Supra RPC (HTTP)
GET events from the saved block height, up to 100 per page, following the x-supra-cursor header
EventStore<E> (user code)
Typed events handed to the consumer's own storage logic
PostgreSQL block_heights
Per-event checkpoint upserted after each batch
What I Built
SupraIndexerwithregister_collector::<E, R, S>()andstart_all(): one concurrent collector per event type.- Trait-based extension points:
EventStore<E>for persistence,ExtractEventData<E>for unwrapping RPC responses,HasBlockHeightfor checkpointing. GenericEventCollector<E, R, S>, so the event type, response wrapper and store are all chosen by the consumer at compile time.- Checkpoint table (
block_heights) created and seeded on startup withON CONFLICT DO NOTHING, so restarts keep existing progress. - Diesel 2 + r2d2 connection pool for PostgreSQL, reqwest for RPC calls, tokio for scheduling.
- Published as
supra_indexer_sdkon crates.io.
Technical Challenges
- Generic but type-safe collectors
- Each collector is generic over the event type, the RPC response shape and the store, so consumers get compile-time checked events without the SDK knowing their schema.
- Resumable indexing
- Progress is stored per event name and upserted as
last_block + 1after every batch. The startup seed never overwrites an existing checkpoint. - Cursor pagination
- The collector follows the
x-supra-cursorresponse header page by page until the RPC stops returning one, then sleeps for the configured interval. - Isolated failures
- Each event type runs in its own task. An RPC or storage error pauses that collector and retries after 5 seconds without stopping the others.
Trade-offs & Limitations
- HTTP polling rather than streaming: simple and matches the RPC, at the cost of latency equal to the fetch interval.
- Events are stored before the checkpoint moves, in separate steps. Delivery is at-least-once, so
EventStoreimplementations should write idempotently. - Fixed 5-second retry with no backoff; Diesel calls are blocking inside async tasks.
- Early version (0.1.x) with no automated tests yet.
