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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ jobs:
env:
# Public RPC
MAINNET_RPC_URL: https://eth.api.pocket.network

- name: Test
run: pnpm test
2 changes: 1 addition & 1 deletion src/application/decoders/erc1271Signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { decodeAbiParameters, type Hex } from "viem";
// GPv2Order.Data: 12 fixed-size fields × 32 bytes = 384 bytes total (used for byte-offset math)
const GPV2_ORDER_BYTES = 384;

const PAYLOAD_STRUCT_ABI = [
export const PAYLOAD_STRUCT_ABI = [
{
type: "tuple" as const,
name: "payload",
Expand Down
14 changes: 14 additions & 0 deletions tests/__mocks__/ponder-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { vi } from "vitest";

// Chainable select stub — each .select() call gets its own independent chain so
// tests can use mockReturnValueOnce to return different rows per query.
export function makeSelectChain(rows: unknown[] = []) {
const where = vi.fn().mockResolvedValue(rows);
const from = vi.fn().mockReturnValue({ where });
return { from };
}

export const db = {
execute: vi.fn().mockResolvedValue({ rows: [] }),
select: vi.fn().mockReturnValue(makeSelectChain()),
};
106 changes: 106 additions & 0 deletions tests/api/execution-summary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { OpenAPIHono } from "@hono/zod-openapi";
import { z } from "zod";

// Mock virtual modules before any ponder-importing source files are loaded.
vi.mock("ponder:api", () => ({ db: { execute: vi.fn() } }));
vi.mock("ponder", () => ({
sql: Object.assign(
(_s: TemplateStringsArray, ..._v: unknown[]) => ({}),
{ raw: (_s: string) => ({}) },
),
}));

import { db } from "ponder:api";
import { executionSummaryRoute } from "../../src/api/routes";
import { executionSummaryHandler } from "../../src/api/endpoints/execution-summary";
import { DiscreteOrderStatusQuery } from "../../src/api/schemas/common";

const Status = DiscreteOrderStatusQuery.enum;
type StatusRow = { status: z.infer<typeof DiscreteOrderStatusQuery>; count: string };

function buildApp() {
const app = new OpenAPIHono();
app.openapi(executionSummaryRoute, executionSummaryHandler);
return app;
}

const EVENT_ID = "177991282000000000000001000000000046395020000000000000001750000000000000054";

function makeUrl(eventId = EVENT_ID, chainId = 1) {
return `http://localhost/generator/${eventId}/execution-summary?chainId=${chainId}`;
}

beforeEach(() => {
vi.mocked(db.execute).mockReset();
});

describe("GET /api/generator/:eventId/execution-summary", () => {
it("returns all-zero counts when no discrete orders exist", async () => {
vi.mocked(db.execute).mockResolvedValue({ rows: [] } as never);

const res = await buildApp().request(makeUrl());
expect(res.status).toBe(200);

const body = await res.json() as Record<string, unknown>;
expect(body["totalParts"]).toBe(0);
expect(body["filledParts"]).toBe(0);
expect(body["openParts"]).toBe(0);
expect(body["unfilledParts"]).toBe(0);
expect(body["expiredParts"]).toBe(0);
expect(body["cancelledParts"]).toBe(0);
Comment on lines +46 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

couldn't this status type be a enum and imported?

});

it("maps fulfilled, expired, open, unfilled, cancelled to the right fields", async () => {
const rows: StatusRow[] = [
{ status: Status.fulfilled, count: "3" },
{ status: Status.expired, count: "7" },
{ status: Status.open, count: "2" },
];
vi.mocked(db.execute).mockResolvedValue({ rows } as never);

const body = await (await buildApp().request(makeUrl())).json() as Record<string, unknown>;

expect(body["filledParts"]).toBe(3);
expect(body["expiredParts"]).toBe(7);
expect(body["openParts"]).toBe(2);
expect(body["unfilledParts"]).toBe(0);
expect(body["cancelledParts"]).toBe(0);
expect(body["totalParts"]).toBe(12);
});

it("totalParts is the sum of all status counts", async () => {
const rows: StatusRow[] = [
{ status: Status.fulfilled, count: "10" },
{ status: Status.cancelled, count: "5" },
{ status: Status.unfilled, count: "3" },
];
vi.mocked(db.execute).mockResolvedValue({ rows } as never);

const body = await (await buildApp().request(makeUrl())).json() as Record<string, unknown>;
expect(body["totalParts"]).toBe(18);
});

it("echoes back the generatorId and chainId", async () => {
vi.mocked(db.execute).mockResolvedValue({ rows: [] } as never);

const body = await (await buildApp().request(makeUrl(EVENT_ID, 100))).json() as Record<string, unknown>;
expect(body["generatorId"]).toBe(EVENT_ID);
expect(body["chainId"]).toBe(100);
});

it("returns 400 when chainId query param is missing", async () => {
const app = buildApp();
const res = await app.request(
`http://localhost/generator/${EVENT_ID}/execution-summary`,
);
expect(res.status).toBe(400);
});

it("returns 500 when the DB throws", async () => {
vi.mocked(db.execute).mockRejectedValueOnce(new Error("db error"));

const res = await buildApp().request(makeUrl());
expect(res.status).toBe(500);
});
});
174 changes: 174 additions & 0 deletions tests/api/orders-by-owner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { describe, it, expect, beforeEach, vi } from "vitest";

// Mock virtual modules before any ponder-importing source files are loaded.
// ponder:api is resolved to tests/__mocks__/ponder-api.ts via vitest alias — no inline override needed.
vi.mock("ponder:schema", () => {
const ownerMapping = { owner: "owner", chainId: "chainId", address: "address" };
const conditionalOrderGenerator = {
eventId: "eventId", chainId: "chainId", orderType: "orderType",
owner: "owner", resolvedOwner: "resolvedOwner", status: "status",
ownerAddressType: "ownerAddressType",
};
const discreteOrder = {
conditionalOrderGeneratorId: "conditionalOrderGeneratorId",
orderUid: "orderUid", chainId: "chainId", status: "status",
sellAmount: "sellAmount", buyAmount: "buyAmount", feeAmount: "feeAmount",
validTo: "validTo", creationDate: "creationDate",
executedSellAmount: "executedSellAmount", executedBuyAmount: "executedBuyAmount",
};
return {
default: { ownerMapping, conditionalOrderGenerator, discreteOrder },
ownerMapping,
conditionalOrderGenerator,
discreteOrder,
};
});
vi.mock("ponder", () => ({
and: (..._args: unknown[]) => ({}),
eq: (..._args: unknown[]) => ({}),
inArray: (..._args: unknown[]) => ({}),
or: (..._args: unknown[]) => ({}),
}));

import { db } from "ponder:api";
import { makeSelectChain } from "../__mocks__/ponder-api";
import { ordersByOwnerHandler } from "../../src/api/endpoints/orders-by-owner";

const OWNER = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const EVENT_ID = "abc123";
const CHAIN_ID = 1;

/** Minimal Hono context stub that satisfies orders-by-owner handler requirements. */
function makeContext({
owner = OWNER,
chainId = CHAIN_ID,
status,
ownerAddressType,
}: {
owner?: string;
chainId?: number;
status?: string;
ownerAddressType?: string;
} = {}) {
const responses: Array<{ body: unknown; status: number }> = [];
return {
req: {
valid: (type: "param" | "query") => {
if (type === "param") return { owner };
return { chainId, status, ownerAddressType };
},
},
json: (body: unknown, httpStatus = 200) => {
// Simulate JSON serialization to catch BigInt issues early.
const serialised = JSON.parse(JSON.stringify(body, (_k, v) =>
typeof v === "bigint" ? v.toString() : v
));
responses.push({ body: serialised, status: httpStatus });
return { body: serialised, status: httpStatus };
},
_responses: responses,
};
}

const GENERATOR = {
eventId: EVENT_ID,
chainId: CHAIN_ID,
orderType: "TWAP",
owner: OWNER,
resolvedOwner: OWNER,
status: "Active",
ownerAddressType: null,
hash: "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1",
};

const ORDER = {
orderUid: "0x" + "bb".repeat(56),
chainId: CHAIN_ID,
status: "fulfilled",
sellAmount: "1000000000000000000",
buyAmount: "2000000000000000000",
feeAmount: "1000000000000000",
validTo: 9_999_999_999,
creationDate: BigInt("1700000000"),
executedSellAmount: "1000000000000000000",
executedBuyAmount: "2000000000000000000",
generatorId: EVENT_ID,
};

beforeEach(() => {
vi.mocked(db.select).mockReset();
});

describe("ordersByOwnerHandler", () => {
it("returns empty orders array when no generators are found", async () => {
vi.mocked(db.select)
.mockReturnValueOnce(makeSelectChain([]) as never) // ownerMapping → no proxies
.mockReturnValueOnce(makeSelectChain([]) as never); // generators → none

const ctx = makeContext();
await ordersByOwnerHandler(ctx as never, vi.fn() as never);
const result = ctx._responses[0]!.body as { orders: unknown[] };
expect(result.orders).toEqual([]);
});

it("returns empty orders when generators exist but have no discrete orders", async () => {
vi.mocked(db.select)
.mockReturnValueOnce(makeSelectChain([]) as never)
.mockReturnValueOnce(makeSelectChain([GENERATOR]) as never)
.mockReturnValueOnce(makeSelectChain([]) as never);

const ctx = makeContext();
await ordersByOwnerHandler(ctx as never, vi.fn() as never);
const result = ctx._responses[0]!.body as { orders: unknown[] };
expect(result.orders).toEqual([]);
});

it("returns enriched orders with embedded generator data", async () => {
vi.mocked(db.select)
.mockReturnValueOnce(makeSelectChain([]) as never)
.mockReturnValueOnce(makeSelectChain([GENERATOR]) as never)
.mockReturnValueOnce(makeSelectChain([ORDER]) as never);

const ctx = makeContext();
await ordersByOwnerHandler(ctx as never, vi.fn() as never);
const result = ctx._responses[0]!.body as { orders: Array<Record<string, unknown>> };

expect(result.orders).toHaveLength(1);
const order = result.orders[0]!;
expect(order["orderUid"]).toBe(ORDER.orderUid);
expect(order["status"]).toBe("fulfilled");
const gen = order["generator"] as Record<string, unknown>;
expect(gen["eventId"]).toBe(EVENT_ID);
expect(gen["orderType"]).toBe("TWAP");
});

it("serialises creationDate as a decimal string (BigInt scalar)", async () => {
vi.mocked(db.select)
.mockReturnValueOnce(makeSelectChain([]) as never)
.mockReturnValueOnce(makeSelectChain([GENERATOR]) as never)
.mockReturnValueOnce(makeSelectChain([ORDER]) as never);

const ctx = makeContext();
await ordersByOwnerHandler(ctx as never, vi.fn() as never);
const result = ctx._responses[0]!.body as { orders: Array<Record<string, unknown>> };
expect(result.orders[0]!["creationDate"]).toBe("1700000000");
});

it("includes proxy addresses from ownerMapping in the generator lookup", async () => {
const PROXY = "0xcccccccccccccccccccccccccccccccccccccccc";
vi.mocked(db.select)
.mockReturnValueOnce(makeSelectChain([{ address: PROXY }]) as never)
.mockReturnValueOnce(makeSelectChain([GENERATOR]) as never)
.mockReturnValueOnce(makeSelectChain([ORDER]) as never);

const ctx = makeContext();
await ordersByOwnerHandler(ctx as never, vi.fn() as never);
const result = ctx._responses[0]!.body as { orders: unknown[] };
expect(result.orders).toHaveLength(1);
});

// Regression guard for COW-993 (F15): hash must be present in generator object.
// Enable this test after the F15 fix lands (add `hash` to GeneratorSummary schema
// and select it in ordersByOwnerHandler).
it.todo("includes hash in generator object (COW-993)");
});
Loading
Loading