Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/application/handlers/blockHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);

Expand Down Expand Up @@ -112,7 +113,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => {
handler: Hex;
salt: Hex;
staticInput: Hex;
orderType: string;
orderType: OrderType;
decodedParams: Record<string, string> | null;
consecutiveTryNextBlock: number;
}[];
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 10 additions & 8 deletions src/application/handlers/composableCow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it worth to log this on a issue

*
* 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.
*
*/

Expand Down Expand Up @@ -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",
Expand Down
21 changes: 1 addition & 20 deletions src/application/handlers/settlement.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -191,32 +191,13 @@ 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();

console.log(
`[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}`,
);
Expand Down
7 changes: 4 additions & 3 deletions src/application/helpers/orderbookClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -52,7 +53,7 @@ export type ComposableOrder = Pick<
uid: string;
generatorId: string;
generatorHash: string;
orderType: string;
orderType: OrderType;
creationDate: number;
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -432,7 +433,7 @@ async function filterAndProcess(
)
.limit(1)) as {
eventId: string;
orderType: string;
orderType: OrderType;
}[];

if (generators.length === 0) continue;
Expand Down
8 changes: 4 additions & 4 deletions src/application/helpers/uidPrecompute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,12 +55,12 @@ export interface PrecomputedOrder {
export function precomputeOrderUids(
chainId: number,
owner: Hex,
orderType: string,
orderType: OrderType,
decodedParams: Record<string, string> | 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;
Expand Down Expand Up @@ -93,7 +93,7 @@ export async function precomputeAndDiscover(
chainId: number,
generatorEventId: string,
owner: Hex,
orderType: string,
orderType: OrderType,
decodedParams: Record<string, string> | null,
blockTimestamp: bigint,
): Promise<boolean> {
Expand Down
16 changes: 11 additions & 5 deletions src/utils/order-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderType>(["TWAP", "StopLoss"]);

export function isDeterministicOrderType(orderType: string): boolean {
return DETERMINISTIC_ORDER_TYPES.has(orderType as OrderType);
}
export const DETERMINISTIC_ORDER_TYPE: Record<OrderType, boolean> = {
TWAP: true,
StopLoss: true,
CirclesBackingOrder: true,
PerpetualSwap: false,
GoodAfterTime: false,
TradeAboveThreshold: false,
SwapOrderHandler: false,
ERC4626CowSwapFeeBurner: false,
Unknown: false,
};
30 changes: 30 additions & 0 deletions tests/utils/order-types.test.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh I don't think this tests are relevant. Instead, we should properly handle types related to order types and deterministic order types.

Two things that would make it better:

  • replace isDeterministicOrderType to a Record<OrderType, boolean>
  • in multiple places, order type is being typed as string while OrderType should be used

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my understanding, the DETERMINISTIC_ORDER_TYPE is already pretty direct and we don't need to create a test for double checking it.

Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading