Skip to content

Commit a4f0b5a

Browse files
refactor: address all PR #29 review comments — semantic fixes and cleanup
1 parent c394c29 commit a4f0b5a

5 files changed

Lines changed: 165 additions & 85 deletions

File tree

.claude/commands/debug-ponder.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,32 @@ Healthy output includes cache hit ratio and active owner count:
163163

164164
High `cacheHits` relative to `owners` means the cache is working. High `apiFetches` with low `activeOwners` may indicate many owners are not yet cached.
165165

166+
### C4 HistoricalBootstrap — one-shot (`endBlock: "latest"`) — PR #29 entrada 9
167+
168+
`HistoricalBootstrap` in `ponder.config.ts` uses `startBlock: "latest"` and `endBlock: "latest"` so it should run **once per chain** when the indexer reaches live (not every block). Use logs to confirm.
169+
170+
**Invocation counter (expect `#1` only per chain per process):**
171+
172+
```bash
173+
grep -n "\[COW:C4\] HistoricalBootstrap invocation" ponder.log
174+
```
175+
176+
Healthy output (one line per chain after live starts):
177+
178+
```
179+
... [COW:C4] HistoricalBootstrap invocation=#1 chain=1 block=... (one-shot bootstrap; see .claude/commands/debug-ponder.md — C4 section)
180+
```
181+
182+
If you see **`invocation=#2`** or a **`WARN`** line saying *invocation #2* / *repeats every block*, `endBlock: "latest"` is not behaving as a one-shot in your Ponder version — escalate or check Ponder release notes. A **full resync** resets the process: you will see `#1` again on the next run (expected).
183+
184+
**Full C4 trace (bootstrap work or empty):**
185+
186+
```bash
187+
grep -n "\[COW:C4\]" ponder.log
188+
```
189+
190+
You should see `invocation=#1`, then either `no generators need bootstrap` or `generators=...` + `DONE`, then **no further `[COW:C4]` lines** for that chain until restart.
191+
166192
### Backfill skip — verify poller is not running during historical sync
167193

168194
During backfill, the orderbook poller is silently skipped (no log line). To confirm live-only behavior, check that poll DONE lines only appear near the tip:

agent_docs/m3-orderbook-integration-flow.md

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ The system has seven components. Each has a single responsibility.
1515
**Runs during**: Backfill AND live sync (two Ponder contract entries: `ComposableCow` for historical, `ComposableCowLive` for live).
1616

1717
**Key behavior difference by order type**:
18-
- **Deterministic (TWAP, StopLoss)**: Pre-computes UIDs → fetches status from API → upserts `discreteOrder` → if all terminal, marks generator `Invalid` (no further polling needed). This happens at both backfill and live sync.
18+
- **Deterministic (TWAP, StopLoss)**: Pre-computes UIDs → fetches status from API → UIDs found on API go into `discreteOrder`, UIDs not found go into `candidateDiscreteOrder` → if all are terminal on API, marks generator `Completed` (no further polling needed). This happens at both backfill and live sync.
1919
- **Non-deterministic (PerpetualSwap, GoodAfterTime, TradeAboveThreshold, Unknown)**: Inserts generator only. Discrete orders will be discovered by the block handlers at live sync.
2020

21-
**Writes to**: `conditionalOrderGenerator`, `discreteOrder` (via UID Pre-computation).
21+
**Writes to**: `conditionalOrderGenerator`, `discreteOrder` and `candidateDiscreteOrder` (via UID Pre-computation).
2222

2323
### Component B: UID Pre-computation (`uidPrecompute.ts`)
2424

@@ -28,7 +28,7 @@ The system has seven components. Each has a single responsibility.
2828

2929
**Used by**: Creation Handler (both backfill and live). Also available to any future component that needs to know UIDs without RPC calls.
3030

