diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index 817453c..054b9cc 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -41,9 +41,10 @@ import { parsePollError, } from "../helpers/pollResultErrors"; import { computeOrderUid, type GPv2OrderData } from "../helpers/orderUid"; +import { type OrderType } from "../../utils/order-types"; -const NON_DETERMINISTIC_TYPES = ["PerpetualSwap", "GoodAfterTime", "TradeAboveThreshold", "Unknown"] as const; -const SINGLE_SHOT_NON_DETERMINISTIC = ["GoodAfterTime", "TradeAboveThreshold"] as const; +const NON_DETERMINISTIC_TYPES: readonly OrderType[] = ["PerpetualSwap", "GoodAfterTime", "TradeAboveThreshold", "Unknown"]; +const SINGLE_SHOT_NON_DETERMINISTIC: readonly OrderType[] = ["GoodAfterTime", "TradeAboveThreshold"]; const BLOCK_NEVER = 2n ** 63n - 1n; // sentinel for epoch-scheduled generators (PollTryAtEpoch) const VALID_DISCRETE_STATUSES = new Set(["fulfilled", "unfilled", "expired", "cancelled"]); @@ -112,7 +113,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { handler: Hex; salt: Hex; staticInput: Hex; - orderType: string; + orderType: OrderType; decodedParams: Record | null; consecutiveTryNextBlock: number; }[]; @@ -197,7 +198,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { .onConflictDoNothing(), ); - const isSingleShot = (SINGLE_SHOT_NON_DETERMINISTIC as readonly string[]).includes(order.orderType); + const isSingleShot = SINGLE_SHOT_NON_DETERMINISTIC.includes(order.orderType); successPromises.push( updateGeneratorPollState(context, chainId, order.generatorId, currentBlock, { nextCheckBlock: currentBlock + RECHECK_INTERVAL, @@ -728,7 +729,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { ) as { generatorId: string; owner: Hex; - orderType: string; + orderType: OrderType; }[]; // Exclude owners already retried above — they were just attempted this run @@ -820,7 +821,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = generatorId: string; owner: Hex; hash: Hex; - orderType: string; + orderType: OrderType; }[]; if (dueGenerators.length === 0) return; diff --git a/src/application/handlers/composableCow.ts b/src/application/handlers/composableCow.ts index 44f7858..61b72a0 100644 --- a/src/application/handlers/composableCow.ts +++ b/src/application/handlers/composableCow.ts @@ -8,8 +8,8 @@ * * For deterministic types (TWAP, StopLoss, CirclesBackingOrder), precomputeAndDiscover * computes all UIDs, fetches their status from the API, upserts discrete orders, and marks - * allCandidatesKnown=true. Non-deterministic types are left for the C1-C4 block handlers to - * discover at live sync. + * allCandidatesKnown=true. Non-deterministic types are left for the OrderDiscoveryPoller + * block handler to discover at live sync. * * CirclesBackingOrder (Gnosis only) additionally reads two constructor immutables * (SELL_TOKEN, SELL_AMOUNT) from the handler contract at creation time and merges them @@ -24,12 +24,14 @@ * * This affects only EIP-1271 composable orders where the user cancels through * the API rather than calling ComposableCoW.remove() on-chain. In practice - * this is rare — the standard cancellation path for composable orders is - * on-chain, which emits ConditionalOrderCancelled (handled elsewhere) or - * triggers PollNever in the block handler. + * this is rare — the standard on-chain cancellation path is detected via + * SingleOrderNotAuthed (OrderDiscoveryPoller) and the CancellationWatcher, + * both of which work correctly. * - * If this gap proves significant in production, a lightweight periodic check - * can be added for owners with open orders. Track via issue tracker if needed. + * A newer ComposableCoW contract version (nullislabs/composable-cow#1) emits a + * ConditionalOrderRemoved event from remove(), which would allow the indexer to + * detect on-chain cancellations directly without polling. Supporting this contract + * version is tracked as a future improvement. * */ @@ -253,7 +255,7 @@ ponder.on( // ─── Live handler (ComposableCowLive — startBlock: "latest") ──────────────── // Same as backfill: pre-compute covers deterministic types. -// Non-deterministic types are discovered by C1-C4 block handlers at live sync. +// Non-deterministic types are discovered by the OrderDiscoveryPoller block handler at live sync. ponder.on( "ComposableCowLive:ConditionalOrderCreated", diff --git a/src/application/handlers/settlement.ts b/src/application/handlers/settlement.ts index 7304b07..60ffda5 100644 --- a/src/application/handlers/settlement.ts +++ b/src/application/handlers/settlement.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; import { AddressType, conditionalOrderGenerator, ownerMapping, transaction } from "ponder:schema"; import { and, eq } from "ponder"; -import { decodeAbiParameters, keccak256, toBytes } from "viem"; +import { keccak256, toBytes } from "viem"; import { AaveV3AdapterHelperAbi } from "../../../abis/AaveV3AdapterHelperAbi"; import { AAVE_V3_ADAPTER_FACTORY_ADDRESSES, @@ -191,20 +191,6 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { ), ); - // Decode non-indexed Trade log fields: sellToken, buyToken, amounts, orderUid - const [sellToken, buyToken, sellAmount, buyAmount, , orderUid] = - decodeAbiParameters( - [ - { type: "address" }, - { type: "address" }, - { type: "uint256" }, - { type: "uint256" }, - { type: "uint256" }, - { type: "bytes" }, - ], - log.data, - ); - stats.mapped++; logStatsIfIntervalPassed(); @@ -212,11 +198,6 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { `[COW:SETTLEMENT:TRADE] AAVE_ADAPTER_MAPPED` + ` adapter=${ownerAddress}` + ` eoa=${eoaOwner.toLowerCase()}` + - ` orderUid=${orderUid}` + - ` sellToken=${sellToken.toLowerCase()}` + - ` buyToken=${buyToken.toLowerCase()}` + - ` sellAmount=${sellAmount}` + - ` buyAmount=${buyAmount}` + ` block=${event.block.number}` + ` chain=${chainId}`, ); diff --git a/src/application/helpers/orderbookClient.ts b/src/application/helpers/orderbookClient.ts index 5ef7b2d..0dd0771 100644 --- a/src/application/helpers/orderbookClient.ts +++ b/src/application/helpers/orderbookClient.ts @@ -20,6 +20,7 @@ import { } from "ponder:schema"; import { and, eq, sql } from "ponder"; import { encodeAbiParameters, keccak256, type Hex } from "viem"; +import { type OrderType } from "../../utils/order-types"; import { COMPOSABLE_COW_HANDLER_ADDRESSES, ORDERBOOK_API_URLS } from "../../data"; import { ORDERBOOK_HTTP_TIMEOUT_MS, SIGNING_SCHEME_EIP1271 } from "../../constants"; import { decodeEip1271Signature } from "../decoders/erc1271Signature"; @@ -52,7 +53,7 @@ export type ComposableOrder = Pick< uid: string; generatorId: string; generatorHash: string; - orderType: string; + orderType: OrderType; creationDate: number; }; @@ -274,7 +275,7 @@ export async function fetchOrderStatusByUids( status: order.status as ComposableOrder["status"], generatorId: "", generatorHash: "", - orderType: "", + orderType: "Unknown", sellAmount: order.sellAmount, buyAmount: order.buyAmount, feeAmount: order.feeAmount, @@ -432,7 +433,7 @@ async function filterAndProcess( ) .limit(1)) as { eventId: string; - orderType: string; + orderType: OrderType; }[]; if (generators.length === 0) continue; diff --git a/src/application/helpers/uidPrecompute.ts b/src/application/helpers/uidPrecompute.ts index 80ca37e..bd25086 100644 --- a/src/application/helpers/uidPrecompute.ts +++ b/src/application/helpers/uidPrecompute.ts @@ -20,7 +20,7 @@ import { and, eq } from "ponder"; import { candidateDiscreteOrder, conditionalOrderGenerator, discreteOrder } from "ponder:schema"; import { computeOrderUid, type GPv2OrderData } from "./orderUid"; import { fetchOrderStatusByUids } from "./orderbookClient"; -import { isDeterministicOrderType } from "../../utils/order-types"; +import { type OrderType, DETERMINISTIC_ORDER_TYPE } from "../../utils/order-types"; // GPv2Order.sol constant hashes const KIND_SELL = "0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775" as Hex; @@ -55,12 +55,12 @@ export interface PrecomputedOrder { export function precomputeOrderUids( chainId: number, owner: Hex, - orderType: string, + orderType: OrderType, decodedParams: Record | null, blockTimestamp: bigint, ): PrecomputedOrder[] | null { if (!decodedParams) { - if (isDeterministicOrderType(orderType)) { + if (DETERMINISTIC_ORDER_TYPE[orderType]) { console.warn(`[COW:PRECOMPUTE] SKIP type=${orderType} owner=${owner} chain=${chainId} reason=decodedParams_null`); } return null; @@ -93,7 +93,7 @@ export async function precomputeAndDiscover( chainId: number, generatorEventId: string, owner: Hex, - orderType: string, + orderType: OrderType, decodedParams: Record | null, blockTimestamp: bigint, ): Promise { diff --git a/src/utils/order-types.ts b/src/utils/order-types.ts index 707eabf..93cd1b3 100644 --- a/src/utils/order-types.ts +++ b/src/utils/order-types.ts @@ -77,8 +77,14 @@ export function getOrderTypeFromHandler( // Single source of truth for which order types have UIDs computable from staticInput // alone (no on-chain calls). Keep in sync with the switch in `precomputeOrderUids`. -export const DETERMINISTIC_ORDER_TYPES = new Set(["TWAP", "StopLoss"]); - -export function isDeterministicOrderType(orderType: string): boolean { - return DETERMINISTIC_ORDER_TYPES.has(orderType as OrderType); -} +export const DETERMINISTIC_ORDER_TYPE: Record = { + TWAP: true, + StopLoss: true, + CirclesBackingOrder: true, + PerpetualSwap: false, + GoodAfterTime: false, + TradeAboveThreshold: false, + SwapOrderHandler: false, + ERC4626CowSwapFeeBurner: false, + Unknown: false, +}; diff --git a/tests/utils/order-types.test.ts b/tests/utils/order-types.test.ts new file mode 100644 index 0000000..46e39d0 --- /dev/null +++ b/tests/utils/order-types.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { + DETERMINISTIC_ORDER_TYPE, + type OrderType, +} from "../../src/utils/order-types"; + +describe("DETERMINISTIC_ORDER_TYPE", () => { + it("covers every OrderType (exhaustive record)", () => { + // If a new OrderType is added to the union without updating the record, + // TypeScript will catch it at compile time. This test documents the intent. + const types = Object.keys(DETERMINISTIC_ORDER_TYPE) as OrderType[]; + expect(types.length).toBeGreaterThan(0); + }); + + it("marks TWAP, StopLoss, CirclesBackingOrder as deterministic", () => { + expect(DETERMINISTIC_ORDER_TYPE["TWAP"]).toBe(true); + expect(DETERMINISTIC_ORDER_TYPE["StopLoss"]).toBe(true); + // Regression guard for COW-1003: CirclesBackingOrder must be deterministic + expect(DETERMINISTIC_ORDER_TYPE["CirclesBackingOrder"]).toBe(true); + }); + + it("marks non-deterministic types as false", () => { + expect(DETERMINISTIC_ORDER_TYPE["PerpetualSwap"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["GoodAfterTime"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["TradeAboveThreshold"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["SwapOrderHandler"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["ERC4626CowSwapFeeBurner"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["Unknown"]).toBe(false); + }); +});