From c25b9075fd5f3f7e38717dce83326dbb8761e113 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 23 Jul 2026 10:49:27 -0300 Subject: [PATCH] feat(schema): store discreteOrder executed amounts as bigint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit executedSellAmount / executedBuyAmount / executedFee move from t.text() to t.bigint() (numeric(78) in Postgres), so the DB sorts and aggregates them as numbers and the TWAP totals query no longer needs ::numeric casts. Conversions happen at the boundaries: - orderbook API strings -> bigint via toBigIntOrNull (filterAndProcess, fetchOrderStatusByUids, fetchOwnerOrderStatuses, remap) - cow_cache tables keep TEXT columns; toCacheRow / cacheUidStatuses stringify on write, reads convert back - the /orders-by-owner REST endpoint stringifies for JSON (same treatment as creationDate/blockNumber) Note: GraphQL output is unchanged — Ponder exposes bigint columns as the BigInt scalar, which serializes as a decimal string in JSON (JSON numbers can't hold uint256). The flash-loan order table still stores text amounts; converting it is a separate change if wanted. --- schema/tables.ts | 6 +-- src/api/endpoints/orders-by-owner.ts | 2 + src/application/helpers/executedAmounts.ts | 6 +-- src/application/helpers/orderbook/cache.ts | 15 +++--- src/application/helpers/orderbook/client.ts | 25 +++++----- .../helpers/orderbook/processing.ts | 13 ++--- src/application/helpers/orderbook/types.ts | 15 ++++-- tests/helpers/orderbookClient.test.ts | 48 +++++++++---------- 8 files changed, 71 insertions(+), 59 deletions(-) diff --git a/schema/tables.ts b/schema/tables.ts index 5dd89f3..d2dfb83 100644 --- a/schema/tables.ts +++ b/schema/tables.ts @@ -155,9 +155,9 @@ export const discreteOrder = onchainTable( feeAmount: t.text().notNull(), validTo: t.integer(), // uint32 Unix timestamp — from API or getTradeableOrderWithSignature creationDate: t.bigint().notNull(), // block timestamp (seconds) - executedSellAmount: t.text(), // actual executed amount (from API, post-settlement) - executedBuyAmount: t.text(), // actual executed amount (from API, post-settlement) - executedFee: t.text(), // actual fee taken from surplus (API executedFee; the legacy executedFeeAmount is always "0") + executedSellAmount: t.bigint(), // actual executed amount (from API, post-settlement) + executedBuyAmount: t.bigint(), // actual executed amount (from API, post-settlement) + executedFee: t.bigint(), // actual fee taken from surplus (API executedFee; the legacy executedFeeAmount is always "0") promotedAt: t.bigint(), // block timestamp when CandidateConfirmer promoted from candidate; null = created directly (precompute or OwnerBackfill) // Sync cursor: the indexer's processing block of the last insert or // status/executed-amount change. Every change here also bumps the parent diff --git a/src/api/endpoints/orders-by-owner.ts b/src/api/endpoints/orders-by-owner.ts index 7d6057b..b10560c 100644 --- a/src/api/endpoints/orders-by-owner.ts +++ b/src/api/endpoints/orders-by-owner.ts @@ -98,6 +98,8 @@ export const ordersByOwnerHandler: RouteHandler< enrichedOrders = orders.map((o) => ({ ...o, creationDate: o.creationDate.toString(), + executedSellAmount: o.executedSellAmount?.toString() ?? null, + executedBuyAmount: o.executedBuyAmount?.toString() ?? null, generator: generatorById[o.generatorId], })); } diff --git a/src/application/helpers/executedAmounts.ts b/src/application/helpers/executedAmounts.ts index 818680d..19378a3 100644 --- a/src/application/helpers/executedAmounts.ts +++ b/src/application/helpers/executedAmounts.ts @@ -51,9 +51,9 @@ export async function refreshTwapExecutedTotals( const rows = await context.db.sql .select({ generatorId: discreteOrder.conditionalOrderGeneratorId, - executedSellAmount: sql`coalesce(sum(${discreteOrder.executedSellAmount}::numeric), 0)::text`.as("executed_sell_amount_sum"), - executedBuyAmount: sql`coalesce(sum(${discreteOrder.executedBuyAmount}::numeric), 0)::text`.as("executed_buy_amount_sum"), - executedFee: sql`coalesce(sum(${discreteOrder.executedFee}::numeric), 0)::text`.as("executed_fee_sum"), + executedSellAmount: sql`coalesce(sum(${discreteOrder.executedSellAmount}), 0)::text`.as("executed_sell_amount_sum"), + executedBuyAmount: sql`coalesce(sum(${discreteOrder.executedBuyAmount}), 0)::text`.as("executed_buy_amount_sum"), + executedFee: sql`coalesce(sum(${discreteOrder.executedFee}), 0)::text`.as("executed_fee_sum"), }) .from(discreteOrder) .where( diff --git a/src/application/helpers/orderbook/cache.ts b/src/application/helpers/orderbook/cache.ts index 2f0cd6b..1f0478c 100644 --- a/src/application/helpers/orderbook/cache.ts +++ b/src/application/helpers/orderbook/cache.ts @@ -11,7 +11,8 @@ import { type FlashLoanEnrichment, } from "./types"; -/** Project a freshly-decoded ComposableOrder into the durable-cache row shape. */ +/** Project a freshly-decoded ComposableOrder into the durable-cache row shape. + * The cache tables store amounts as TEXT — bigints convert at this boundary. */ export function toCacheRow(o: ComposableOrder): ComposableCacheRow { return { orderUid: o.uid, @@ -23,9 +24,9 @@ export function toCacheRow(o: ComposableOrder): ComposableCacheRow { feeAmount: o.feeAmount, validTo: o.validTo ?? null, creationDate: o.creationDate, - executedSellAmount: o.executedSellAmount ?? null, - executedBuyAmount: o.executedBuyAmount ?? null, - executedFee: o.executedFee ?? null, + executedSellAmount: o.executedSellAmount?.toString() ?? null, + executedBuyAmount: o.executedBuyAmount?.toString() ?? null, + executedFee: o.executedFee?.toString() ?? null, }; } @@ -228,9 +229,9 @@ export async function cacheUidStatuses( orderUid: order.uid, status: order.status, fetchedAt: now, - executedSellAmount: order.executedSellAmount, - executedBuyAmount: order.executedBuyAmount, - executedFee: order.executedFee, + executedSellAmount: order.executedSellAmount?.toString() ?? null, + executedBuyAmount: order.executedBuyAmount?.toString() ?? null, + executedFee: order.executedFee?.toString() ?? null, }))) .onConflictDoUpdate({ target: [orderUidCache.chainId, orderUidCache.orderUid], diff --git a/src/application/helpers/orderbook/client.ts b/src/application/helpers/orderbook/client.ts index 8af73b6..0cd61ec 100644 --- a/src/application/helpers/orderbook/client.ts +++ b/src/application/helpers/orderbook/client.ts @@ -53,6 +53,7 @@ import { import { PAGE_LIMIT, TERMINAL_STATUSES, + toBigIntOrNull, type ComposableOrder, type FlashLoanEnrichment, type OrderStatusInfo, @@ -338,9 +339,9 @@ export async function fetchOrderStatusByUids( if (cachedData && TERMINAL_STATUSES.has(cachedData.status)) { const info: OrderStatusInfo = { status: cachedData.status, - executedSellAmount: cachedData.executedSellAmount, - executedBuyAmount: cachedData.executedBuyAmount, - executedFee: cachedData.executedFee, + executedSellAmount: toBigIntOrNull(cachedData.executedSellAmount), + executedBuyAmount: toBigIntOrNull(cachedData.executedBuyAmount), + executedFee: toBigIntOrNull(cachedData.executedFee), }; if (cachedData.status === "fulfilled" && cachedData.executedFee == null) { staleFallbacks.set(uid, info); @@ -380,9 +381,9 @@ export async function fetchOrderStatusByUids( for (const order of fetched) { result.set(order.uid, { status: order.status, - executedSellAmount: order.executedSellAmount, - executedBuyAmount: order.executedBuyAmount, - executedFee: order.executedFee, + executedSellAmount: toBigIntOrNull(order.executedSellAmount), + executedBuyAmount: toBigIntOrNull(order.executedBuyAmount), + executedFee: toBigIntOrNull(order.executedFee), }); if (TERMINAL_STATUSES.has(order.status)) { newTerminal.push({ @@ -396,9 +397,9 @@ export async function fetchOrderStatusByUids( feeAmount: order.feeAmount, validTo: order.validTo, creationDate: 0n, - executedSellAmount: order.executedSellAmount, - executedBuyAmount: order.executedBuyAmount, - executedFee: order.executedFee, + executedSellAmount: toBigIntOrNull(order.executedSellAmount), + executedBuyAmount: toBigIntOrNull(order.executedBuyAmount), + executedFee: toBigIntOrNull(order.executedFee), }); } } @@ -436,9 +437,9 @@ export async function fetchOwnerOrderStatuses( for (const order of orders) { result.set(order.uid, { status: order.status, - executedSellAmount: order.executedSellAmount, - executedBuyAmount: order.executedBuyAmount, - executedFee: order.executedFee, + executedSellAmount: toBigIntOrNull(order.executedSellAmount), + executedBuyAmount: toBigIntOrNull(order.executedBuyAmount), + executedFee: toBigIntOrNull(order.executedFee), }); } return result; diff --git a/src/application/helpers/orderbook/processing.ts b/src/application/helpers/orderbook/processing.ts index 7111fb9..91277ed 100644 --- a/src/application/helpers/orderbook/processing.ts +++ b/src/application/helpers/orderbook/processing.ts @@ -12,6 +12,7 @@ import { fetchOrdersByUids } from "./http"; import { upsertComposableCache } from "./cache"; import { TERMINAL_STATUSES, + toBigIntOrNull, type ComposableCacheRow, type ComposableOrder, type OrderbookOrder, @@ -102,9 +103,9 @@ export async function filterAndProcess( feeAmount: order.feeAmount, validTo: order.validTo, creationDate: BigInt(Math.floor(new Date(order.creationDate).getTime() / 1000)), - executedSellAmount: order.executedSellAmount, - executedBuyAmount: order.executedBuyAmount, - executedFee: order.executedFee, + executedSellAmount: toBigIntOrNull(order.executedSellAmount), + executedBuyAmount: toBigIntOrNull(order.executedBuyAmount), + executedFee: toBigIntOrNull(order.executedFee), }); } @@ -197,9 +198,9 @@ export async function remapToCurrentGenerators( feeAmount: row.feeAmount, validTo: row.validTo, creationDate: row.creationDate, - executedSellAmount: row.executedSellAmount, - executedBuyAmount: row.executedBuyAmount, - executedFee: row.executedFee, + executedSellAmount: toBigIntOrNull(row.executedSellAmount), + executedBuyAmount: toBigIntOrNull(row.executedBuyAmount), + executedFee: toBigIntOrNull(row.executedFee), }); } return results; diff --git a/src/application/helpers/orderbook/types.ts b/src/application/helpers/orderbook/types.ts index 116e70a..f862a79 100644 --- a/src/application/helpers/orderbook/types.ts +++ b/src/application/helpers/orderbook/types.ts @@ -34,12 +34,19 @@ export type ComposableOrder = Pick< creationDate: bigint; }; -/** Status + executed amounts returned by fetchOrderStatusByUids. */ +/** Status + executed amounts returned by fetchOrderStatusByUids. + * Amounts are bigint (matching the discreteOrder columns); null when the + * cached entry predates the executed columns. */ export interface OrderStatusInfo { status: string; - executedSellAmount: string | null; // null when served from cache - executedBuyAmount: string | null; - executedFee: string | null; + executedSellAmount: bigint | null; + executedBuyAmount: bigint | null; + executedFee: bigint | null; +} + +/** API/cache decimal string -> bigint at the storage boundary. */ +export function toBigIntOrNull(value: string | null | undefined): bigint | null { + return value == null ? null : BigInt(value); } /** CoW-order fields used to enrich a flash-loan order, from the orderbook. */ diff --git a/tests/helpers/orderbookClient.test.ts b/tests/helpers/orderbookClient.test.ts index 96d1fcc..d734f9d 100644 --- a/tests/helpers/orderbookClient.test.ts +++ b/tests/helpers/orderbookClient.test.ts @@ -172,9 +172,9 @@ describe("fetchOrderStatusByUids", () => { try { const result = await fetchOrderStatusByUids(makeContext(), TEST_CHAIN_ID, [UID_A]); const info = result.get(UID_A); - expect(info?.executedSellAmount).toBe("1000000000000000000"); - expect(info?.executedBuyAmount).toBe("2000000000000000000"); - expect(info?.executedFee).toBe("1000000000000000"); + expect(info?.executedSellAmount).toBe(1000000000000000000n); + expect(info?.executedBuyAmount).toBe(2000000000000000000n); + expect(info?.executedFee).toBe(1000000000000000n); } finally { await close(); } @@ -267,9 +267,9 @@ describe("fetchOrderStatusByUids — stale fulfilled cache entries", () => { expect(calls).toBe(1); // stale entry treated as a miss expect(result.get(UID_A)).toEqual({ status: "fulfilled", - executedSellAmount: "1000000000000000000", - executedBuyAmount: "2000000000000000000", - executedFee: "1000000000000000", + executedSellAmount: 1000000000000000000n, + executedBuyAmount: 2000000000000000000n, + executedFee: 1000000000000000n, }); } finally { await close(); @@ -289,8 +289,8 @@ describe("fetchOrderStatusByUids — stale fulfilled cache entries", () => { const result = await fetchOrderStatusByUids(ctx, TEST_CHAIN_ID, [UID_A]); expect(result.get(UID_A)).toEqual({ status: "fulfilled", - executedSellAmount: "1", - executedBuyAmount: "2", + executedSellAmount: 1n, + executedBuyAmount: 2n, executedFee: null, }); } finally { @@ -312,7 +312,7 @@ describe("fetchOrderStatusByUids — stale fulfilled cache entries", () => { try { const result = await fetchOrderStatusByUids(ctx, TEST_CHAIN_ID, [UID_A]); expect(calls).toBe(0); // no network — cache is complete - expect(result.get(UID_A)?.executedFee).toBe("3"); + expect(result.get(UID_A)?.executedFee).toBe(3n); } finally { await close(); } @@ -428,21 +428,21 @@ describe("fetchOwnerOrderStatuses", () => { expect(result.get("0xuid1")).toEqual({ status: "fulfilled", - executedSellAmount: "500", - executedBuyAmount: "1000", - executedFee: "0", + executedSellAmount: 500n, + executedBuyAmount: 1000n, + executedFee: 0n, }); expect(result.get("0xuid2")).toEqual({ status: "open", - executedSellAmount: "0", - executedBuyAmount: "0", - executedFee: "0", + executedSellAmount: 0n, + executedBuyAmount: 0n, + executedFee: 0n, }); expect(result.get("0xuid3")).toEqual({ status: "expired", - executedSellAmount: "250", - executedBuyAmount: "500", - executedFee: "0", + executedSellAmount: 250n, + executedBuyAmount: 500n, + executedFee: 0n, }); }); } finally { @@ -539,9 +539,9 @@ describe("fetchOwnerOrderStatuses", () => { expect(result.get("0xpage2-0")).toEqual({ status: "fulfilled", - executedSellAmount: "999", - executedBuyAmount: "888", - executedFee: "0", + executedSellAmount: 999n, + executedBuyAmount: 888n, + executedFee: 0n, }); }); } finally { @@ -823,7 +823,7 @@ describe("drainOwnerSlice — delta mode (fully drained owner)", () => { expect(row).toBeDefined(); expect(row!.conditionalOrderGeneratorId).toBe("gen-current"); expect(row!.status).toBe("fulfilled"); - expect(row!.executedFee).toBe("17"); + expect(row!.executedFee).toBe(17n); }); } finally { await close(); @@ -947,8 +947,8 @@ describe("upsertDiscreteOrders — chunking", () => { feeAmount: "0", validTo: 9999999999, creationDate: 1700000000n, - executedSellAmount: "0", - executedBuyAmount: "0", + executedSellAmount: 0n, + executedBuyAmount: 0n, })); const { ctx, inserted, statementCount } = makeDrainContext();