31-
**Writes to**: `discreteOrder`, updates `conditionalOrderGenerator.status` to `Invalid` if all orders are terminal.
31+
**Writes to**: `discreteOrder` (for UIDs found on API), `candidateDiscreteOrder` (for UIDs not yet on API). Updates `conditionalOrderGenerator.status` to `Completed` if all orders are terminal on API.
3232

3333
### Component C1: Contract Poller (`blockHandler.ts` — handler 1)
3434

@@ -48,15 +48,17 @@ The system has seven components. Each has a single responsibility.
4848

4949
### Component C2: Candidate Confirmer (`blockHandler.ts` — handler 2)
5050

51-
**Responsibility**: Checks if candidate discrete orders exist on the Orderbook API. When confirmed, moves them to `discreteOrder`.
51+
**Responsibility**: Checks if candidate discrete orders exist on the Orderbook API. When confirmed, promotes them to `discreteOrder` and deletes the candidate row.
5252

5353
**When it runs**: Every block at live sync.
5454

55-
**How it works**: Queries `candidateDiscreteOrder` rows that don't yet have a corresponding `discreteOrder` row. Batch-fetches their UIDs from the API via `POST /orders/by_uids`. If the API has the order, upserts into `discreteOrder` with the API's authoritative status.
55+
**How it works**: Queries `candidateDiscreteOrder` rows that don't yet have a corresponding `discreteOrder` row. Batch-fetches their UIDs from the API via `POST /orders/by_uids`. If the API has the order, upserts into `discreteOrder` with the API's authoritative status, then deletes the `candidateDiscreteOrder` row.
56+
57+
**Cleanup**: After promotion, confirmed candidates are deleted from `candidateDiscreteOrder`. Stale candidates past their `validTo` are also cleaned up — if the watch-tower never submitted them, they're expired and won't appear on the API.
5658

5759
**Why a separate handler?** The Contract Poller (C1) discovers orders on-chain, but the API may not have them yet (watch-tower submission delay). This handler polls the API repeatedly until the candidate is confirmed. Separation means C1 focuses on RPC, C2 focuses on API — different cost profiles, can be tuned independently.
5860

59-
**Writes to**: `discreteOrder`.
61+
**Writes to**: `discreteOrder`. **Deletes from**: `candidateDiscreteOrder`.
6062

6163
### Component C3: Status Updater (`blockHandler.ts` — handler 3)
6264

@@ -114,8 +116,10 @@ The parent entity. One row per `ConditionalOrderCreated` event.
114116
| `resolvedOwner` | The EOA behind the proxy |
115117
| `handler`, `salt`, `staticInput`, `hash` | On-chain order parameters |
116118
| `orderType` | TWAP, StopLoss, PerpetualSwap, GoodAfterTime, TradeAboveThreshold, Unknown |
117-
| `status` | **Active** (needs polling), **Cancelled** (on-chain removal), **Invalid** (completed or PollNever) |
119+
| `status` | **Active** (needs polling), **Cancelled** (on-chain removal), **Completed** (all orders terminal or PollNever) |
118120
| `decodedParams` | JSON with decoded staticInput |
121+
| `decodeError` | `"invalid_static_input"` or null |
122+
| `txHash` | FK → transaction.hash |
119123
| `nextCheckBlock` | When the contract poller should next check this generator |
120124
| `nextCheckTimestamp` | For PollTryAtEpoch — stored directly, no estimation |
121125
| `lastCheckBlock`, `lastPollResult` | Audit trail |
@@ -137,7 +141,7 @@ Confirmed orders. API-authoritative status. What consumers query.
137141

138142
### `candidateDiscreteOrder`
139143

140-
Orders discovered on-chain by the Contract Poller but not yet confirmed on the Orderbook API. Same schema as `discreteOrder`. The Candidate Confirmer (C2) promotes them to `discreteOrder` once the API has them.
144+
Orders discovered on-chain (by C1 or UID pre-computation) but not yet confirmed on the Orderbook API. Same schema as `discreteOrder` minus the `status` column — candidates are pending by definition. The Candidate Confirmer (C2) promotes them to `discreteOrder` once the API has them.
141145

