From f46e344adc7253f81c928c74bb6a2d60148360b3 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 15:53:03 -0300 Subject: [PATCH 01/10] refactor: rename block handlers to semantic namespaced names (COW-1000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames all five Ponder block-handler entries from opaque C1–C5 shorthand to self-documenting composableCow.* names that match their responsibility. Updates ponder.config.ts keys, ponder.on() call names, section headers, log prefixes, and all in-file cross-references. Co-Authored-By: Claude Sonnet 4.6 --- ponder.config.ts | 25 ++++--- src/application/handlers/blockHandler.ts | 86 ++++++++++++------------ 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index c3e66a6..ecc6c16 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -38,47 +38,46 @@ export default createConfig({ }, }, blocks: { - // C1: Contract Poller — RPC multicall for non-deterministic generators + // composableCow.OrderDiscoveryPoller — RPC multicall for non-deterministic generators. // Gnosis interval=4 (~20s) vs mainnet interval=1 (~12s). // The CoW watch-tower processes orders sequentially — with 1,461+ gnosis // generators, a full cycle takes many blocks. Polling every 5s gnosis block // wastes RPC calls since state rarely changes between blocks. - ContractPoller: { + "composableCow.OrderDiscoveryPoller": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest", interval: 4 }, }, interval: 1, }, - // C2: Candidate Confirmer — checks API for unconfirmed candidates - CandidateConfirmer: { + // composableCow.CandidateConfirmer — checks API for unconfirmed candidates. + "composableCow.CandidateConfirmer": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, }, interval: 1, }, - // C3: Status Updater — polls API for open discrete order status - StatusUpdater: { + // composableCow.OrderStatusTracker — polls API for open discrete order status. + "composableCow.OrderStatusTracker": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, }, interval: 1, }, - // C4: Historical Bootstrap — one-time owner fetch for non-deterministic backfill orders - HistoricalBootstrap: { + // composableCow.OwnerBackfill — one-time owner fetch for non-deterministic backfill orders. + "composableCow.OwnerBackfill": { chain: { mainnet: { startBlock: "latest", endBlock: "latest" }, gnosis: { startBlock: "latest", endBlock: "latest" }, }, interval: 1, }, - // C5: Deterministic Cancellation Sweeper — singleOrders() mapping read for - // generators C1 skips (allCandidatesKnown=true). Cadence per generator is - // DETERMINISTIC_CANCEL_SWEEP_INTERVAL blocks; the handler itself is cheap - // when nothing is due. - DeterministicCancellationSweeper: { + // composableCow.CancellationWatcher — singleOrders() mapping read for deterministic + // generators (allCandidatesKnown=true). Cadence per generator is + // DETERMINISTIC_CANCEL_SWEEP_INTERVAL blocks; the handler itself is cheap when nothing is due. + "composableCow.CancellationWatcher": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index 817453c..e058bc6 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -1,17 +1,17 @@ /** * Block handlers — five responsibilities split into separate Ponder block entries. * - * C1 (ContractPoller): RPC multicall for non-deterministic generators. Every block. - * C2 (CandidateConfirmer): API batch check for unconfirmed candidates. Every block. - * C3 (StatusUpdater): API batch check for open discrete orders + expiry. Every block. - * C4 (HistoricalBootstrap): One-time owner fetch for non-deterministic backfill orders. - * C5 (DeterministicCancellationSweeper): singleOrders() mapping read for - * deterministic generators (allCandidatesKnown=true) that - * C1 skips. Runs every block but re-checks each generator - * only every DETERMINISTIC_CANCEL_SWEEP_INTERVAL blocks. + * OrderDiscoveryPoller: RPC multicall for non-deterministic generators. Every block. + * CandidateConfirmer: API batch check for unconfirmed candidates. Every block. + * OrderStatusTracker: API batch check for open discrete orders + expiry. Every block. + * OwnerBackfill: One-time owner fetch for non-deterministic backfill orders. + * CancellationWatcher: singleOrders() mapping read for deterministic generators + * (allCandidatesKnown=true) that OrderDiscoveryPoller skips. + * Runs every block but re-checks each generator only every + * DETERMINISTIC_CANCEL_SWEEP_INTERVAL blocks. * * All handlers start at "latest" — only run during live sync. - * C4 additionally has endBlock: "latest", so it fires exactly once. + * OwnerBackfill additionally has endBlock: "latest", so it fires exactly once. */ import { ponder } from "ponder:registry"; @@ -47,7 +47,7 @@ const SINGLE_SHOT_NON_DETERMINISTIC = ["GoodAfterTime", "TradeAboveThreshold"] a const BLOCK_NEVER = 2n ** 63n - 1n; // sentinel for epoch-scheduled generators (PollTryAtEpoch) const VALID_DISCRETE_STATUSES = new Set(["fulfilled", "unfilled", "expired", "cancelled"]); -// Minimal ABI for C5: reads the singleOrders(owner, hash) mapping on ComposableCoW. +// Minimal ABI for CancellationWatcher: reads the singleOrders(owner, hash) mapping on ComposableCoW. // `false` means the owner called remove() — generator is cancelled on-chain. const SINGLE_ORDERS_ABI = [ { @@ -63,12 +63,12 @@ const SINGLE_ORDERS_ABI = [ ] as const; -// ─── C1: Contract Poller ───────────────────────────────────────────────────── +// ─── composableCow.OrderDiscoveryPoller ────────────────────────────────────── // Polls getTradeableOrderWithSignature for any active generator where // allCandidatesKnown=false. Normally only non-deterministic types, but also // serves as fallback for deterministic types whose precompute failed. -ponder.on("ContractPoller:block", async ({ event, context }) => { +ponder.on("composableCow.OrderDiscoveryPoller:block", async ({ event, context }) => { if (process.env.DISABLE_POLL_RESULT_CHECK) return; const chainId = context.chain.id as SupportedChainId; @@ -120,7 +120,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { if (dueOrders.length === 0) return; console.log( - `[COW:C1] ENTER block=${currentBlock} chain=${chainId} due=${dueOrders.length}`, + `[COW:OrderDiscoveryPoller] ENTER block=${currentBlock} chain=${chainId} due=${dueOrders.length}`, ); const c1MulticallPromise = context.client.multicall({ @@ -148,7 +148,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { } catch (err) { if (err instanceof TimeoutError) { console.warn( - `[COW:C1] multicall timeout block=${currentBlock} chain=${chainId} due=${dueOrders.length}`, + `[COW:OrderDiscoveryPoller] multicall timeout block=${currentBlock} chain=${chainId} due=${dueOrders.length}`, ); return; } @@ -264,7 +264,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { ), ); console.log( - `[COW:C1] NEVER generatorId=${order.generatorId} reason=${pollResult.reason} block=${currentBlock} chain=${chainId}`, + `[COW:OrderDiscoveryPoller] NEVER generatorId=${order.generatorId} reason=${pollResult.reason} block=${currentBlock} chain=${chainId}`, ); neverCount++; break; @@ -285,7 +285,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { ), ); console.log( - `[COW:C1] CANCELLED generatorId=${order.generatorId} block=${currentBlock} chain=${chainId}`, + `[COW:OrderDiscoveryPoller] CANCELLED generatorId=${order.generatorId} block=${currentBlock} chain=${chainId}`, ); break; } @@ -296,15 +296,15 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { const capped = dueOrders.length === maxGeneratorsPerBlock; console.log( - `[COW:C1] DONE block=${currentBlock} chain=${chainId} due=${dueOrders.length} success=${successCount} never=${neverCount} backedOff=${backedOffCount}${capped ? " CAPPED" : ""}`, + `[COW:OrderDiscoveryPoller] DONE block=${currentBlock} chain=${chainId} due=${dueOrders.length} success=${successCount} never=${neverCount} backedOff=${backedOffCount}${capped ? " CAPPED" : ""}`, ); }); -// ─── C2: Candidate Confirmer ───────────────────────────────────────────────── +// ─── composableCow.CandidateConfirmer ──────────────────────────────────────── // Checks if candidate discrete orders exist on the Orderbook API. // When confirmed, promotes them to discreteOrder. -ponder.on("CandidateConfirmer:block", async ({ event, context }) => { +ponder.on("composableCow.CandidateConfirmer:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; // Parent-cancelled cascade: candidates whose parent generator flipped to @@ -387,7 +387,7 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { ); console.log( - `[COW:C2] block=${event.block.number} chain=${chainId} parent-cancelled=${orphanCandidates.length}`, + `[COW:CandidateConfirmer] block=${event.block.number} chain=${chainId} parent-cancelled=${orphanCandidates.length}`, ); } } @@ -549,15 +549,15 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { if (confirmed > 0 || stale.length > 0) { console.log( - `[COW:C2] block=${event.block.number} chain=${chainId} candidates=${unconfirmed.length} confirmed=${confirmed} expired=${stale.length}`, + `[COW:CandidateConfirmer] block=${event.block.number} chain=${chainId} candidates=${unconfirmed.length} confirmed=${confirmed} expired=${stale.length}`, ); } }); -// ─── C3: Status Updater ────────────────────────────────────────────────────── +// ─── composableCow.OrderStatusTracker ──────────────────────────────────────── // Polls the API for status updates on open discrete orders. Expires past validTo. -ponder.on("StatusUpdater:block", async ({ event, context }) => { +ponder.on("composableCow.OrderStatusTracker:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; const currentTimestamp = event.block.timestamp; @@ -603,7 +603,7 @@ ponder.on("StatusUpdater:block", async ({ event, context }) => { if (updated > 0) { console.log( - `[COW:C3] block=${event.block.number} chain=${chainId} open=${openOrders.length} updated=${updated}`, + `[COW:OrderStatusTracker] block=${event.block.number} chain=${chainId} open=${openOrders.length} updated=${updated}`, ); } } @@ -654,11 +654,11 @@ ponder.on("StatusUpdater:block", async ({ event, context }) => { ); }); -// ─── C4: Historical Bootstrap ──────────────────────────────────────────────── +// ─── composableCow.OwnerBackfill ───────────────────────────────────────────── // One-time discovery of historical discrete orders for non-deterministic // generators created during backfill. Fires once at startBlock=endBlock="latest". -ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { +ponder.on("composableCow.OwnerBackfill:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; const currentBlock = event.block.number; @@ -669,7 +669,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { .where(eq(bootstrapRetryQueue.chainId, chainId)); console.log( - `[COW:C4] block=${currentBlock} chain=${chainId} pending_retry=${queued.length}`, + `[COW:OwnerBackfill] block=${currentBlock} chain=${chainId} pending_retry=${queued.length}`, ); let totalDiscovered = 0; @@ -691,7 +691,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { } catch (err) { if (err instanceof TimeoutError) { console.warn( - `[COW:C4] owner retry timeout owner=${owner} chain=${chainId} retry_count=${retryCount + 1} after=${BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS}ms`, + `[COW:OwnerBackfill] owner retry timeout owner=${owner} chain=${chainId} retry_count=${retryCount + 1} after=${BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS}ms`, ); await context.db.sql .update(bootstrapRetryQueue) @@ -735,13 +735,13 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { const freshOwners = new Set(generators.map((g) => g.owner).filter((o) => !retriedOwners.has(o))); if (freshOwners.size === 0 && retriedOwners.size === 0) { - console.log(`[COW:C4] block=${currentBlock} chain=${chainId} no generators need bootstrap`); + console.log(`[COW:OwnerBackfill] block=${currentBlock} chain=${chainId} no generators need bootstrap`); return; } if (freshOwners.size > 0) { console.log( - `[COW:C4] block=${currentBlock} chain=${chainId} generators=${generators.length} fresh_owners=${freshOwners.size}`, + `[COW:OwnerBackfill] block=${currentBlock} chain=${chainId} generators=${generators.length} fresh_owners=${freshOwners.size}`, ); } @@ -757,7 +757,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { } catch (err) { if (err instanceof TimeoutError) { console.warn( - `[COW:C4] owner timeout owner=${owner} chain=${chainId} after=${BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS}ms`, + `[COW:OwnerBackfill] owner timeout owner=${owner} chain=${chainId} after=${BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS}ms`, ); await context.db.sql .insert(bootstrapRetryQueue) @@ -770,20 +770,20 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { } console.log( - `[COW:C4] DONE block=${currentBlock} chain=${chainId} discovered=${totalDiscovered}`, + `[COW:OwnerBackfill] DONE block=${currentBlock} chain=${chainId} discovered=${totalDiscovered}`, ); }); -// ─── C5: Deterministic Cancellation Sweeper ────────────────────────────────── -// C1 skips generators with allCandidatesKnown=true (deterministic types: TWAP, -// StopLoss, CirclesBackingOrder), so SingleOrderNotAuthed is never observed -// for them. This handler closes that gap by reading +// ─── composableCow.CancellationWatcher ─────────────────────────────────────── +// OrderDiscoveryPoller skips generators with allCandidatesKnown=true (deterministic +// types: TWAP, StopLoss, CirclesBackingOrder), so SingleOrderNotAuthed is never +// observed for them. This handler closes that gap by reading // ComposableCoW.singleOrders(owner, hash) on a DETERMINISTIC_CANCEL_SWEEP_INTERVAL // cadence. A `false` result means the owner called remove() on-chain → flip to -// Cancelled, which lets the C2/C3 parent-cancelled cascade (COW-918) reconcile -// the child discrete / candidate rows on the next block. +// Cancelled, which lets the CandidateConfirmer/OrderStatusTracker parent-cancelled +// cascade (COW-918) reconcile the child discrete / candidate rows on the next block. -ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) => { +ponder.on("composableCow.CancellationWatcher:block", async ({ event, context }) => { if (process.env.DISABLE_DETERMINISTIC_CANCEL_SWEEP) return; const chainId = context.chain.id as SupportedChainId; @@ -826,7 +826,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = if (dueGenerators.length === 0) return; console.log( - `[COW:C5] ENTER block=${currentBlock} chain=${chainId} due=${dueGenerators.length}`, + `[COW:CancellationWatcher] ENTER block=${currentBlock} chain=${chainId} due=${dueGenerators.length}`, ); const c5MulticallPromise = context.client.multicall({ @@ -849,7 +849,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = } catch (err) { if (err instanceof TimeoutError) { console.warn( - `[COW:C5] multicall timeout block=${currentBlock} chain=${chainId} due=${dueGenerators.length}`, + `[COW:CancellationWatcher] multicall timeout block=${currentBlock} chain=${chainId} due=${dueGenerators.length}`, ); return; } @@ -888,7 +888,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = ), ); console.log( - `[COW:C5] CANCELLED generatorId=${gen.generatorId} orderType=${gen.orderType} block=${currentBlock} chain=${chainId}`, + `[COW:CancellationWatcher] CANCELLED generatorId=${gen.generatorId} orderType=${gen.orderType} block=${currentBlock} chain=${chainId}`, ); cancelledCount++; } else { @@ -910,7 +910,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = } console.log( - `[COW:C5] DONE block=${currentBlock} chain=${chainId} due=${dueGenerators.length} cancelled=${cancelledCount} stillActive=${stillActiveCount} errors=${errorCount}`, + `[COW:CancellationWatcher] DONE block=${currentBlock} chain=${chainId} due=${dueGenerators.length} cancelled=${cancelledCount} stillActive=${stillActiveCount} errors=${errorCount}`, ); }); From d2ed6d853fdefb5c32e8099f78d499fd414ef1b5 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 19:17:55 -0300 Subject: [PATCH 02/10] fix: remove composableCow. prefix from block interval names (Ponder dot-namespace conflict) Ponder 0.16.x treats dots in block interval names as namespace separators, causing ponder.on('composableCow.OrderDiscoveryPoller:block') to fail validation. Block intervals must use simple names without dots. Co-Authored-By: Claude Sonnet 4.6 --- ponder.config.ts | 20 ++++++++++---------- src/application/handlers/blockHandler.ts | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index ecc6c16..0c97b8b 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -38,46 +38,46 @@ export default createConfig({ }, }, blocks: { - // composableCow.OrderDiscoveryPoller — RPC multicall for non-deterministic generators. + // OrderDiscoveryPoller — RPC multicall for non-deterministic generators. // Gnosis interval=4 (~20s) vs mainnet interval=1 (~12s). // The CoW watch-tower processes orders sequentially — with 1,461+ gnosis // generators, a full cycle takes many blocks. Polling every 5s gnosis block // wastes RPC calls since state rarely changes between blocks. - "composableCow.OrderDiscoveryPoller": { + "OrderDiscoveryPoller": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest", interval: 4 }, }, interval: 1, }, - // composableCow.CandidateConfirmer — checks API for unconfirmed candidates. - "composableCow.CandidateConfirmer": { + // CandidateConfirmer — checks API for unconfirmed candidates. + "CandidateConfirmer": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, }, interval: 1, }, - // composableCow.OrderStatusTracker — polls API for open discrete order status. - "composableCow.OrderStatusTracker": { + // OrderStatusTracker — polls API for open discrete order status. + "OrderStatusTracker": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, }, interval: 1, }, - // composableCow.OwnerBackfill — one-time owner fetch for non-deterministic backfill orders. - "composableCow.OwnerBackfill": { + // OwnerBackfill — one-time owner fetch for non-deterministic backfill orders. + "OwnerBackfill": { chain: { mainnet: { startBlock: "latest", endBlock: "latest" }, gnosis: { startBlock: "latest", endBlock: "latest" }, }, interval: 1, }, - // composableCow.CancellationWatcher — singleOrders() mapping read for deterministic + // CancellationWatcher — singleOrders() mapping read for deterministic // generators (allCandidatesKnown=true). Cadence per generator is // DETERMINISTIC_CANCEL_SWEEP_INTERVAL blocks; the handler itself is cheap when nothing is due. - "composableCow.CancellationWatcher": { + "CancellationWatcher": { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index e058bc6..d8cf55d 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -63,12 +63,12 @@ const SINGLE_ORDERS_ABI = [ ] as const; -// ─── composableCow.OrderDiscoveryPoller ────────────────────────────────────── +// ─── OrderDiscoveryPoller ────────────────────────────────────── // Polls getTradeableOrderWithSignature for any active generator where // allCandidatesKnown=false. Normally only non-deterministic types, but also // serves as fallback for deterministic types whose precompute failed. -ponder.on("composableCow.OrderDiscoveryPoller:block", async ({ event, context }) => { +ponder.on("OrderDiscoveryPoller:block", async ({ event, context }) => { if (process.env.DISABLE_POLL_RESULT_CHECK) return; const chainId = context.chain.id as SupportedChainId; @@ -300,11 +300,11 @@ ponder.on("composableCow.OrderDiscoveryPoller:block", async ({ event, context }) ); }); -// ─── composableCow.CandidateConfirmer ──────────────────────────────────────── +// ─── CandidateConfirmer ──────────────────────────────────────── // Checks if candidate discrete orders exist on the Orderbook API. // When confirmed, promotes them to discreteOrder. -ponder.on("composableCow.CandidateConfirmer:block", async ({ event, context }) => { +ponder.on("CandidateConfirmer:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; // Parent-cancelled cascade: candidates whose parent generator flipped to @@ -554,10 +554,10 @@ ponder.on("composableCow.CandidateConfirmer:block", async ({ event, context }) = } }); -// ─── composableCow.OrderStatusTracker ──────────────────────────────────────── +// ─── OrderStatusTracker ──────────────────────────────────────── // Polls the API for status updates on open discrete orders. Expires past validTo. -ponder.on("composableCow.OrderStatusTracker:block", async ({ event, context }) => { +ponder.on("OrderStatusTracker:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; const currentTimestamp = event.block.timestamp; @@ -654,11 +654,11 @@ ponder.on("composableCow.OrderStatusTracker:block", async ({ event, context }) = ); }); -// ─── composableCow.OwnerBackfill ───────────────────────────────────────────── +// ─── OwnerBackfill ───────────────────────────────────────────── // One-time discovery of historical discrete orders for non-deterministic // generators created during backfill. Fires once at startBlock=endBlock="latest". -ponder.on("composableCow.OwnerBackfill:block", async ({ event, context }) => { +ponder.on("OwnerBackfill:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; const currentBlock = event.block.number; @@ -774,7 +774,7 @@ ponder.on("composableCow.OwnerBackfill:block", async ({ event, context }) => { ); }); -// ─── composableCow.CancellationWatcher ─────────────────────────────────────── +// ─── CancellationWatcher ─────────────────────────────────────── // OrderDiscoveryPoller skips generators with allCandidatesKnown=true (deterministic // types: TWAP, StopLoss, CirclesBackingOrder), so SingleOrderNotAuthed is never // observed for them. This handler closes that gap by reading @@ -783,7 +783,7 @@ ponder.on("composableCow.OwnerBackfill:block", async ({ event, context }) => { // Cancelled, which lets the CandidateConfirmer/OrderStatusTracker parent-cancelled // cascade (COW-918) reconcile the child discrete / candidate rows on the next block. -ponder.on("composableCow.CancellationWatcher:block", async ({ event, context }) => { +ponder.on("CancellationWatcher:block", async ({ event, context }) => { if (process.env.DISABLE_DETERMINISTIC_CANCEL_SWEEP) return; const chainId = context.chain.id as SupportedChainId; From 810949edf423cdd59c8f77cf6c7f6a122d86d9af Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 2 Jun 2026 17:06:18 -0300 Subject: [PATCH 03/10] fix: update stale C1/C5 name references in constants.ts and docs (COW-1000) Co-Authored-By: Claude Sonnet 4.6 --- docs/api-reference.md | 2 +- docs/deployment.md | 6 +++--- src/constants.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 36c680f..e7bc8c7 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -92,7 +92,7 @@ There is one principled exception to "everything as string": `discreteOrder.vali | `discreteOrder.validTo` | number | yes | Unix seconds when this discrete order expires. `uint32` per CoW protocol. | | `discreteOrder.creationDate` | string | no | Unix seconds when the discrete order was first observed. Source varies — see the GraphQL field doc. | | `candidateDiscreteOrder.validTo` | number | yes | Same as `discreteOrder.validTo`. | -| `candidateDiscreteOrder.creationDate` | string | no | Block timestamp at C1 discovery. | +| `candidateDiscreteOrder.creationDate` | string | no | Block timestamp at **OrderDiscoveryPoller** discovery. | | `candidateDiscreteOrder.possibleValidAfterTimestamp` | string | yes | TWAP only: `t0 + partIndex*t`. Earliest Unix-seconds time the part can be valid. | ### Timestamp-like values inside `decodedParams` diff --git a/docs/deployment.md b/docs/deployment.md index 29a4171..2971052 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -28,9 +28,9 @@ Example: `DATABASE_URL=postgresql://cow_programmatic:secretpass@localhost:5433/c | Variable | Required | Description | |----------|----------|-------------| -| `DISABLE_POLL_RESULT_CHECK` | No | Disables the C1 ContractPoller block handler. Skips RPC multicalls for non-deterministic generators. Saves RPC calls during initial sync at the cost of not detecting poll results until re-enabled. | -| `DISABLE_DETERMINISTIC_CANCEL_SWEEP` | No | Disables the C5 DeterministicCancellationSweeper. Skips periodic `singleOrders()` reads on deterministic generators. While disabled, on-chain `ComposableCoW.remove()` calls on TWAP/StopLoss/CirclesBackingOrder generators will not be detected and those generators stay `Active`. | -| `MAX_GENERATORS_PER_BLOCK_` | No | Per-block cap on how many generators C1 and C5 will touch on the given chain (e.g. `MAX_GENERATORS_PER_BLOCK_1=200`, `MAX_GENERATORS_PER_BLOCK_100=400`). Default is 200. Excess generators defer to the next block, prioritized by oldest `lastCheckBlock` first. | +| `DISABLE_POLL_RESULT_CHECK` | No | Disables the OrderDiscoveryPoller block handler. Skips RPC multicalls for non-deterministic generators. Saves RPC calls during initial sync at the cost of not detecting poll results until re-enabled. | +| `DISABLE_DETERMINISTIC_CANCEL_SWEEP` | No | Disables the CancellationWatcher. Skips periodic `singleOrders()` reads on deterministic generators. While disabled, on-chain `ComposableCoW.remove()` calls on TWAP/StopLoss/CirclesBackingOrder generators will not be detected and those generators stay `Active`. | +| `MAX_GENERATORS_PER_BLOCK_` | No | Per-block cap on how many generators OrderDiscoveryPoller and CancellationWatcher will touch on the given chain (e.g. `MAX_GENERATORS_PER_BLOCK_1=200`, `MAX_GENERATORS_PER_BLOCK_100=400`). Default is 200. Excess generators defer to the next block, prioritized by oldest `lastCheckBlock` first. | | `DISABLE_SETTLEMENT_FACTORY_CHECK` | No | Skips `getCode` + `FACTORY()` RPC calls in the GPv2Settlement handler. Useful for benchmarking base sync throughput. | | `PINO_LOG_LEVEL` | No | Log verbosity: `debug`, `info`, `warn`, `error`. Defaults to Ponder's built-in default. | diff --git a/src/constants.ts b/src/constants.ts index b427256..719b8cb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,7 +19,7 @@ export const RECHECK_INTERVAL = BigInt(ORDERBOOK_POLL_INTERVAL); export const SIGNING_SCHEME_EIP1271 = "eip1271"; /** - * COW-908: Hard per-block ceiling on how many generators the C1 ContractPoller + * COW-908: Hard per-block ceiling on how many generators the OrderDiscoveryPoller * will multicall in a single block. Generators exceeding the cap defer to the * next block (prioritized by oldest lastCheckBlock first). * @@ -47,13 +47,13 @@ export const TRY_NEXT_BLOCK_BACKOFF_MID = 10n; export const TRY_NEXT_BLOCK_BACKOFF_COLD = 50n; /** - * C5 (DeterministicCancellationSweeper) re-check cadence, in blocks. + * CancellationWatcher re-check cadence, in blocks. * * For deterministic generators (`allCandidatesKnown = true`), `remove()` detection * is via a `ComposableCoW.singleOrders(owner, hash)` storage read. `remove()` is * rare; a ~100 block cadence gives a worst-case detection lag of ~20 min on - * mainnet and ~8 min on Gnosis while keeping the RPC cost well below C1's - * every-block poll. + * mainnet and ~8 min on Gnosis while keeping the RPC cost well below + * OrderDiscoveryPoller's every-block poll. */ export const DETERMINISTIC_CANCEL_SWEEP_INTERVAL = 100n; @@ -67,14 +67,14 @@ export const ORDERBOOK_HTTP_TIMEOUT_MS = 10_000; /** * Hard wall-clock cap for a block handler's aggregate `context.client.multicall` - * call (C1, C5). viem has no per-call signal; the timer races the promise and + * call (OrderDiscoveryPoller, CancellationWatcher). viem has no per-call signal; the timer races the promise and * the handler returns cleanly on breach. */ export const BLOCK_HANDLER_RPC_TIMEOUT_MS = 15_000; /** - * Hard wall-clock cap for the whole per-owner bootstrap fetch in C4 + * Hard wall-clock cap for the whole per-owner bootstrap fetch in OwnerBackfill * (account pagination + by_uids refresh). Owners that exceed this are skipped; - * the normal C1 / C2 path picks them up on subsequent blocks. + * the normal OrderDiscoveryPoller / CandidateConfirmer path picks them up on subsequent blocks. */ export const BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS = 30_000; From f41b45d05563187c6e3067570d1099d4b00b2c1b Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 10:47:27 -0300 Subject: [PATCH 04/10] fix: wrap handler names in inline code in deployment.md flags table Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 2971052..9a76d5d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -28,9 +28,9 @@ Example: `DATABASE_URL=postgresql://cow_programmatic:secretpass@localhost:5433/c | Variable | Required | Description | |----------|----------|-------------| -| `DISABLE_POLL_RESULT_CHECK` | No | Disables the OrderDiscoveryPoller block handler. Skips RPC multicalls for non-deterministic generators. Saves RPC calls during initial sync at the cost of not detecting poll results until re-enabled. | -| `DISABLE_DETERMINISTIC_CANCEL_SWEEP` | No | Disables the CancellationWatcher. Skips periodic `singleOrders()` reads on deterministic generators. While disabled, on-chain `ComposableCoW.remove()` calls on TWAP/StopLoss/CirclesBackingOrder generators will not be detected and those generators stay `Active`. | -| `MAX_GENERATORS_PER_BLOCK_` | No | Per-block cap on how many generators OrderDiscoveryPoller and CancellationWatcher will touch on the given chain (e.g. `MAX_GENERATORS_PER_BLOCK_1=200`, `MAX_GENERATORS_PER_BLOCK_100=400`). Default is 200. Excess generators defer to the next block, prioritized by oldest `lastCheckBlock` first. | +| `DISABLE_POLL_RESULT_CHECK` | No | Disables the `OrderDiscoveryPoller` block handler. Skips RPC multicalls for non-deterministic generators. Saves RPC calls during initial sync at the cost of not detecting poll results until re-enabled. | +| `DISABLE_DETERMINISTIC_CANCEL_SWEEP` | No | Disables the `CancellationWatcher`. Skips periodic `singleOrders()` reads on deterministic generators. While disabled, on-chain `ComposableCoW.remove()` calls on TWAP/StopLoss/CirclesBackingOrder generators will not be detected and those generators stay `Active`. | +| `MAX_GENERATORS_PER_BLOCK_` | No | Per-block cap on how many generators `OrderDiscoveryPoller` and `CancellationWatcher` will touch on the given chain (e.g. `MAX_GENERATORS_PER_BLOCK_1=200`, `MAX_GENERATORS_PER_BLOCK_100=400`). Default is 200. Excess generators defer to the next block, prioritized by oldest `lastCheckBlock` first. | | `DISABLE_SETTLEMENT_FACTORY_CHECK` | No | Skips `getCode` + `FACTORY()` RPC calls in the GPv2Settlement handler. Useful for benchmarking base sync throughput. | | `PINO_LOG_LEVEL` | No | Log verbosity: `debug`, `info`, `warn`, `error`. Defaults to Ponder's built-in default. | From 47e135c426e8b6bcad0be68cb8c38d4446a5f65d Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 19:09:02 -0300 Subject: [PATCH 05/10] style: remove unnecessary quotes from block handler keys in ponder.config.ts All five keys are valid identifiers and don't need quoting. Co-Authored-By: Claude Sonnet 4.6 --- ponder.config.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index 0c97b8b..b936a36 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -43,7 +43,7 @@ export default createConfig({ // The CoW watch-tower processes orders sequentially — with 1,461+ gnosis // generators, a full cycle takes many blocks. Polling every 5s gnosis block // wastes RPC calls since state rarely changes between blocks. - "OrderDiscoveryPoller": { + OrderDiscoveryPoller: { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest", interval: 4 }, @@ -51,7 +51,7 @@ export default createConfig({ interval: 1, }, // CandidateConfirmer — checks API for unconfirmed candidates. - "CandidateConfirmer": { + CandidateConfirmer: { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, @@ -59,7 +59,7 @@ export default createConfig({ interval: 1, }, // OrderStatusTracker — polls API for open discrete order status. - "OrderStatusTracker": { + OrderStatusTracker: { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, @@ -67,7 +67,7 @@ export default createConfig({ interval: 1, }, // OwnerBackfill — one-time owner fetch for non-deterministic backfill orders. - "OwnerBackfill": { + OwnerBackfill: { chain: { mainnet: { startBlock: "latest", endBlock: "latest" }, gnosis: { startBlock: "latest", endBlock: "latest" }, @@ -77,7 +77,7 @@ export default createConfig({ // CancellationWatcher — singleOrders() mapping read for deterministic // generators (allCandidatesKnown=true). Cadence per generator is // DETERMINISTIC_CANCEL_SWEEP_INTERVAL blocks; the handler itself is cheap when nothing is due. - "CancellationWatcher": { + CancellationWatcher: { chain: { mainnet: { startBlock: "latest" }, gnosis: { startBlock: "latest" }, From e772e3f462eb051ff48ebbe8027f52f312da58be Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 8 Jun 2026 10:09:27 -0300 Subject: [PATCH 06/10] docs: rename block handlers in architecture.md, document flash loan path and order_uid_cache (COW-1000) - Update all C1-C5 / ContractPoller / StatusUpdater / HistoricalBootstrap / DeterministicCancellationSweeper references to the new semantic names (OrderDiscoveryPoller, OrderStatusTracker, OwnerBackfill, CancellationWatcher) - Clarify in overview and data-flow diagram that block handlers are generic (apply to all generators regardless of type); Aave flash loan detection is event-driven via settlement.ts, not a block handler - Add cow_cache.order_uid_cache section explaining why it's retained across Ponder deployments and what is/isn't cached Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture.md | 56 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index ff5abef..bfc270d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,7 +6,7 @@ This document covers how the indexer works, from on-chain events to the GraphQL The system is a Ponder 0.16.x indexer that watches the ComposableCoW contract on Ethereum mainnet and Gnosis Chain. When a user creates a programmatic order (TWAP, Stop Loss, etc.), the contract emits a `ConditionalOrderCreated` event. The indexer picks that up, decodes the order parameters, resolves the actual owner (which may be behind a proxy), and writes the result to Postgres. A Hono HTTP server exposes the data through GraphQL and a SQL passthrough endpoint. -Ponder registers nine top-level handlers: four contract event handlers (`ComposableCow` backfill, `ComposableCowLive`, `CoWShedFactory`, `GPv2Settlement`) plus five live-only block handlers in `blockHandler.ts` (C1–C5). The contract handlers react to on-chain events; C1–C5 poll contract state and the orderbook API during live sync. `settlement.ts` inspects `Settlement` receipts to detect Aave adapters from Trade logs. +Ponder registers nine top-level handlers: four contract event handlers (`ComposableCow` backfill, `ComposableCowLive`, `CoWShedFactory`, `GPv2Settlement`) plus five live-only block handlers in `blockHandler.ts`. The contract handlers react to on-chain events; the block handlers poll contract state and the orderbook API during live sync. `settlement.ts` inspects `Settlement` receipts to detect Aave flash loan adapters — this is event-driven, not a block handler. Block handlers are generic: they apply to all ComposableCoW generators regardless of order type. ## Contracts and Chains @@ -19,7 +19,7 @@ Currently active: Stub configs exist for all 12 chains in cow-sdk's `ALL_SUPPORTED_CHAIN_IDS`; contract addresses for the remaining chains need verification before enabling (see COW-986). -`ponder.config.ts` derives all config from `ACTIVE_CHAINS` in `src/chains/index.ts` and wires it into Ponder's `createConfig`. It never contains raw addresses or block numbers directly. The config also sets up five live-only block handlers — C1 (`ContractPoller`), C2 (`CandidateConfirmer`), C3 (`StatusUpdater`), C4 (`HistoricalBootstrap`), C5 (`DeterministicCancellationSweeper`) — all running every block during live sync. +`ponder.config.ts` derives all config from `ACTIVE_CHAINS` in `src/chains/index.ts` and wires it into Ponder's `createConfig`. It never contains raw addresses or block numbers directly. The config also sets up five live-only block handlers — `OrderDiscoveryPoller`, `CandidateConfirmer`, `OrderStatusTracker`, `OwnerBackfill`, `CancellationWatcher` — all running during live sync. Three contracts are indexed: @@ -57,17 +57,17 @@ settlement.ts handler | - for each trade owner: check if it's an Aave adapter (FACTORY() call) | - if yes: resolve EOA via owner(), write ownerMapping v -blockHandler.ts (five live-only block handlers) - | C1 (ContractPoller) — multicall getTradeableOrderWithSignature for non-deterministic - | generators; detects cancellation via SingleOrderNotAuthed error - | C2 (CandidateConfirmer) — confirms candidate orders via orderbook API → discreteOrder; - | cascades parent Cancelled status to orphan candidates - | C3 (StatusUpdater) — polls API for status updates on open discrete orders; - | cascades parent Cancelled status to orphan open rows - | C4 (HistoricalBootstrap) — one-time backfill of non-deterministic historical orders - | C5 (DeterministicCancellationSweeper) — periodic singleOrders() mapping read for - | deterministic generators (allCandidatesKnown=true); flips - | to Cancelled when remove() has been called on-chain +blockHandler.ts (five live-only block handlers — generic, apply to all generators) + | OrderDiscoveryPoller — multicall getTradeableOrderWithSignature for non-deterministic + | generators; detects cancellation via SingleOrderNotAuthed error + | CandidateConfirmer — confirms candidate orders via orderbook API → discreteOrder; + | cascades parent Cancelled status to orphan candidates + | OrderStatusTracker — polls API for status updates on open discrete orders; + | cascades parent Cancelled status to orphan open rows + | OwnerBackfill — one-time backfill of non-deterministic historical orders + | CancellationWatcher — periodic singleOrders() mapping read for + | deterministic generators (allCandidatesKnown=true); flips + | to Cancelled when remove() has been called on-chain v schema tables (Postgres) | @@ -109,14 +109,14 @@ PK: `(chainId, eventId)`. Indexed on `owner`, `handler`, `hash`, `chainId+owner` ### discrete_order -Links individual order UIDs (from the CoW Protocol orderbook) back to their parent generator. One generator can produce many discrete orders over its lifetime — a TWAP with 10 parts creates 10 discrete orders. Populated by C2 (CandidateConfirmer) after confirmation against the orderbook API; status kept current by C3 (StatusUpdater). +Links individual order UIDs (from the CoW Protocol orderbook) back to their parent generator. One generator can produce many discrete orders over its lifetime — a TWAP with 10 parts creates 10 discrete orders. Populated by `CandidateConfirmer` after confirmation against the orderbook API; status kept current by `OrderStatusTracker`. Key columns: `orderUid`, `chainId`, `conditionalOrderGeneratorId` (references `eventId`), `status` (open/fulfilled/unfilled/expired/cancelled), `sellAmount`, `buyAmount`, `executedSellAmount`, `executedBuyAmount`. PK: `(chainId, orderUid)`. See [api-reference.md](./api-reference.md) for full field docs. ### candidate_discrete_order -Staging rows for discrete orders discovered by C1 (`getTradeableOrderWithSignature`) before the orderbook API lists them. When C2 confirms a UID against the API, the row is promoted to `discrete_order` and removed from candidates. +Staging rows for discrete orders discovered by `OrderDiscoveryPoller` (`getTradeableOrderWithSignature`) before the orderbook API lists them. When `CandidateConfirmer` confirms a UID against the API, the row is promoted to `discrete_order` and removed from candidates. Key columns: `orderUid`, `chainId`, `conditionalOrderGeneratorId`, amounts, `validTo`, `creationDate`, `possibleValidAfterTimestamp` (TWAP scheduling). PK: `(chainId, orderUid)`. @@ -132,6 +132,14 @@ PK: `(chainId, address)`. The `resolutionDepth` column records how many hops were needed to reach the EOA. For CoWShed proxies it's 0 (the `COWShedBuilt` event directly provides the user). For Aave adapters it's 1 (call `owner()` on the adapter to get the EOA). +### cow_cache.order_uid_cache + +A persistent cache table created outside Ponder's per-deployment schema (in a dedicated `cow_cache` PostgreSQL schema) by `setup.ts` on startup. It stores the last-known terminal status (`fulfilled`, `expired`, `cancelled`) and executed amounts for each discrete order UID. + +**Why it's retained across restarts:** Ponder creates a new schema namespace on each `ponder start` deployment. Without a separate cache schema, `CandidateConfirmer` and `OrderStatusTracker` would re-query the orderbook API for every order on startup — including thousands of already-final orders. The `cow_cache` schema survives deployments, so handlers skip API calls for orders whose terminal status is already known. + +**What is and isn't cached:** Only terminal statuses are stored. Open orders are never cached — they are always re-fetched on the next block. If an entry is present for a UID, the handler uses the cached status and skips the API call. + ## Handlers in Detail ### composableCow.ts -- ConditionalOrderCreated @@ -169,23 +177,23 @@ The handler uses raw `eth_call` for the FACTORY() check specifically to avoid Po Stats are accumulated and logged every 30 seconds to track throughput without per-event log spam. -### blockHandler.ts -- C1 / C2 / C3 / C4 / C5 +### blockHandler.ts — five live-only block handlers -Five live-only block handlers, all in a single file. They only run during live sync (startBlock: "latest") to avoid hammering the orderbook API during historical backfill. C1 and C5 share a per-chain batch cap (`MAX_GENERATORS_PER_BLOCK_`, default 200) and pull from a priority queue ordered by oldest `lastCheckBlock` first. Generators past the cap defer to the next block. +All five are generic — they apply to all ComposableCoW generators regardless of order type. Aave flash loan adapter detection is separate and event-driven (see `settlement.ts`). All handlers run during live sync only (`startBlock: "latest"`); they never fire during historical backfill to avoid hammering the orderbook API. `OrderDiscoveryPoller` and `CancellationWatcher` share a per-chain batch cap (`MAX_GENERATORS_PER_BLOCK_`, default 200), pulling from a priority queue ordered by oldest `lastCheckBlock` first. -**C1 — ContractPoller** (every block): Multicalls `getTradeableOrderWithSignature` on ComposableCoW for each `Active` generator where `allCandidatesKnown=false`. A success result creates a `candidateDiscreteOrder` entry. A `SingleOrderNotAuthed` error marks the generator as `Cancelled` with `lastPollResult='cancelled:SingleOrderNotAuthed'`. Other errors (tryNextBlock, tryAtEpoch, etc.) advance the generator's `nextCheckBlock` accordingly. Single-shot non-deterministic types (GoodAfterTime, TradeAboveThreshold) set `allCandidatesKnown=true` after first success. Can be disabled with `DISABLE_POLL_RESULT_CHECK=true`. +**OrderDiscoveryPoller** (every block): Multicalls `getTradeableOrderWithSignature` on ComposableCoW for each `Active` generator where `allCandidatesKnown=false`. A success result creates a `candidateDiscreteOrder` entry. A `SingleOrderNotAuthed` error marks the generator as `Cancelled` with `lastPollResult='cancelled:SingleOrderNotAuthed'`. Other errors (tryNextBlock, tryAtEpoch, etc.) advance the generator's `nextCheckBlock` accordingly. Single-shot non-deterministic types (GoodAfterTime, TradeAboveThreshold) set `allCandidatesKnown=true` after first success. Can be disabled with `DISABLE_POLL_RESULT_CHECK=true`. -**C2 — CandidateConfirmer** (every block): First drains any `candidateDiscreteOrder` rows whose parent generator is `Cancelled` — promoting them into `discreteOrder` with `status='cancelled'` and deleting the candidate rows. Then checks remaining `candidateDiscreteOrder` rows against the orderbook API: when a candidate appears in the API, it's promoted to `discreteOrder` and deleted from candidates. Candidates past their `validTo` are also pruned. +**CandidateConfirmer** (every block): First drains any `candidateDiscreteOrder` rows whose parent generator is `Cancelled` — promoting them into `discreteOrder` with `status='cancelled'` and deleting the candidate rows. Then checks remaining `candidateDiscreteOrder` rows against the orderbook API: when a candidate appears in the API, it's promoted to `discreteOrder` and deleted from candidates. Candidates past their `validTo` are also pruned. -**C3 — StatusUpdater** (every block): Polls the orderbook API for all `open` discrete orders and updates their status from the API response. Then sweeps any remaining `open` rows whose parent generator is `Cancelled` to `status='cancelled'` (API-terminal statuses from the loop above still win for children that were traded before on-chain cancellation). Finally expires any orders past their `validTo` timestamp. +**OrderStatusTracker** (every block): Polls the orderbook API for all `open` discrete orders and updates their status from the API response. Then sweeps any remaining `open` rows whose parent generator is `Cancelled` to `status='cancelled'` (API-terminal statuses from the loop above still win for children that were traded before on-chain cancellation). Finally expires any orders past their `validTo` timestamp. -**C4 — HistoricalBootstrap** (fires once at latest block): One-time fetch of historical orders for non-deterministic generators (PerpetualSwap, GoodAfterTime, TradeAboveThreshold, Unknown) that were active during backfill but have no discrete orders yet. Queries the CoW Protocol `/orders?owner=` endpoint per owner. +**OwnerBackfill** (fires once at latest block): One-time fetch of historical orders for non-deterministic generators (PerpetualSwap, GoodAfterTime, TradeAboveThreshold, Unknown) that were active during backfill but have no discrete orders yet. Queries the CoW Protocol `/orders?owner=` endpoint per owner. -**C5 — DeterministicCancellationSweeper** (every block): Closes C1's blind spot. C1 skips `allCandidatesKnown=true` generators, so removals via `ComposableCoW.remove()` on deterministic types (TWAP, StopLoss, CirclesBackingOrder) would otherwise go undetected — `remove()` emits no event. C5 multicalls `singleOrders(owner, hash)` on a per-generator cadence of `DETERMINISTIC_CANCEL_SWEEP_INTERVAL` blocks (default 100). A `false` return means the owner called `remove()` on-chain: the generator is flipped to `Cancelled` with `lastPollResult='cancelled:removeMapping'`, after which C2 and C3's parent-cancelled cascades reconcile the children on the next block. `true` reschedules the next check. Can be disabled with `DISABLE_DETERMINISTIC_CANCEL_SWEEP=true`. +**CancellationWatcher** (every block): Closes `OrderDiscoveryPoller`'s blind spot. `OrderDiscoveryPoller` skips `allCandidatesKnown=true` generators, so removals via `ComposableCoW.remove()` on deterministic types (TWAP, StopLoss, CirclesBackingOrder) would otherwise go undetected — `remove()` emits no event. `CancellationWatcher` multicalls `singleOrders(owner, hash)` on a per-generator cadence of `DETERMINISTIC_CANCEL_SWEEP_INTERVAL` blocks (default 100). A `false` return means the owner called `remove()` on-chain: the generator is flipped to `Cancelled` with `lastPollResult='cancelled:removeMapping'`, after which `CandidateConfirmer` and `OrderStatusTracker`'s parent-cancelled cascades reconcile the children on the next block. `true` reschedules the next check. Can be disabled with `DISABLE_DETERMINISTIC_CANCEL_SWEEP=true`. ## Order Types and Decoders -Eight order types are supported, each with a dedicated decoder in `src/decoders/`. Three are deterministic (UIDs precomputed at creation, `allCandidatesKnown=true`, not polled by C1): TWAP, StopLoss, CirclesBackingOrder. Five are non-deterministic (UIDs depend on runtime state, polled every block by C1): PerpetualSwap, GoodAfterTime, TradeAboveThreshold, SwapOrderHandler, ERC4626CowSwapFeeBurner. +Eight order types are supported, each with a dedicated decoder in `src/decoders/`. Three are deterministic (UIDs precomputed at creation, `allCandidatesKnown=true`, not polled by `OrderDiscoveryPoller`): TWAP, StopLoss, CirclesBackingOrder. Five are non-deterministic (UIDs depend on runtime state, polled every block by `OrderDiscoveryPoller`): PerpetualSwap, GoodAfterTime, TradeAboveThreshold, SwapOrderHandler, ERC4626CowSwapFeeBurner. Core handler addresses are identical across all chains; some newer handlers (SwapOrderHandler, ERC4626CowSwapFeeBurner) are per-chain overlays. Both are tracked in `src/utils/order-types.ts`. @@ -227,5 +235,5 @@ See [api-reference.md](./api-reference.md) for the full endpoint list. ## Known Limitations -- Cancellation detection has a small lag. For non-deterministic generators, C1 catches `SingleOrderNotAuthed` on the next poll (every block). For deterministic generators, C5 reads `singleOrders(owner, hash)` every `DETERMINISTIC_CANCEL_SWEEP_INTERVAL` blocks (default 100) — so on-chain removal is reflected with worst-case latency of ~100 blocks (~20 min mainnet, ~8 min Gnosis). There is no on-chain event for `remove()`, so shorter detection latency would require a higher-cadence sweep. Once the generator is marked `Cancelled`, C2 and C3 cascade the state to children on the next block; API-terminal statuses (`fulfilled` / `unfilled` / `expired`) still win for children that were already traded on the orderbook. +- Cancellation detection has a small lag. For non-deterministic generators, `OrderDiscoveryPoller` catches `SingleOrderNotAuthed` on the next poll (every block). For deterministic generators, `CancellationWatcher` reads `singleOrders(owner, hash)` every `DETERMINISTIC_CANCEL_SWEEP_INTERVAL` blocks (default 100) — so on-chain removal is reflected with worst-case latency of ~100 blocks (~20 min mainnet, ~8 min Gnosis). There is no on-chain event for `remove()`, so shorter detection latency would require a higher-cadence sweep. Once the generator is marked `Cancelled`, `CandidateConfirmer` and `OrderStatusTracker` cascade the state to children on the next block; API-terminal statuses (`fulfilled` / `unfilled` / `expired`) still win for children that were already traded on the orderbook. - Aave adapter owner resolution is reactive — `owner_mapping` is written when the adapter appears in settlement, which may be after the conditional order is created. The generator row keeps `resolvedOwner` equal to the adapter address when no mapping existed at insert time; that column is not backfilled when the mapping is inserted later. `ownerAddressType` on the generator IS backfilled when the mapping is inserted — after which GraphQL and REST filters on `ownerAddressType = "flash_loan_helper"` reflect the correct value. `resolvedOwner` is still not backfilled (set once at insert, unchanged thereafter). From bce4ea15ed6f2641ab953ede1887fb0942a86677 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 8 Jun 2026 10:59:16 -0300 Subject: [PATCH 07/10] docs: replace "generic" with "order-type-agnostic" for block handlers (COW-1000) Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 30af595..0a46e4d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,7 +6,7 @@ This document covers how the indexer works, from on-chain events to the GraphQL The system is a Ponder 0.16.x indexer that watches the ComposableCoW contract on Ethereum mainnet and Gnosis Chain. When a user creates a programmatic order (TWAP, Stop Loss, etc.), the contract emits a `ConditionalOrderCreated` event. The indexer picks that up, decodes the order parameters, resolves the actual owner (which may be behind a proxy), and writes the result to Postgres. A Hono HTTP server exposes the data through GraphQL and a SQL passthrough endpoint. -Ponder registers nine top-level handlers: four contract event handlers (`ComposableCow` backfill, `ComposableCowLive`, `CoWShedFactory`, `GPv2Settlement`) plus five live-only block handlers in `blockHandler.ts`. The contract handlers react to on-chain events; the block handlers poll contract state and the orderbook API during live sync. `settlement.ts` inspects `Settlement` receipts to detect Aave flash loan adapters — this is event-driven, not a block handler. Block handlers are generic: they apply to all ComposableCoW generators regardless of order type. +Ponder registers nine top-level handlers: four contract event handlers (`ComposableCow` backfill, `ComposableCowLive`, `CoWShedFactory`, `GPv2Settlement`) plus five live-only block handlers in `blockHandler.ts`. The contract handlers react to on-chain events; the block handlers poll contract state and the orderbook API during live sync. `settlement.ts` inspects `Settlement` receipts to detect Aave flash loan adapters — this is event-driven, not a block handler. Block handlers are order-type-agnostic: they apply to all ComposableCoW generators regardless of order type. ## Contracts and Chains @@ -57,7 +57,7 @@ settlement.ts handler | - for each trade owner: check if it's an Aave adapter (FACTORY() call) | - if yes: resolve EOA via owner(), write ownerMapping v -blockHandler.ts (five live-only block handlers — generic, apply to all generators) +blockHandler.ts (five live-only block handlers — order-type-agnostic, apply to all generators) | OrderDiscoveryPoller — multicall getTradeableOrderWithSignature for non-deterministic | generators; detects cancellation via SingleOrderNotAuthed error | CandidateConfirmer — confirms candidate orders via orderbook API → discreteOrder; @@ -179,7 +179,7 @@ Stats are accumulated and logged every 30 seconds to track throughput without pe ### blockHandler.ts — five live-only block handlers -All five are generic — they apply to all ComposableCoW generators regardless of order type. Aave flash loan adapter detection is separate and event-driven (see `settlement.ts`). All handlers run during live sync only (`startBlock: "latest"`); they never fire during historical backfill to avoid hammering the orderbook API. `OrderDiscoveryPoller` and `CancellationWatcher` share a per-chain batch cap (`MAX_GENERATORS_PER_BLOCK_`, default 200), pulling from a priority queue ordered by oldest `lastCheckBlock` first. +All five are order-type-agnostic — they apply to all ComposableCoW generators regardless of order type. Aave flash loan adapter detection is separate and event-driven (see `settlement.ts`). All handlers run during live sync only (`startBlock: "latest"`); they never fire during historical backfill to avoid hammering the orderbook API. `OrderDiscoveryPoller` and `CancellationWatcher` share a per-chain batch cap (`MAX_GENERATORS_PER_BLOCK_`, default 200), pulling from a priority queue ordered by oldest `lastCheckBlock` first. **OrderDiscoveryPoller** (every block): Multicalls `getTradeableOrderWithSignature` on ComposableCoW for each `Active` generator where `allCandidatesKnown=false`. A success result creates a `candidateDiscreteOrder` entry. A `SingleOrderNotAuthed` error marks the generator as `Cancelled` with `lastPollResult='cancelled:SingleOrderNotAuthed'`. Other errors (tryNextBlock, tryAtEpoch, etc.) advance the generator's `nextCheckBlock` accordingly. Single-shot non-deterministic types (GoodAfterTime, TradeAboveThreshold) set `allCandidatesKnown=true` after first success. Can be disabled with `DISABLE_POLL_RESULT_CHECK=true`. From 2157e4a1ba94148182e5247459946b337a29d16b Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 8 Jun 2026 14:35:48 -0300 Subject: [PATCH 08/10] docs: minor whitespace fix in architecture.md Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture.md b/docs/architecture.md index 0a46e4d..1e999a1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -95,6 +95,7 @@ PK: `(chainId, hash)`. The main table. One row per `ConditionalOrderCreated` event. Stores the raw order params, the decoded params (as JSON), and the resolved owner. Key columns: + - `eventId` -- Ponder's event ID, used as the entity identifier - `owner` -- the address from the event (could be a proxy) - `resolvedOwner` -- EOA from `ownerMapping` when `owner` already has a row at insert time; otherwise the same as `owner`. Not rewritten when a new `owner_mapping` row is added later. From ac0f05663d5be5c2fa6551ed1a1e395c4f1ca4ca Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 8 Jun 2026 16:12:10 -0300 Subject: [PATCH 09/10] fix: align ponder.on registrations with renamed block handlers in ponder.config.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ponder.config.ts renamed the five block handlers to semantic names but blockHandler.ts still had the old ponder.on("ContractPoller:block", ...) registrations — causing Ponder to fail at startup or silently skip the mismatched handlers. Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/blockHandler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index 0cca35e..f59f90d 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -72,7 +72,7 @@ const SINGLE_ORDERS_ABI = [ // allCandidatesKnown=false. Normally only non-deterministic types, but also // serves as fallback for deterministic types whose precompute failed. -ponder.on("ContractPoller:block", async ({ event, context }) => { +ponder.on("OrderDiscoveryPoller:block", async ({ event, context }) => { if (process.env.DISABLE_POLL_RESULT_CHECK) return; const chainId = context.chain.id as SupportedChainId; @@ -570,7 +570,7 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { // ─── C3: Status Updater ────────────────────────────────────────────────────── // Polls the API for status updates on open discrete orders. Expires past validTo. -ponder.on("StatusUpdater:block", async ({ event, context }) => { +ponder.on("OrderStatusTracker:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; const currentTimestamp = event.block.timestamp; @@ -669,7 +669,7 @@ ponder.on("StatusUpdater:block", async ({ event, context }) => { // One-time discovery of historical discrete orders for non-deterministic // generators created during backfill. Fires once at startBlock=endBlock="latest". -ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { +ponder.on("OwnerBackfill:block", async ({ event, context }) => { const chainId = context.chain.id as SupportedChainId; const currentBlock = event.block.number; @@ -784,7 +784,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { // Cancelled, which lets the C2/C3 parent-cancelled cascade (COW-918) reconcile // the child discrete / candidate rows on the next block. -ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) => { +ponder.on("CancellationWatcher:block", async ({ event, context }) => { if (process.env.DISABLE_DETERMINISTIC_CANCEL_SWEEP) return; const chainId = context.chain.id as SupportedChainId; From 2f2ccb764e5f5517ec417c5164767a4ce05fed2f Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 9 Jun 2026 14:32:18 -0300 Subject: [PATCH 10/10] docs: remove PostgreSQL memory flags, production architecture, and What's Not Implemented sections; drop COW-908 task ref from constant comment (COW-1000) Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 23 ----------------------- src/constants.ts | 2 +- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index a505901..27159f2 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -95,18 +95,6 @@ docker compose --profile deploy up -d The `Dockerfile` in the project root builds the Ponder image: two-stage Node 22 Alpine, installs dependencies with `--frozen-lockfile`, exposes port 3000, runs `pnpm start`. The health check hits `/ready` with a 24-hour start period (initial sync takes hours). -### PostgreSQL Memory Flags - -Memory settings are hardcoded in the `command:` block of `docker-compose.yml`, tuned for 1G RAM: - -- `shared_buffers`: 204MB (~20% RAM) -- `work_mem`: 2MB per connection (~25% RAM / max_connections) -- `effective_cache_size`: 512MB (~50% RAM) -- `maintenance_work_mem`: 51MB - -Adjust these proportionally if you change the host's available memory. - - ## Deploying ### How it works in practice @@ -130,14 +118,3 @@ On the target machine, you need Docker and DNS configured to point at the contai To tear down: `npx tsx deployment/manage.ts down --env-file deployment/.env` -### Production architecture - -For a production setup, run at least two containers: one dedicated to indexing and one (or more) serving the API. This way if a user overloads the API with queries, the indexer keeps working. And if the indexer crashes or restarts, the API stays up with the last-synced data. - -The current deploy profile in `docker-compose.yml` runs a single container doing both. Splitting indexer and API is a straightforward change: run two instances of the same image, one with indexing enabled and one configured as API-only (Ponder supports this via its `--api-only` flag or by disabling indexing). - -## What's Not Implemented - -- No monitoring or alerting. Watch container logs and the `/healthz` endpoint. Standard observability tooling (Prometheus, Grafana) can be wired up but nothing is preconfigured. -- No automated backups. Use standard PostgreSQL tools (`pg_dump`, WAL archiving). -- Single-instance deployment by default. See the production architecture section above for multi-container guidance. diff --git a/src/constants.ts b/src/constants.ts index 719b8cb..946f879 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,7 +19,7 @@ export const RECHECK_INTERVAL = BigInt(ORDERBOOK_POLL_INTERVAL); export const SIGNING_SCHEME_EIP1271 = "eip1271"; /** - * COW-908: Hard per-block ceiling on how many generators the OrderDiscoveryPoller + * Hard per-block ceiling on how many generators the OrderDiscoveryPoller * will multicall in a single block. Generators exceeding the cap defer to the * next block (prioritized by oldest lastCheckBlock first). *