From 294fc9da4476f1304ef7b19360c96fb424fe7c63 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 15:40:33 -0300 Subject: [PATCH 1/6] feat: expose generator hash in REST /api/orders/by-owner response (COW-993) hash = keccak256(abi.encode(handler, salt, staticInput)) is the on-chain canonical identifier used by ComposableCow.singleOrders() and remove(). It was already indexed in the schema but missing from the REST response, forcing integrators to use GraphQL to look up an order by hash. Co-Authored-By: Claude Sonnet 4.6 --- src/api/endpoints/orders-by-owner.ts | 1 + src/api/schemas/orders-by-owner.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/api/endpoints/orders-by-owner.ts b/src/api/endpoints/orders-by-owner.ts index 42c754e..f8b7d04 100644 --- a/src/api/endpoints/orders-by-owner.ts +++ b/src/api/endpoints/orders-by-owner.ts @@ -54,6 +54,7 @@ export const ordersByOwnerHandler: RouteHandler< owner: schema.conditionalOrderGenerator.owner, resolvedOwner: schema.conditionalOrderGenerator.resolvedOwner, status: schema.conditionalOrderGenerator.status, + hash: schema.conditionalOrderGenerator.hash, ownerAddressType: schema.conditionalOrderGenerator.ownerAddressType, }) .from(schema.conditionalOrderGenerator) diff --git a/src/api/schemas/orders-by-owner.ts b/src/api/schemas/orders-by-owner.ts index 884c36c..1f132ce 100644 --- a/src/api/schemas/orders-by-owner.ts +++ b/src/api/schemas/orders-by-owner.ts @@ -19,6 +19,11 @@ export const GeneratorSummary = z.object({ owner: z.string(), resolvedOwner: z.string().nullable(), status: z.string(), + hash: z + .string() + .describe( + "On-chain canonical identifier: keccak256(abi.encode(handler, salt, staticInput)). Used by ComposableCow.singleOrders(owner, hash) and remove(owner, hash).", + ), ownerAddressType: z .enum(["cowshed_proxy", "flash_loan_helper"]) .nullable() From 8804df3f21ca6da07ee34a569efd128ddb583c50 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 18:42:32 -0300 Subject: [PATCH 2/6] test: add GeneratorSummary schema tests including hash field (COW-993) Co-Authored-By: Claude Sonnet 4.6 --- tests/api/orders-by-owner.test.ts | 119 ++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/api/orders-by-owner.test.ts diff --git a/tests/api/orders-by-owner.test.ts b/tests/api/orders-by-owner.test.ts new file mode 100644 index 0000000..ce76433 --- /dev/null +++ b/tests/api/orders-by-owner.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from "vitest"; +import { + GeneratorSummary, + OrdersByOwnerResponse, +} from "../../src/api/schemas/orders-by-owner"; + +// A minimal valid GeneratorSummary payload that satisfies all required fields. +const validGenerator = { + eventId: "0xabc123", + chainId: 1, + orderType: "TWAP", + owner: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + resolvedOwner: null, + status: "open", + hash: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + ownerAddressType: null, +} as const; + +describe("GeneratorSummary schema", () => { + it("parses correctly when hash is present as a valid hex string", () => { + const result = GeneratorSummary.safeParse(validGenerator); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.hash).toBe(validGenerator.hash); + } + }); + + it("fails parse when hash is missing", () => { + const { hash: _omitted, ...withoutHash } = validGenerator; + const result = GeneratorSummary.safeParse(withoutHash); + expect(result.success).toBe(false); + if (!result.success) { + const paths = result.error.issues.map((i) => i.path.join(".")); + expect(paths).toContain("hash"); + } + }); + + it("fails parse when hash is not a string (number supplied)", () => { + const result = GeneratorSummary.safeParse({ ...validGenerator, hash: 12345 }); + expect(result.success).toBe(false); + if (!result.success) { + const paths = result.error.issues.map((i) => i.path.join(".")); + expect(paths).toContain("hash"); + } + }); + + it("hash field carries the correct describe() text", () => { + const shape = GeneratorSummary.shape; + const description = shape.hash.description; + expect(description).toBe( + "On-chain canonical identifier: keccak256(abi.encode(handler, salt, staticInput)). Used by ComposableCow.singleOrders(owner, hash) and remove(owner, hash).", + ); + }); + + it("ownerAddressType accepts null (regression guard for unchanged field)", () => { + const result = GeneratorSummary.safeParse({ ...validGenerator, ownerAddressType: null }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.ownerAddressType).toBeNull(); + } + }); + + it("ownerAddressType accepts the enum value 'cowshed_proxy'", () => { + const result = GeneratorSummary.safeParse({ + ...validGenerator, + ownerAddressType: "cowshed_proxy", + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.ownerAddressType).toBe("cowshed_proxy"); + } + }); + + it("ownerAddressType accepts the enum value 'flash_loan_helper'", () => { + const result = GeneratorSummary.safeParse({ + ...validGenerator, + ownerAddressType: "flash_loan_helper", + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.ownerAddressType).toBe("flash_loan_helper"); + } + }); +}); + +describe("OrdersByOwnerResponse schema", () => { + it("wraps an array of GeneratorSummary correctly via the orders field", () => { + // Build a minimal OrderItem that nests the generator. + const orderItem = { + orderUid: "0xorder001", + chainId: 1, + status: "open", + sellAmount: "1000000000000000000", + buyAmount: "2000000000000000000", + feeAmount: "0", + validTo: null, + creationDate: "1700000000", + executedSellAmount: null, + executedBuyAmount: null, + generatorId: "0xabc123", + generator: validGenerator, + }; + + const result = OrdersByOwnerResponse.safeParse({ orders: [orderItem] }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.orders).toHaveLength(1); + expect(result.data.orders[0].generator?.hash).toBe(validGenerator.hash); + } + }); + + it("parses an empty orders array", () => { + const result = OrdersByOwnerResponse.safeParse({ orders: [] }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.orders).toHaveLength(0); + } + }); +}); From 4594e5f66a7daa9459b6485795b8411056fca433 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 2 Jun 2026 17:05:53 -0300 Subject: [PATCH 3/6] fix: correct hash field describe() to use tuple abi.encode notation (COW-993) Co-Authored-By: Claude Sonnet 4.6 --- src/api/schemas/orders-by-owner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/schemas/orders-by-owner.ts b/src/api/schemas/orders-by-owner.ts index 1f132ce..e9e5c85 100644 --- a/src/api/schemas/orders-by-owner.ts +++ b/src/api/schemas/orders-by-owner.ts @@ -22,7 +22,7 @@ export const GeneratorSummary = z.object({ hash: z .string() .describe( - "On-chain canonical identifier: keccak256(abi.encode(handler, salt, staticInput)). Used by ComposableCow.singleOrders(owner, hash) and remove(owner, hash).", + "On-chain canonical identifier: keccak256(abi.encode((handler, salt, staticInput))). Used by ComposableCow.singleOrders(owner, hash) and remove(owner, hash).", ), ownerAddressType: z .enum(["cowshed_proxy", "flash_loan_helper"]) From bb80e89eaf331ed91bad06a382b44c3fde708f4b Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Wed, 3 Jun 2026 10:21:22 -0300 Subject: [PATCH 4/6] fix: add non-null assertion on orders[0] in test to fix TS2532 Co-Authored-By: Claude Sonnet 4.6 --- tests/api/orders-by-owner.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/orders-by-owner.test.ts b/tests/api/orders-by-owner.test.ts index ce76433..c96bb3b 100644 --- a/tests/api/orders-by-owner.test.ts +++ b/tests/api/orders-by-owner.test.ts @@ -105,7 +105,7 @@ describe("OrdersByOwnerResponse schema", () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.orders).toHaveLength(1); - expect(result.data.orders[0].generator?.hash).toBe(validGenerator.hash); + expect(result.data.orders[0]!.generator?.hash).toBe(validGenerator.hash); } }); From a418d7b8443083902a7f7d7e1eb41008e9d6ecb1 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 10:47:12 -0300 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20improve=20hash=20field=20description?= =?UTF-8?q?=20accuracy=20=E2=80=94=20reference=20ComposableCow.hash(params?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/api/schemas/orders-by-owner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/schemas/orders-by-owner.ts b/src/api/schemas/orders-by-owner.ts index e9e5c85..d55fd08 100644 --- a/src/api/schemas/orders-by-owner.ts +++ b/src/api/schemas/orders-by-owner.ts @@ -22,7 +22,7 @@ export const GeneratorSummary = z.object({ hash: z .string() .describe( - "On-chain canonical identifier: keccak256(abi.encode((handler, salt, staticInput))). Used by ComposableCow.singleOrders(owner, hash) and remove(owner, hash).", + "On-chain canonical identifier: keccak256(abi.encode(ConditionalOrderParams { handler, salt, staticInput })) — the value returned by ComposableCow.hash(params) and used as the key in singleOrders(owner, hash) and remove(owner, hash).", ), ownerAddressType: z .enum(["cowshed_proxy", "flash_loan_helper"]) From 928101662d99751f38d8da3ec9fd21c4d1414d55 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 18:49:54 -0300 Subject: [PATCH 6/6] test: trim orders-by-owner schema tests to regression-only guards Remove tests that duplicate TypeScript's static coverage (type check, describe text, enum values). Keep only the two runtime regression guards that safeParse-unknown cannot catch at compile time. Co-Authored-By: Claude Sonnet 4.6 --- tests/api/orders-by-owner.test.ts | 47 ++++--------------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/tests/api/orders-by-owner.test.ts b/tests/api/orders-by-owner.test.ts index c96bb3b..c24e916 100644 --- a/tests/api/orders-by-owner.test.ts +++ b/tests/api/orders-by-owner.test.ts @@ -25,6 +25,9 @@ describe("GeneratorSummary schema", () => { } }); + // Regression guard for COW-993: hash was previously missing from the schema, + // causing it to be silently dropped from API responses. safeParse accepts + // unknown so TS gives no protection here at runtime. it("fails parse when hash is missing", () => { const { hash: _omitted, ...withoutHash } = validGenerator; const result = GeneratorSummary.safeParse(withoutHash); @@ -35,57 +38,19 @@ describe("GeneratorSummary schema", () => { } }); - it("fails parse when hash is not a string (number supplied)", () => { - const result = GeneratorSummary.safeParse({ ...validGenerator, hash: 12345 }); - expect(result.success).toBe(false); - if (!result.success) { - const paths = result.error.issues.map((i) => i.path.join(".")); - expect(paths).toContain("hash"); - } - }); - - it("hash field carries the correct describe() text", () => { - const shape = GeneratorSummary.shape; - const description = shape.hash.description; - expect(description).toBe( - "On-chain canonical identifier: keccak256(abi.encode(handler, salt, staticInput)). Used by ComposableCow.singleOrders(owner, hash) and remove(owner, hash).", - ); - }); - - it("ownerAddressType accepts null (regression guard for unchanged field)", () => { + // Regression guard: ownerAddressType is nullable — null is the common case + // for generators that don't go through a proxy. + it("ownerAddressType accepts null", () => { const result = GeneratorSummary.safeParse({ ...validGenerator, ownerAddressType: null }); expect(result.success).toBe(true); if (result.success) { expect(result.data.ownerAddressType).toBeNull(); } }); - - it("ownerAddressType accepts the enum value 'cowshed_proxy'", () => { - const result = GeneratorSummary.safeParse({ - ...validGenerator, - ownerAddressType: "cowshed_proxy", - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.ownerAddressType).toBe("cowshed_proxy"); - } - }); - - it("ownerAddressType accepts the enum value 'flash_loan_helper'", () => { - const result = GeneratorSummary.safeParse({ - ...validGenerator, - ownerAddressType: "flash_loan_helper", - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.ownerAddressType).toBe("flash_loan_helper"); - } - }); }); describe("OrdersByOwnerResponse schema", () => { it("wraps an array of GeneratorSummary correctly via the orders field", () => { - // Build a minimal OrderItem that nests the generator. const orderItem = { orderUid: "0xorder001", chainId: 1,