142146
### `cow_cache.order_uid_cache`
143147

@@ -171,9 +175,11 @@ Per-UID terminal status cache. Survives Ponder resyncs (external `cow_cache` sch
171175
- Not cached → batch-fetch via `POST /orders/by_uids`
172176
- Cache newly terminal results
173177

174-
5. **Upsert discrete orders.** For each UID, insert/update `discreteOrder` with API status. If not found on API, defaults to `open`.
178+
5. **Insert results.** For each UID:
179+
- **Found on API** → upsert into `discreteOrder` with API-authoritative status.
180+
- **Not found on API** → insert into `candidateDiscreteOrder`. C2 will promote to `discreteOrder` when the API has it.
175181

176-
6. **Generator deactivation.** If ALL orders are terminal → set `status = 'Invalid'`, `allCandidatesKnown = true`, `lastPollResult = 'precompute:allTerminal'`. No further polling needed.
182+
6. **Generator deactivation.** If ALL orders are terminal on the API → set `status = 'Completed'`, `allCandidatesKnown = true`, `lastPollResult = 'precompute:allTerminal'`. No further polling needed. If some UIDs are candidates (not yet on API), the generator stays Active — `allCandidatesKnown` is still set to `true` so C1 skips it.
177183

178184
**Result**: Deterministic orders are fully discovered at creation time. They never need the Contract Poller (C1). The Status Updater (C3) handles any open orders that haven't settled yet.
179185

@@ -203,13 +209,13 @@ Per-UID terminal status cache. Survives Ponder resyncs (external `cow_cache` sch
203209

204210
| Result | Action |
205211
|--------|--------|
206-
| **Success** | Compute `orderUid`, INSERT `candidateDiscreteOrder` (open), schedule recheck |
212+
| **Success** | Compute `orderUid`, INSERT `candidateDiscreteOrder`, schedule recheck |
207213
| **PollTryNextBlock / OrderNotValid / Unknown** | `nextCheckBlock = currentBlock + 1` |
208214
| **PollTryAtBlock(N)** | `nextCheckBlock = N` |
209215
| **PollTryAtEpoch(T)** | `nextCheckTimestamp = T` |
210-
| **PollNever(reason)** | `status = 'Invalid'`. Do NOT expire discrete orders. |
216+
| **PollNever(reason)** | `status = 'Completed'`. Do NOT expire discrete orders. |
211217

212-
4. **Note on `allCandidatesKnown`**: For non-deterministic single-part orders (StopLoss created at live, GoodAfterTime, TradeAboveThreshold), once the contract returns success once, set `allCandidatesKnown = true` — the order UID is now known and C2/C3 handle the rest. For repeating orders (PerpetualSwap), this flag stays false because new orders keep appearing.
218+
4. **Note on `allCandidatesKnown`**: For confirmed single-shot non-deterministic types (**GoodAfterTime**, **TradeAboveThreshold**), once the contract returns success once, set `allCandidatesKnown = true` — the order UID is now known and C2/C3 handle the rest. For repeating orders (**PerpetualSwap**) and **Unknown** types (which may be multi-part), this flag stays `false`.
213219

214220
### 3.4 Candidate Confirmer (C2) — Promoting Candidates
215221

@@ -223,6 +229,8 @@ Per-UID terminal status cache. Survives Ponder resyncs (external `cow_cache` sch
223229

224230
4. **For each NOT found:** Leave as candidate. Will be checked again next block. The watch-tower may not have submitted it yet.
225231

232+
5. **Cleanup:** Delete promoted candidates from `candidateDiscreteOrder`. Also delete stale candidates past their `validTo` — the watch-tower likely never submitted them.
233+
226234
### 3.5 Status Updater (C3) — Tracking Open Orders
227235

228236
**When**: Every block at live sync.
@@ -275,15 +283,15 @@ The Orderbook Client is used by all other components. Two main entry points:
275283

276284
### TWAP — Deterministic, multi-part
277285

278-
**Backfill:** Generator created → all N part UIDs pre-computed → API status fetched → `discreteOrder` rows created → if all terminal, generator marked Invalid.
286+
**Backfill:** Generator created → all N part UIDs pre-computed → API status fetched → UIDs found on API go into `discreteOrder`, UIDs not found go into `candidateDiscreteOrder` → if all terminal on API, generator marked Completed.
279287

280-
**Live sync:** Same as backfill (UID pre-computation works at both). If some parts are still open, C3 (Status Updater) tracks them until fulfilled/expired.
288+
**Live sync:** Same as backfill (UID pre-computation works at both). UIDs not yet on API are candidates; C2 promotes when API confirms. C3 tracks open `discreteOrder` rows.
281289

282290
**The Contract Poller (C1) is NEVER involved for TWAP.** All discovery happens via UID pre-computation.
283291

284292
### StopLoss — Deterministic, single-part
285293

286-
**Backfill:** Generator created → single UID pre-computed → API status fetched → `discreteOrder` created → if terminal, generator marked Invalid.
294+
**Backfill:** Generator created → single UID pre-computed → API status fetched → if found on API, `discreteOrder` created; if not, `candidateDiscreteOrder` → if terminal on API, generator marked Completed.
287295

288296
**Live sync:** Same as backfill. If the order is still open (price hasn't triggered yet), C3 tracks it. The Contract Poller is not involved.
289297

@@ -310,17 +318,18 @@ The Orderbook Client is used by all other components. Two main entry points:
310318
1. Block 18M: `ConditionalOrderCreated`. Generator inserted. UID Pre-computation computes 5 UIDs.
311319
2. API batch fetch: all 5 → `fulfilled`. Cached in `order_uid_cache`.
312320
3. 5 `discreteOrder` rows created (all fulfilled).
313-
4. Generator → `Invalid` (`allCandidatesKnown = true`).
321+
4. Generator → `Completed` (`allCandidatesKnown = true`).
314322
5. **At live sync**: Contract Poller skips this generator. Status Updater has nothing to update. Zero ongoing cost.
315323

316324
### Scenario B: StopLoss, created at live sync, price triggers 3 hours later
317325

318326
1. Live block: `ConditionalOrderCreated`. Generator inserted. UID Pre-computed = `0xabc...`.
319-
2. API fetch: `0xabc` not found (watch-tower hasn't submitted yet). `discreteOrder` inserted as `open`.
320-
3. **C3 (Status Updater) polls every block**: `0xabc` → API still not found or `open`.
327+
2. API fetch: `0xabc` not found (watch-tower hasn't submitted yet). `candidateDiscreteOrder` inserted.
328+
3. **C2 (Candidate Confirmer) polls every block**: `0xabc` → API still not found. Retry next block.
321329
4. 3 hours later: price triggers, watch-tower submits, solver settles.
322-
5. **C3 polls**: `0xabc``fulfilled`. `discreteOrder` updated. Cached as terminal.
323-
6. Generator stays Active until block handler checks and gets `PollNever``Invalid`.
330+
5. **C2 polls**: `0xabc` → API returns order. Promoted to `discreteOrder`. Candidate deleted.
331+
6. **C3 polls**: `0xabc``fulfilled`. `discreteOrder` updated. Cached as terminal.
332+
7. Generator stays Active until block handler checks and gets `PollNever``Completed`.
324333

325334
**Note:** The Contract Poller (C1) is NOT involved. The UID was known from creation. Status tracking is pure API work.
326335

@@ -342,23 +351,25 @@ The Orderbook Client is used by all other components. Two main entry points:
342351

343352
---
344353

345-
## 6. Open Questions
354+
## 6. Design Decisions (Resolved)
355+
356+
### `allCandidatesKnown` — Semantics (Resolved)
346357

347-
### `allCandidatesKnown` — Exact semantics
358+
A **boolean** is the correct type. It answers one question: "does C1 still need to poll this generator?"
348359

349-
**For deterministic types**: Set to `true` at creation time (all UIDs known immediately).
360+
**For deterministic types (TWAP, StopLoss)**: Set to `true` at creation time. All UIDs are computed from `staticInput` — no RPC needed.
350361

351-
**For non-deterministic single-part types**: Set to `true` after the first success from the Contract Poller (the UID is now known).
362+
**For non-deterministic single-part types (GoodAfterTime, TradeAboveThreshold)**: Set to `true` after the first success from the Contract Poller. The UID is now known; C2/C3 handle confirmation and status.
352363

353-
**For PerpetualSwap (repeating)**: Never set to `true` — new orders keep appearing. The Contract Poller must keep polling.
364+
**For PerpetualSwap (repeating)**: Stays `false` — new orders keep appearing. The Contract Poller must keep polling.
354365

355-
**Question**: Should `allCandidatesKnown` be a boolean, or should we store the count of expected parts? For TWAP, we know `n` parts. For StopLoss, we know 1 part. For PerpetualSwap, it's unbounded.
366+
**Why not a part count?** Part count is TWAP-specific. The polling decision is binary: either C1 needs to discover more UIDs, or it doesn't. A part count would be unused for all order types except TWAP and adds no value over the boolean.
356367

357-
### Historical Bootstrap — Completeness
368+
### Historical Bootstrap — Completeness (Resolved)
358369

359-
The bootstrap discovers orders via `fetchComposableOrders(owner)`, which relies on the Orderbook API having the orders. If an order was created and expired without ever being submitted to the API (e.g., the watch-tower was down), it won't be discovered.
370+
The bootstrap discovers orders via `fetchComposableOrders(owner)`, which relies on the Orderbook API having the orders. If an order never reached the API, the bootstrap won't discover it.
360371

361-
**Likely acceptable**: The watch-tower is operated by the CoW Protocol team and is highly available.
372+
**This is correct behavior, not a gap.** All CoW Protocol orders go through the Orderbook API. An order that never reached the API is the same as an order that never existed from the protocol's perspective — it was never submitted to solvers and never had a chance to be settled. The watch-tower is the standard submission path and is operated by the CoW Protocol team.
362373

363374
---
364375

@@ -401,15 +412,15 @@ flowchart TB
401412
subgraph CompE["API Endpoints"]
402413
EGql["/graphql"]
403414
EOwner["/api/orders/by-owner/:owner"]
404-
ESum["/api/generator/:id/summary"]
415+
ESum["/api/generator/:eventId/execution-summary"]
405416
end
406417
407418
CC & CCL --> CompA
408419
GPV & CSF --> OMap
409420
410421
CompA -->|insert| Gen
411422
AB -->|upsert| Disc
412-
AB -.->|all terminal → Invalid| Gen
423+
AB -.->|all terminal → Completed| Gen
413424
414425
BH1 -->|"RPC multicall<br/>(non-deterministic only)"| Cand
415426
BH1 -->|update scheduling| Gen
@@ -441,9 +452,9 @@ flowchart TD
441452
Check -->|"PerpetualSwap / GoodAfterTime<br/>TradeAboveThreshold / Unknown<br/>(non-deterministic)"| Skip["No pre-computation<br/>Generator stays Active"]
442453
443454
Pre --> API["Orderbook Client:<br/>fetchOrderStatusByUids()"]
444-
API --> Upsert["Upsert discreteOrder<br/>for each UID"]
455+
API --> Upsert["API has UID → discreteOrder<br/>API missing UID → candidateDiscreteOrder"]
445456
Upsert --> Term{"All terminal?"}
446-
Term -->|Yes| Deactivate["Generator → Invalid<br/>allCandidatesKnown = true<br/>No further polling"]
457+
Term -->|Yes| Deactivate["Generator → Completed<br/>allCandidatesKnown = true<br/>No further polling"]
447458
Term -->|No| Active["Generator stays Active<br/>C3 tracks open orders"]
448459
449460
Skip --> BackfillQ{"Backfill or<br/>Live sync?"}
@@ -465,16 +476,17 @@ flowchart LR
465476
direction TB
466477
C1Q["Query: Active generators<br/>non-deterministic<br/>allCandidatesKnown=false<br/>due for check"]
467478
C1M["RPC multicall:<br/>getTradeableOrderWithSignature"]
468-
C1R["On success → candidate<br/>On PollNever → Invalid"]
479+
C1R["On success → candidate<br/>On PollNever → Completed"]
469480
C1Q --> C1M --> C1R
470481
end
471482
472483
subgraph C2["C2: Candidate Confirmer"]
473484
direction TB
474485
C2Q["Query: candidateDiscreteOrder<br/>not yet in discreteOrder"]
475486
C2F["API: POST /orders/by_uids"]
476-
C2U["Found → upsert discreteOrder<br/>Not found → retry next block"]
477-
C2Q --> C2F --> C2U
487+
C2U["Found → upsert discreteOrder<br/>+ delete candidate<br/>Not found → retry next block"]
488+
C2X["Delete stale candidates<br/>past validTo"]
489+
C2Q --> C2F --> C2U --> C2X
478490
end
479491
480492
subgraph C3["C3: Status Updater"]
@@ -512,7 +524,7 @@ flowchart LR
512524
E3["Pre-compute 5 UIDs"]
513525
E4["Fetch API status"]
514526
E5{"All terminal?"}
515-
E6["Generator → Invalid"]
527+
E6["Generator → Completed"]
516528
E7["Some open"]
517529
E1 --> E2 --> E3 --> E4 --> E5
518530
E5 -->|Yes| E6
@@ -545,12 +557,13 @@ flowchart TD
545557
E["ConditionalOrderCreated<br/>(StopLoss, live)"] --> G["Insert generator"]
546558
G --> Pre["Pre-compute UID = 0xabc"]
547559
Pre --> API["API fetch: 0xabc not found yet"]
548-
API --> Open["discreteOrder: open"]
560+
API --> Cand["candidateDiscreteOrder"]
549561
550-
Open --> C3["C3: polls API every block"]
551-
C3 --> Wait["0xabc still open..."]
562+
Cand --> C2["C2: polls API every block"]
563+
C2 --> Wait["0xabc not on API yet..."]
552564
Wait --> Trigger["Price triggers →<br/>watch-tower submits →<br/>solver settles"]
553-
Trigger --> Fulfilled["C3: API returns fulfilled<br/>→ update discreteOrder"]
565+
Trigger --> Promoted["C2: API returns order<br/>→ promote to discreteOrder"]
566+
Promoted --> Fulfilled["C3: API returns fulfilled<br/>→ update discreteOrder"]
554567
555568
style Trigger fill:#fff3cd
556569
style Fulfilled fill:#d4edda
@@ -614,12 +627,14 @@ flowchart LR
614627
end
615628
616629
A -->|"INSERT (Active)"| Gen
617-
B -->|"UPDATE (Invalid)"| Gen
630+
B -->|"UPDATE (Completed)"| Gen
618631
C1 -->|"UPDATE (scheduling, status)"| Gen
619632
620-
C1 -->|"INSERT (open)"| Cand
633+
B -->|"INSERT (API missing)"| Cand
634+
C1 -->|"INSERT"| Cand
635+
C2 -->|"DELETE (promoted + stale)"| Cand
621636
622-
B -->|"UPSERT"| Disc
637+
B -->|"UPSERT (API found)"| Disc
623638
C2 -->|"UPSERT"| Disc
624639
C3 -->|"UPDATE status"| Disc
625640
C4 -->|"UPSERT"| Disc

0 commit comments

Comments
 (0)