Architecture Overview
A production-grade Canopy indexer consists of three loosely coupled layers:- Ingestion layer — polls the RPC node using
eth_getLogs(or subscribes via WebSocket) to fetch Canopy events in chronological block order and writes raw event records to a local store. - State layer — applies each event as a delta to a structured offer and series table, maintaining derived fields like
status,assetFilled, andassetRemainingso downstream consumers never need to re-derive state themselves. - API layer — exposes a REST or GraphQL interface that queries the state layer and returns pre-computed order book snapshots, series metadata, and fill history to frontends and analytics tools.
offerId and seriesId.
Key Events to Index
Handle each of the following events to maintain complete and accurate state:| Event | Action |
|---|---|
OfferCreated | Insert offer record with escrowedAssetAmount; set assetFilled = 0, status = OPEN |
OfferFilled | Increment assetFilled by assetUnits; set status = FILLED when assetFilled == escrowedAssetAmount |
OfferCancelled | Set status = CANCELLED; store assetDebited and assetReceived |
SeriesCreated | Insert series record with claimToken address, asset, and unlockTime |
Redeemed | Decrement series.backing by amount; record assetReceived |
All event parameters needed for full state reconstruction are included in the event payloads themselves. You do not need to call
offers() or series() contract reads during initial sync — the events are self-contained, which significantly speeds up historical backfill.Sample Indexer Loop
The following TypeScript snippet shows a block-range ingestion function that processesOfferCreated events and upserts them into a database. Extend the same pattern to cover the remaining event types listed above.
indexEvents in a loop — advancing fromBlock by your chosen chunk size on each iteration — until you reach the chain head, then switch to polling or subscribing for new blocks.
Handling Reorgs
Chain reorganizations can invalidate events you have already processed. To handle them gracefully:- Store the
blockNumberandtransactionHashalongside every event record when you ingest it. - On each new polling cycle, compare the latest stored block hash against the current canonical chain. If they diverge, you have detected a reorg.
- Roll back all event records with
blockNumbergreater than the last stable (finalized) block, then re-index from that point forward.
Serving Market Depth
Once your state layer is populated, computing an order book for a trading pair is a straightforward query:- Filter all offers where
asset == targetAssetANDquote == targetQuoteANDstatus == OPENANDexpiry > now. - Compute
assetRemaining = escrowedAssetAmount - assetFilledfor each matching offer. - Sort by
pricePerUnit— ascending for asks, descending for bids — to produce depth ladders. - Optionally aggregate offers at the same price level into a single depth entry for a traditional order book display.