Skip to content

feat: add new getTwapOrders method - #928

Open
kernelwhisperer wants to merge 8 commits into
mainfrom
daniel/fe-272-list-twap-orders-for-eoa
Open

feat: add new getTwapOrders method#928
kernelwhisperer wants to merge 8 commits into
mainfrom
daniel/fe-272-list-twap-orders-for-eoa

Conversation

@kernelwhisperer

@kernelwhisperer kernelwhisperer commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Part of FE-272

Add a read-only ProgrammaticOrderApi to @cowprotocol/sdk-composable for listing TWAP orders by EOA or Safe. Parents and part orders are fetched separately.

Changes

  • Add getTwapOrders({ resolvedOwner, chainId }, options?).
  • Add getTwapPartOrders({ eventId, chainId }, options?).
  • Share validated QueryOptions: offset, limit, and direction.
  • Return QueryPage<T> with items and totalCount.
  • Return parent schedules, execution totals, part-order counts, owner, resolvedOwner, eventId, and hash.
  • Validate inputs and GraphQL responses with Valibot. (cowswap-frontend should be changed to rid of zod 3 which is waay heavier.)
  • Normalize addresses and parse amounts as bigint.
  • Expose failures as ProgrammaticOrderApiError.
  • It misses incremental fetching: using a blockNumber cursor to get the history starting at a certain "since"/"from". (that's a follow-up)
const api = new ProgrammaticOrderApi()

const { items: orders } = await api.getTwapOrders(
  { resolvedOwner, chainId },
  { limit: 100, direction: 'desc' },
)

const { items: parts } = await api.getTwapPartOrders(
  { eventId: orders[0].eventId, chainId },
  { limit: 10, direction: 'asc' },
)

Summary by CodeRabbit

  • New Features
    • Added a programmatic orders API to retrieve paginated TWAP parent orders and their TWAP part orders.
    • Introduced configurable pagination/sorting and more detailed TWAP order, schedule, status, and execution result typing.
  • Documentation
    • Added “Read TWAP history” documentation with examples and clarified pagination/response structure.
    • Marked DCA and Stop-Loss order types as work in progress.
  • Bug Fixes / Reliability
    • Improved request validation and clearer, method-specific API error handling.
  • Chores / Tests
    • Added unit tests for the GraphQL client and the TWAP API.

@kernelwhisperer kernelwhisperer self-assigned this Jul 17, 2026
@linear-code

linear-code Bot commented Jul 17, 2026

Copy link
Copy Markdown

FE-272

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a public programmatic-orders module for paginated TWAP parent and part-order retrieval, with Valibot validation, GraphQL response decoding, normalized API errors, exported TWAP types, tests, and README documentation.

Changes

Programmatic TWAP history

Layer / File(s) Summary
Public contracts and validation
packages/composable/package.json, packages/composable/src/programmaticOrders/types.ts, packages/composable/src/programmaticOrders/twap-types.ts, packages/composable/src/programmaticOrders/schemas.ts, packages/composable/src/programmaticOrders/validation.ts, packages/composable/src/programmaticOrders/index.ts, packages/composable/src/index.ts
Adds exported query, pagination, error, and TWAP domain types; Valibot schemas for inputs, identifiers, numeric values, timestamps, and statuses; parsing helpers; and package exports.
GraphQL transport and page decoding
packages/composable/src/programmaticOrders/graphql.ts, packages/composable/tests/graphql.spec.ts
Adds HTTP(S) endpoint normalization, GraphQL envelope validation, request/error handling, page decoding, and transport tests.
TWAP queries and payload schemas
packages/composable/src/programmaticOrders/twap-queries.ts, packages/composable/src/programmaticOrders/twap-schemas.ts
Adds GraphQL queries for TWAP parents and discrete parts, then validates and transforms their payloads into TWAP models.
TWAP API methods and verification
packages/composable/src/programmaticOrders/ProgrammaticOrderApi.ts, packages/composable/README.md, packages/composable/tests/ProgrammaticOrderApi.spec.ts
Adds paginated getTwapOrders and getTwapPartOrders methods with defaults and error wrapping, documents their usage, marks unsupported order types as WIP, and tests validation, requests, responses, and live retrievals.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ProgrammaticOrderApi
  participant GraphqlClient
  participant GraphQLAPI
  Caller->>ProgrammaticOrderApi: getTwapOrders(params, options)
  ProgrammaticOrderApi->>ProgrammaticOrderApi: validate inputs and apply pagination defaults
  ProgrammaticOrderApi->>GraphqlClient: queryPage(TWAP_ORDERS_QUERY)
  GraphqlClient->>GraphQLAPI: POST query with chainId, resolvedOwner, limit, offset, direction
  GraphQLAPI-->>GraphqlClient: TWAP parent page
  GraphqlClient-->>ProgrammaticOrderApi: decoded items and totalCount
  ProgrammaticOrderApi-->>Caller: QueryPage<TwapOrder>
Loading

Suggested reviewers: danziger

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is relevant to the changeset, though it mentions only one of several new TWAP API additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch daniel/fe-272-list-twap-orders-for-eoa

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📦 GitHub Packages Published

Last updated: Jul 27, 2026, 03:16:03 PM UTC

The following packages have been published to GitHub Packages with pre-release version pr-928-f62dc6e6:

  • @cowprotocol/cow-sdk@9.2.4-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-app-data@5.3.2-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-bridging@4.3.1-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-common@0.12.0-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-composable@1.2.1-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-config@2.3.1-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-contracts-ts@3.3.1-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-cow-shed@0.4.3-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-ethers-v5-adapter@0.4.10-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-ethers-v6-adapter@0.4.10-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-flash-loans@3.3.1-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-order-book@4.0.1-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-order-signing@1.1.4-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-subgraph@1.2.0-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-trading@2.2.4-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-viem-adapter@0.3.25-pr-928-f62dc6e6.0
  • @cowprotocol/sdk-weiroll@0.2.1-pr-928-f62dc6e6.0

Installation

These packages require authentication to install from GitHub Packages. First, create a .npmrc file:

# Create .npmrc file in your project root
echo "@cowprotocol:registry=https://npm.pkg.github.com" > .npmrc
echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN" >> .npmrc

To get your GitHub token:

  1. Go to https://github.com/settings/tokens
  2. Click "Generate new token (classic)"
  3. Check only the "read:packages" scope
  4. Copy the token and replace YOUR_GITHUB_TOKEN in the .npmrc file

Then install any of the packages above, either by exact version (i.e. @cowprotocol/cow-sdk@9.2.4-pr-928-f62dc6e6.0) or more conveniently by using the tag (@cowprotocol/cow-sdk@pr-928):

# Yarn
yarn add npm:@cowprotocol/cow-sdk@pr-928

# pnpm
pnpm install npm:@cowprotocol/cow-sdk@pr-928

# NPM
npm install npm:@cowprotocol/cow-sdk@pr-928

Update to the latest version (only if you used the tag)

Every commit will publish a new package. To upgrade to the latest version, run:

# Yarn
yarn upgrade @cowprotocol/cow-sdk

# pnpm
pnpm update @cowprotocol/cow-sdk

# NPM
npm update @cowprotocol/cow-sdk

View Packages

You can view the published packages at: https://github.com/cowprotocol/cow-sdk/packages

@socket-security

socket-security Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedvalibot@​1.4.21001009987100

View full report

@kernelwhisperer
kernelwhisperer marked this pull request as ready for review July 22, 2026 08:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/composable/src/programmaticOrders/graphql.ts`:
- Around line 37-50: Update GraphqlClient.query to enforce a request timeout by
creating or accepting an AbortSignal and passing it to fetch, including the
signal through pagination and getTwapOrders call paths. Catch aborts caused by
timeout and map them to GraphqlClientError with clear timeout context, while
preserving existing handling for other request failures; add a test that
verifies a stalled fetch rejects within the configured timeout.

In `@packages/composable/tests/ProgrammaticOrderApi.spec.ts`:
- Around line 9-28: Make the ProgrammaticOrderApi tests hermetic by mocking
deterministic GraphQL responses for both EOA and Safe getTwapOrders cases
instead of using the live default API. Update snapshots to reflect the fixed
fixtures, and move any live API coverage into a separately opt-in smoke test so
the default suite is independent of network access and mutable order history.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1b9982a-ba61-41e8-9d23-3a58e81bdd7c

📥 Commits

Reviewing files that changed from the base of the PR and between fa73993 and c102e92.

⛔ Files ignored due to path filters (2)
  • packages/composable/tests/__snapshots__/ProgrammaticOrderApi.spec.ts.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • packages/composable/README.md
  • packages/composable/package.json
  • packages/composable/src/index.ts
  • packages/composable/src/programmaticOrders/ProgrammaticOrderApi.ts
  • packages/composable/src/programmaticOrders/graphql.ts
  • packages/composable/src/programmaticOrders/index.ts
  • packages/composable/src/programmaticOrders/schemas.ts
  • packages/composable/src/programmaticOrders/twap-queries.ts
  • packages/composable/src/programmaticOrders/twap-schemas.ts
  • packages/composable/src/programmaticOrders/twap-types.ts
  • packages/composable/src/programmaticOrders/types.ts
  • packages/composable/tests/GraphqlClient.spec.ts
  • packages/composable/tests/ProgrammaticOrderApi.spec.ts

Comment on lines +37 to +50
async query(query: string, variables: Record<string, unknown>): Promise<unknown> {
let response: Response

try {
response = await fetch(this.url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
})
} catch (cause) {
throw new GraphqlClientError('GraphQL request failed', { cause })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols first.
ast-grep outline packages/composable/src/programmaticOrders/graphql.ts --view expanded

# Inspect the file with line numbers.
cat -n packages/composable/src/programmaticOrders/graphql.ts | sed -n '1,220p'

# Find callers and any timeout/AbortSignal patterns in the repo.
rg -n "AbortSignal|AbortController|timeout\(|fetch\(" packages/composable packages -g '!**/dist/**' -g '!**/build/**'

# Check package/runtime targets.
git ls-files 'packages/**/package.json' 'package.json' 'tsconfig*.json' 'packages/**/tsconfig*.json' | xargs -r rg -n '"(engines|target|lib|types|type)":|"typescript"'

Repository: cowprotocol/cow-sdk

Length of output: 11871


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the root and composable package/runtime targets.
cat -n package.json | sed -n '1,120p'
printf '\n--- composable package ---\n'
cat -n packages/composable/package.json | sed -n '1,120p'
printf '\n--- composable tsconfig ---\n'
cat -n packages/composable/tsconfig.json | sed -n '1,120p'

# Look for any request context/abort signal plumbing in composable.
rg -n "signal:|AbortSignal|AbortController|timeout\(|contextOverride|timeoutMs|request.*timeout" packages/composable packages/order-book packages/contracts-ts -g '!**/dist/**' -g '!**/build/**'

Repository: cowprotocol/cow-sdk

Length of output: 11335


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find GraphqlClient usage and the public TWAP surface.
rg -n "new GraphqlClient|GraphqlClient\\b|getTwapOrders|paginate\\(" packages/composable -g '!**/dist/**' -g '!**/build/**'

# Inspect the TWAP-related entrypoints.
ast-grep outline packages/composable/src --view expanded
rg -n "TWAP|twap|programmaticOrders|graphql" packages/composable/src -g '!**/dist/**' -g '!**/build/**'

# Inspect the composable package exports for a public API surface that might carry context/options.
cat -n packages/composable/src/index.ts | sed -n '1,220p'

Repository: cowprotocol/cow-sdk

Length of output: 29697


Bound the GraphQL request in packages/composable/src/programmaticOrders/graphql.ts:41. A stalled upstream can leave query(), pagination, and getTwapOrders() pending indefinitely; thread an AbortSignal/timeout through GraphqlClient, map expiry to GraphqlClientError, and add a stalled-fetch test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/src/programmaticOrders/graphql.ts` around lines 37 - 50,
Update GraphqlClient.query to enforce a request timeout by creating or accepting
an AbortSignal and passing it to fetch, including the signal through pagination
and getTwapOrders call paths. Catch aborts caused by timeout and map them to
GraphqlClientError with clear timeout context, while preserving existing
handling for other request failures; add a test that verifies a stalled fetch
rejects within the configured timeout.

Comment on lines +9 to +28
it('lists all known EOA TWAPs from the live programmatic orders API', async () => {
const progress: TwapOrder[] = []
const orders = await new ProgrammaticOrderApi().getTwapOrders({
resolvedOwner: EOA,
chainId: SupportedChainId.GNOSIS_CHAIN,
onProgress: (order) => progress.push(order),
})

expect(progress).toEqual(orders)
expect(orders).toMatchSnapshot()
}, 30_000)

it('lists all known Safe TWAPs from the live programmatic orders API', async () => {
const orders = await new ProgrammaticOrderApi().getTwapOrders({
resolvedOwner: SAFE,
chainId: SupportedChainId.GNOSIS_CHAIN,
})

expect(orders).toMatchSnapshot()
}, 30_000)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make these tests hermetic.

They call the live default API and snapshot mutable order history, so new orders, executions, or endpoint/network failures can break CI without a code regression. Mock fixed GraphQL responses for the default suite; keep any live smoke test opt-in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/tests/ProgrammaticOrderApi.spec.ts` around lines 9 - 28,
Make the ProgrammaticOrderApi tests hermetic by mocking deterministic GraphQL
responses for both EOA and Safe getTwapOrders cases instead of using the live
default API. Update snapshots to reflect the fixed fixtures, and move any live
API coverage into a separately opt-in smoke test so the default suite is
independent of network access and mutable order history.

@kernelwhisperer
kernelwhisperer requested review from a team and Danziger July 22, 2026 08:17
"@cowprotocol/sdk-order-signing": "workspace:*",
"@openzeppelin/merkle-tree": "^1.0.8"
"@openzeppelin/merkle-tree": "^1.0.8",
"valibot": "1.4.2"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Regarding valibot vs zod (4), no preference, but I think it would make sense to use the same one in both projects so that we have a single dependency. Maybe even declare it as a peer dependency here.

readonly name = 'GraphqlClientError'
}

export class GraphqlClient {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How come you need a custom GraphQL client instad of using a lib like Apollo or similar?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mostly to keep the bundle small and the npm supply chain attack vector small.

This is the only place where we use graphql in the sdk, it felt a bit overkill to pull in Apollo, but I'm not against exploring it.

const parents = await getTwapParents(this.graphql, resolvedOwner, chainId)

for (const parent of parents) {
const partOrders = await getTwapPartOrders(this.graphql, chainId, parent.eventId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess this is here for the initial version, but it will be removed later once the indexer includes this data in parent orders (as discussed in the sync) (?)

@Danziger Danziger Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In any case, maybe better to use Promise.all? Even though on that case maybe onProgress doesn't make much sense. Can you elaborate on this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0989f13 (refactor: decouple parts from twap order)

for (const record of records) {
const { chainId: recordChainId, parentEventId: recordParentEventId, ...partOrder } = record

if (recordChainId !== chainId || recordParentEventId !== parentEventId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need this? Also, if you are going to throw, there's no need for push (if the extra chainId and parentEventId are not an issue):

const records = awat client.paginate(...)

if (records.find(...)) throw new Error(...)

return records

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 0989f13 (refactor: decouple parts from twap order)

Comment thread packages/composable/README.md Outdated

Each result preserves both its creation `eventId` and ComposableCoW `hash`. `schedule.numberOfParts` is the number of scheduled parts. `partOrders` contains the corresponding part orders, so its length may be smaller. `executedAmounts` sums those part orders. The schedule's `effectiveStartTime` is the creation block timestamp when the on-chain `t0` was zero. Pass `apiUrl` to the constructor to use another programmatic orders API deployment.

Pass `onProgress` to receive each fully assembled TWAP order after all its available part orders are fetched. The promise returns the complete result. If a later page fails, the promise rejects and previously emitted orders remain provisional.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Based on this, should we...:

  • Implement auto-retry?
  • Implement a getTwapOrder (singular), as it looks like one use case for onProgress might be to get TWAP orders when looking for a specific one (so we don't care if fetching other ones fail).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is applicable anymore in 0989f13 (refactor: decouple parts from twap order). We're doing polling so auto-retry adds little value.

The use case of onProgress was not to get 1 twap order, but to start hydrating the ui with partial results rather thank a bulk update at the end.

@kernelwhisperer

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kernelwhisperer
kernelwhisperer requested a review from Danziger July 27, 2026 07:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/composable/src/programmaticOrders/graphql.ts (1)

42-56: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout/cancellation on the GraphQL fetch call.

A stalled or slow upstream response leaves query(), queryPage(), and both ProgrammaticOrderApi methods pending indefinitely. Thread an AbortSignal/timeout through GraphqlClient and map expiry to GraphqlClientError.

🔧 Suggested approach
-  async query(query: string, variables: Record<string, unknown>): Promise<unknown> {
+  async query(query: string, variables: Record<string, unknown>, timeoutMs = 30_000): Promise<unknown> {
     let response: Response
+    const controller = new AbortController()
+    const timeout = setTimeout(() => controller.abort(), timeoutMs)

     try {
       response = await fetch(this.url, {
         method: 'POST',
         headers: {
           Accept: 'application/json',
           'Content-Type': 'application/json',
         },
         body: JSON.stringify({ query, variables }),
+        signal: controller.signal,
       })
     } catch (cause) {
-      throw new GraphqlClientError('GraphQL request failed', { cause })
+      throw new GraphqlClientError(
+        controller.signal.aborted ? 'GraphQL request timed out' : 'GraphQL request failed',
+        { cause },
+      )
+    } finally {
+      clearTimeout(timeout)
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/src/programmaticOrders/graphql.ts` around lines 42 - 56,
Update GraphqlClient.query to support request cancellation via an AbortSignal
and a bounded timeout, passing the signal into fetch so query(), queryPage(),
and both ProgrammaticOrderApi methods cannot remain pending indefinitely.
Distinguish timeout/abort failures and map them to GraphqlClientError while
preserving the existing handling for other fetch failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/composable/tests/ProgrammaticOrderApi.spec.ts`:
- Around line 172-212: Make the live-network tests in the ProgrammaticOrderApi
test suite hermetic by replacing direct default-API calls with a deterministic
mock or fixture-backed response. Update the EOA and Safe TWAP parent tests and
the part-order test so snapshots and the executed-fee parent selection use
stable controlled data, while preserving assertions for pagination and
part-order behavior.
- Around line 88-131: Update the query assertion in the “requests parent-only
TWAPs by creation” test to detect nested part-order item selection despite the
existing discreteOrders(limit: 1) syntax. Assert against the actual rendered
nested-field pattern, while preserving the response-level assertion that
page.items[0] has no partOrders.

---

Duplicate comments:
In `@packages/composable/src/programmaticOrders/graphql.ts`:
- Around line 42-56: Update GraphqlClient.query to support request cancellation
via an AbortSignal and a bounded timeout, passing the signal into fetch so
query(), queryPage(), and both ProgrammaticOrderApi methods cannot remain
pending indefinitely. Distinguish timeout/abort failures and map them to
GraphqlClientError while preserving the existing handling for other fetch
failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d2fb917e-7c88-4c37-b44b-4aa80adf0543

📥 Commits

Reviewing files that changed from the base of the PR and between c102e92 and 0989f13.

⛔ Files ignored due to path filters (1)
  • packages/composable/tests/__snapshots__/ProgrammaticOrderApi.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • packages/composable/README.md
  • packages/composable/src/programmaticOrders/ProgrammaticOrderApi.ts
  • packages/composable/src/programmaticOrders/graphql.ts
  • packages/composable/src/programmaticOrders/index.ts
  • packages/composable/src/programmaticOrders/schemas.ts
  • packages/composable/src/programmaticOrders/twap-queries.ts
  • packages/composable/src/programmaticOrders/twap-schemas.ts
  • packages/composable/src/programmaticOrders/twap-types.ts
  • packages/composable/src/programmaticOrders/types.ts
  • packages/composable/src/programmaticOrders/validation.ts
  • packages/composable/tests/ProgrammaticOrderApi.spec.ts
  • packages/composable/tests/graphql.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/composable/src/programmaticOrders/index.ts
  • packages/composable/src/programmaticOrders/twap-schemas.ts
  • packages/composable/src/programmaticOrders/schemas.ts
  • packages/composable/README.md

Comment on lines +88 to +131
it('requests parent-only TWAPs by creation and preserves the returned order', async () => {
const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(
JSON.stringify({
data: {
twapOrders: {
items: [twapParent('newer-event', 200), twapParent('older-event', 100)],
totalCount: 2,
},
},
}),
{ headers: { 'Content-Type': 'application/json' } },
),
)

const page = await new ProgrammaticOrderApi({ apiUrl: 'https://example.com' }).getTwapOrders(
{
resolvedOwner: EOA,
chainId: SupportedChainId.GNOSIS_CHAIN,
},
{ direction: 'asc', limit: 2, offset: 1 },
)
const request = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)) as {
query: string
variables: Record<string, unknown>
}

expect(request.query).toContain('orderBy: "eventId"')
expect(request.query).toContain('orderDirection: $direction')
expect(request.query).not.toContain('discreteOrders {\n')
expect(request.variables).toEqual({
resolvedOwner: EOA.toLowerCase(),
chainId: SupportedChainId.GNOSIS_CHAIN,
offset: 1,
limit: 2,
direction: 'asc',
})
expect(page.totalCount).toBe(2)
expect(page.items.map(({ eventId, createdAt }) => ({ eventId, createdAt }))).toEqual([
{ eventId: 'newer-event', createdAt: 200 },
{ eventId: 'older-event', createdAt: 100 },
])
expect(page.items[0]).not.toHaveProperty('partOrders')
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tautological assertion doesn't verify the "no nested part-order items" claim.

request.query never contains the literal 'discreteOrders {\n' because the query always renders discreteOrders(limit: 1) {. This assertion passes even if the parent query started selecting nested part-order item fields.

🔧 Suggested fix
-    expect(request.query).not.toContain('discreteOrders {\n')
+    expect(request.query).not.toContain('discreteOrders(limit: 1) {\n        orderUid')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('requests parent-only TWAPs by creation and preserves the returned order', async () => {
const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(
JSON.stringify({
data: {
twapOrders: {
items: [twapParent('newer-event', 200), twapParent('older-event', 100)],
totalCount: 2,
},
},
}),
{ headers: { 'Content-Type': 'application/json' } },
),
)
const page = await new ProgrammaticOrderApi({ apiUrl: 'https://example.com' }).getTwapOrders(
{
resolvedOwner: EOA,
chainId: SupportedChainId.GNOSIS_CHAIN,
},
{ direction: 'asc', limit: 2, offset: 1 },
)
const request = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)) as {
query: string
variables: Record<string, unknown>
}
expect(request.query).toContain('orderBy: "eventId"')
expect(request.query).toContain('orderDirection: $direction')
expect(request.query).not.toContain('discreteOrders {\n')
expect(request.variables).toEqual({
resolvedOwner: EOA.toLowerCase(),
chainId: SupportedChainId.GNOSIS_CHAIN,
offset: 1,
limit: 2,
direction: 'asc',
})
expect(page.totalCount).toBe(2)
expect(page.items.map(({ eventId, createdAt }) => ({ eventId, createdAt }))).toEqual([
{ eventId: 'newer-event', createdAt: 200 },
{ eventId: 'older-event', createdAt: 100 },
])
expect(page.items[0]).not.toHaveProperty('partOrders')
})
it('requests parent-only TWAPs by creation and preserves the returned order', async () => {
const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(
JSON.stringify({
data: {
twapOrders: {
items: [twapParent('newer-event', 200), twapParent('older-event', 100)],
totalCount: 2,
},
},
}),
{ headers: { 'Content-Type': 'application/json' } },
),
)
const page = await new ProgrammaticOrderApi({ apiUrl: 'https://example.com' }).getTwapOrders(
{
resolvedOwner: EOA,
chainId: SupportedChainId.GNOSIS_CHAIN,
},
{ direction: 'asc', limit: 2, offset: 1 },
)
const request = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)) as {
query: string
variables: Record<string, unknown>
}
expect(request.query).toContain('orderBy: "eventId"')
expect(request.query).toContain('orderDirection: $direction')
expect(request.query).not.toContain('discreteOrders(limit: 1) {\n orderUid')
expect(request.variables).toEqual({
resolvedOwner: EOA.toLowerCase(),
chainId: SupportedChainId.GNOSIS_CHAIN,
offset: 1,
limit: 2,
direction: 'asc',
})
expect(page.totalCount).toBe(2)
expect(page.items.map(({ eventId, createdAt }) => ({ eventId, createdAt }))).toEqual([
{ eventId: 'newer-event', createdAt: 200 },
{ eventId: 'older-event', createdAt: 100 },
])
expect(page.items[0]).not.toHaveProperty('partOrders')
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/tests/ProgrammaticOrderApi.spec.ts` around lines 88 -
131, Update the query assertion in the “requests parent-only TWAPs by creation”
test to detect nested part-order item selection despite the existing
discreteOrders(limit: 1) syntax. Assert against the actual rendered nested-field
pattern, while preserving the response-level assertion that page.items[0] has no
partOrders.

Comment on lines +172 to +212
it('lists the latest EOA TWAP parents from the live programmatic orders API', async () => {
const page = await new ProgrammaticOrderApi().getTwapOrders({
resolvedOwner: EOA,
chainId: SupportedChainId.GNOSIS_CHAIN,
})

expect(page.items).toMatchSnapshot()
}, 30_000)

it('lists the latest Safe TWAP parents from the live programmatic orders API', async () => {
const page = await new ProgrammaticOrderApi().getTwapOrders({
resolvedOwner: SAFE,
chainId: SupportedChainId.GNOSIS_CHAIN,
})

expect(page.items).toMatchSnapshot()
}, 30_000)

it('lists one page of EOA TWAP part orders from the live programmatic orders API', async () => {
const api = new ProgrammaticOrderApi()
const parentsPage = await api.getTwapOrders({
resolvedOwner: EOA,
chainId: SupportedChainId.GNOSIS_CHAIN,
})
const parent = parentsPage.items.find(({ executedAmounts }) => executedAmounts.executedFeeAmount > 0n)

expect(parent).toBeDefined()

const page = await api.getTwapPartOrders(
{
eventId: String(parent?.eventId),
chainId: SupportedChainId.GNOSIS_CHAIN,
},
{ offset: 0, limit: 10 },
)

expect(page.totalCount).toBe(parent?.partOrdersCount)
expect(page.items.some(({ executedFeeAmount }) => executedFeeAmount !== null)).toBe(true)
expect(page).toMatchSnapshot()
}, 30_000)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Live-network tests remain non-hermetic.

These tests call the live default programmatic orders API and depend on mutable on-chain order history (snapshot comparisons, and finding an order with executedFeeAmount > 0n). Network issues, indexer downtime, or the underlying data changing can break CI without any code regression — the same concern raised previously and still present after the refactor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/tests/ProgrammaticOrderApi.spec.ts` around lines 172 -
212, Make the live-network tests in the ProgrammaticOrderApi test suite hermetic
by replacing direct default-API calls with a deterministic mock or
fixture-backed response. Update the EOA and Safe TWAP parent tests and the
part-order test so snapshots and the executed-fee parent selection use stable
controlled data, while preserving assertions for pagination and part-order
behavior.

@kernelwhisperer
kernelwhisperer requested a review from a team July 27, 2026 07:53
@kernelwhisperer
kernelwhisperer force-pushed the daniel/fe-272-list-twap-orders-for-eoa branch from ff7f4c7 to f62dc6e Compare July 27, 2026 15:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/composable/src/programmaticOrders/graphql.ts (1)

42-56: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fetch has no timeout/AbortSignal.

A stalled upstream leaves query(), queryPage(), and callers like getTwapOrders() pending indefinitely. This was flagged in a prior review round on this same method and remains unaddressed.

🔧 Proposed fix: bound the request with an AbortSignal timeout
   async query(query: string, variables: Record<string, unknown>): Promise<unknown> {
     let response: Response
+    const controller = new AbortController()
+    const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS)

     try {
       response = await fetch(this.url, {
         method: 'POST',
         headers: {
           Accept: 'application/json',
           'Content-Type': 'application/json',
         },
         body: JSON.stringify({ query, variables }),
+        signal: controller.signal,
       })
     } catch (cause) {
+      if (controller.signal.aborted) throw new GraphqlClientError('GraphQL request timed out', { cause })
       throw new GraphqlClientError('GraphQL request failed', { cause })
+    } finally {
+      clearTimeout(timeout)
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/src/programmaticOrders/graphql.ts` around lines 42 - 56,
Update the GraphQLClient query method’s fetch request to use an AbortSignal
timeout, ensuring stalled upstream requests are aborted within the configured
timeout and continue to be wrapped as GraphqlClientError. Preserve the existing
request payload and error-handling behavior.
🧹 Nitpick comments (1)
packages/composable/src/programmaticOrders/graphql.ts (1)

76-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Invalid-envelope/page errors discard validation details.

result.issues is dropped when throwing 'Invalid GraphQL response' / `Invalid GraphQL page: ${operation.page}`, making it hard to diagnose schema mismatches from production errors.

♻️ Proposed fix: include a compact issue summary
     if (!result.success) {
-      throw new GraphqlClientError('Invalid GraphQL response')
+      throw new GraphqlClientError('Invalid GraphQL response', { cause: result.issues })
     }
     if (!result.success) {
-      throw new GraphqlClientError(`Invalid GraphQL page: ${operation.page}`)
+      throw new GraphqlClientError(`Invalid GraphQL page: ${operation.page}`, { cause: result.issues })
     }

Also applies to: 108-110

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/composable/src/programmaticOrders/graphql.ts` around lines 76 - 80,
Update the error handling after safeParse in the GraphQL response and page
validation paths to include a compact summary derived from result.issues in the
thrown GraphqlClientError messages. Preserve the existing invalid-envelope and
invalid-page context, including the operation.page value, while retaining the
current success flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/composable/src/programmaticOrders/graphql.ts`:
- Around line 42-56: Update the GraphQLClient query method’s fetch request to
use an AbortSignal timeout, ensuring stalled upstream requests are aborted
within the configured timeout and continue to be wrapped as GraphqlClientError.
Preserve the existing request payload and error-handling behavior.

---

Nitpick comments:
In `@packages/composable/src/programmaticOrders/graphql.ts`:
- Around line 76-80: Update the error handling after safeParse in the GraphQL
response and page validation paths to include a compact summary derived from
result.issues in the thrown GraphqlClientError messages. Preserve the existing
invalid-envelope and invalid-page context, including the operation.page value,
while retaining the current success flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d6dec8e-0195-46f3-b466-d263b177ef1e

📥 Commits

Reviewing files that changed from the base of the PR and between 01f8519 and f62dc6e.

⛔ Files ignored due to path filters (2)
  • packages/composable/tests/__snapshots__/ProgrammaticOrderApi.spec.ts.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • packages/composable/README.md
  • packages/composable/package.json
  • packages/composable/src/index.ts
  • packages/composable/src/programmaticOrders/ProgrammaticOrderApi.ts
  • packages/composable/src/programmaticOrders/graphql.ts
  • packages/composable/src/programmaticOrders/index.ts
  • packages/composable/src/programmaticOrders/schemas.ts
  • packages/composable/src/programmaticOrders/twap-queries.ts
  • packages/composable/src/programmaticOrders/twap-schemas.ts
  • packages/composable/src/programmaticOrders/twap-types.ts
  • packages/composable/src/programmaticOrders/types.ts
  • packages/composable/src/programmaticOrders/validation.ts
  • packages/composable/tests/ProgrammaticOrderApi.spec.ts
  • packages/composable/tests/graphql.spec.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/composable/src/index.ts
  • packages/composable/src/programmaticOrders/validation.ts
  • packages/composable/src/programmaticOrders/twap-queries.ts
  • packages/composable/src/programmaticOrders/index.ts
  • packages/composable/src/programmaticOrders/types.ts
  • packages/composable/tests/graphql.spec.ts
  • packages/composable/src/programmaticOrders/ProgrammaticOrderApi.ts
  • packages/composable/src/programmaticOrders/twap-types.ts
  • packages/composable/src/programmaticOrders/twap-schemas.ts
  • packages/composable/tests/ProgrammaticOrderApi.spec.ts
  • packages/composable/README.md
  • packages/composable/src/programmaticOrders/schemas.ts

@shoom3301 shoom3301 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Semantically all good. I haven't checked the queries too deep.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants