/** @module-tag localnet */ import type * as z from 'zod/mini' import { Address, Value as core_Value } from 'ox' import { Db, Schema } from 'tapimo' import { FxOracle } from 'tapimo/apps' import { decodeFunctionData, zeroHash } from 'viem' import { Abis, Actions, Addresses } from 'viem/tempo' import * as TestApp from '../../../../test/App.js' import * as Relay from '../../../../test/Relay.js' import * as Runtime from '../../../../test/runtime.js' import * as Exchanges from './exchanges.js' type JsonSchema = { $ref?: string anyOf?: readonly JsonSchema[] description?: string enum?: readonly string[] examples?: readonly unknown[] format?: string items?: JsonSchema properties?: Record required?: readonly string[] } type OpenApiResponse = { $ref?: string content?: Record } type OpenApiOperation = { operationId?: string requestBody?: { content?: Record } responses?: Record } type OrdersResponse = z.output type OpenApiDocument = { components: { schemas: Record } paths: Record< string, { get?: OpenApiOperation post?: OpenApiOperation } > } type PairsResponse = z.output // Deterministic EUR-based rate set so conversion math is exact in assertions. const fixed = FxOracle.from({ name: 'fixed', rates: async () => ({ asOf: '2026-01-01T00:00:00.000Z', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' }, }), }) function resolveSchema(spec: OpenApiDocument, schema: JsonSchema | undefined): JsonSchema { if (!schema) throw new Error('Missing OpenAPI schema') if (!schema.$ref) return schema const name = schema.$ref.split('/').at(-1) if (!name) throw new Error(`Invalid OpenAPI component ref ${schema.$ref}`) return component(spec, name) } function component(spec: OpenApiDocument, name: string): JsonSchema { const schema = spec.components.schemas[name] if (!schema) throw new Error(`Missing OpenAPI component ${name}`) return schema } describe('OpenAPI', () => { test('publishes generator-ready exchange contracts', async () => { const app = TestApp.create({ auth: false }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operations = { createQuote: spec.paths['/v1/exchange/quotes']?.post, finalizeQuote: spec.paths['/v1/exchange/quotes/execute']?.post, getDepth: spec.paths['/v1/exchange/pairs/{base}/depth']?.get, getOhlc: spec.paths['/v1/exchange/pairs/{base}/ohlc']?.get, getOrder: spec.paths['/v1/exchange/orders/{orderId}']?.get, getOrderFills: spec.paths['/v1/exchange/orders/{orderId}/fills']?.get, getOrders: spec.paths['/v1/exchange/orders']?.get, getPair: spec.paths['/v1/exchange/pairs/{base}']?.get, getPairs: spec.paths['/v1/exchange/pairs']?.get, getSwaps: spec.paths['/v1/exchange/swaps']?.get, } if (Object.values(operations).some((operation) => !operation)) throw new Error('Missing exchange OpenAPI operation') const summarizeError = (response: OpenApiResponse | undefined) => { if (!response) return undefined if (response.$ref) return { $ref: response.$ref } const schema = response.content?.['application/json']?.schema return resolveSchema(spec, schema).properties?.['error']?.properties?.['code']?.enum } const summarize = (operation: OpenApiOperation | undefined) => { if (!operation) throw new Error('Missing exchange OpenAPI operation') return { errors: Object.fromEntries( [400, 401, 402, 403, 404, 429, 500, 502, 504] .filter((status) => operation.responses?.[status]) .map((status) => [status, summarizeError(operation.responses?.[status])]), ), operationId: operation.operationId, request: operation.requestBody?.content?.['application/json']?.schema, response: operation.responses?.['200']?.content?.['application/json']?.schema, } } const components = { depth: component(spec, 'ExchangePairDepth'), ohlc: component(spec, 'ExchangePairOhlc'), ohlcBucket: component(spec, 'ExchangeOhlcBucket'), order: component(spec, 'ExchangeRestingOrder'), orderFill: component(spec, 'ExchangeOrderFill'), orderFillList: component(spec, 'ExchangeOrderFillList'), orderList: component(spec, 'ExchangeOrderList'), pair: component(spec, 'ExchangePair'), pairList: component(spec, 'ExchangePairList'), quote: component(spec, 'ExchangeQuote'), quoteApproval: component(spec, 'ExchangeQuoteExactSourceApprovalRequired'), quoteReady: component(spec, 'ExchangeQuoteExactSourceReady'), quoteSignature: component(spec, 'ExchangeQuoteExactSourceSignatureRequired'), swap: component(spec, 'ExchangeSwap'), swapList: component(spec, 'ExchangeSwapList'), } const quote = components.quote const quoteVariants = quote.anyOf ?? [] const exchangeComponentNames = Object.keys(spec.components.schemas) .filter((name) => name.includes('Exchange')) .sort() expect({ components: exchangeComponentNames, formats: { ohlc: components.ohlcBucket.properties?.['timestamp']?.format, order: components.order.properties?.['placedAt']?.format, orderFill: components.orderFill.properties?.['filledAt']?.format, pair: components.pair.properties?.['timestamp']?.format, swap: components.swap.properties?.['filledAt']?.format, }, models: { depthLevels: components.depth.properties?.['asks']?.items, ohlcBuckets: components.ohlc.properties?.['data']?.items, orderFills: components.orderFillList.properties?.['data']?.items, orderFillsMeta: components.orderFillList.properties?.['meta'], orders: components.orderList.properties?.['data']?.items, ordersMeta: components.orderList.properties?.['meta'], pairDetail: operations.getPair?.responses?.['200']?.content?.['application/json']?.schema, pairs: components.pairList.properties?.['data']?.items, pairsMeta: components.pairList.properties?.['meta'], quoteApproval: components.quoteApproval.properties?.['approval'], quoteAmount: components.quoteSignature.properties?.['sourceAmount'], quoteMeta: components.quoteSignature.properties?.['meta'], quoteToken: components.quoteSignature.properties?.['sourceToken'], quoteTransaction: components.quoteReady.properties?.['transaction'], quoteTypedData: components.quoteSignature.properties?.['typedData'], quoteVariants: { count: quoteVariants.length, referenced: quoteVariants.every((variant) => variant.$ref), schemas: quoteVariants, }, swapFills: components.swap.properties?.['fills']?.items, swaps: components.swapList.properties?.['data']?.items, swapsMeta: components.swapList.properties?.['meta'], }, operations: Object.fromEntries( Object.entries(operations).map(([name, operation]) => [name, summarize(operation)]), ), undescribedComponents: exchangeComponentNames.filter( (name) => !component(spec, name).description, ), }).toMatchInlineSnapshot(` { "components": [ "CreateExchangeQuoteRequest", "ExchangeDepthLevel", "ExchangeOhlcBucket", "ExchangeOrder", "ExchangeOrderFill", "ExchangeOrderFillList", "ExchangeOrderFillListMeta", "ExchangeOrderList", "ExchangeOrderListMeta", "ExchangeOrderPair", "ExchangePair", "ExchangePairDepth", "ExchangePairList", "ExchangePairListMeta", "ExchangePairOhlc", "ExchangePairToken", "ExchangeQuote", "ExchangeQuoteApproval", "ExchangeQuoteCall", "ExchangeQuoteDestinationToken", "ExchangeQuoteExactDestinationApprovalRequired", "ExchangeQuoteExactDestinationReady", "ExchangeQuoteExactDestinationSignatureRequired", "ExchangeQuoteExactSourceApprovalRequired", "ExchangeQuoteExactSourceReady", "ExchangeQuoteExactSourceSignatureRequired", "ExchangeQuoteMeta", "ExchangeQuoteSourceToken", "ExchangeQuoteTransaction", "ExchangeQuoteTypedData", "ExchangeRestingOrder", "ExchangeSwap", "ExchangeSwapFill", "ExchangeSwapList", "ExchangeSwapListMeta", "ExchangeSwapToken", "ExchangeValuedAmount", "FinalizeExchangeQuoteRequest", "FinalizedExchangeQuote", ], "formats": { "ohlc": "date-time", "order": "date-time", "orderFill": "date-time", "pair": "date-time", "swap": "date-time", }, "models": { "depthLevels": { "$ref": "#/components/schemas/ExchangeDepthLevel", }, "ohlcBuckets": { "$ref": "#/components/schemas/ExchangeOhlcBucket", }, "orderFills": { "$ref": "#/components/schemas/ExchangeOrderFill", }, "orderFillsMeta": { "$ref": "#/components/schemas/ExchangeOrderFillListMeta", }, "orders": { "$ref": "#/components/schemas/ExchangeRestingOrder", }, "ordersMeta": { "$ref": "#/components/schemas/ExchangeOrderListMeta", }, "pairDetail": { "$ref": "#/components/schemas/ExchangePair", }, "pairs": { "$ref": "#/components/schemas/ExchangePair", }, "pairsMeta": { "$ref": "#/components/schemas/ExchangePairListMeta", }, "quoteAmount": { "$ref": "#/components/schemas/ExchangeValuedAmount", }, "quoteApproval": { "$ref": "#/components/schemas/ExchangeQuoteApproval", }, "quoteMeta": { "$ref": "#/components/schemas/ExchangeQuoteMeta", }, "quoteToken": { "$ref": "#/components/schemas/ExchangeQuoteSourceToken", }, "quoteTransaction": { "$ref": "#/components/schemas/ExchangeQuoteTransaction", }, "quoteTypedData": { "$ref": "#/components/schemas/ExchangeQuoteTypedData", }, "quoteVariants": { "count": 6, "referenced": true, "schemas": [ { "$ref": "#/components/schemas/ExchangeQuoteExactSourceApprovalRequired", }, { "$ref": "#/components/schemas/ExchangeQuoteExactSourceReady", }, { "$ref": "#/components/schemas/ExchangeQuoteExactSourceSignatureRequired", }, { "$ref": "#/components/schemas/ExchangeQuoteExactDestinationApprovalRequired", }, { "$ref": "#/components/schemas/ExchangeQuoteExactDestinationReady", }, { "$ref": "#/components/schemas/ExchangeQuoteExactDestinationSignatureRequired", }, ], }, "swapFills": { "$ref": "#/components/schemas/ExchangeSwapFill", }, "swaps": { "$ref": "#/components/schemas/ExchangeSwap", }, "swapsMeta": { "$ref": "#/components/schemas/ExchangeSwapListMeta", }, }, "operations": { "createQuote": { "errors": { "400": [ "api_key_malformed", "body_invalid", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "quote_amount_out_of_range", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "quote_not_available", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "createExchangeQuote", "request": { "$ref": "#/components/schemas/CreateExchangeQuoteRequest", }, "response": { "$ref": "#/components/schemas/ExchangeQuote", }, }, "finalizeQuote": { "errors": { "400": [ "api_key_malformed", "body_invalid", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "swap_continuation_invalid", "swap_provider_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "finalizeExchangeQuote", "request": { "$ref": "#/components/schemas/FinalizeExchangeQuoteRequest", }, "response": { "$ref": "#/components/schemas/FinalizedExchangeQuote", }, }, "getDepth": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "pair_invalid", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "pair_not_found", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getPairDepth", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangePairDepth", }, }, "getOhlc": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "pair_invalid", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "pair_not_found", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getPairOhlc", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangePairOhlc", }, }, "getOrder": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "order_invalid", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "order_not_found", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getOrder", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangeOrder", }, }, "getOrderFills": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "order_invalid", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getOrderFills", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangeOrderFillList", }, }, "getOrders": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "pair_not_found", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getOrders", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangeOrderList", }, }, "getPair": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "pair_invalid", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "pair_not_found", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getPair", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangePair", }, }, "getPairs": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getPairs", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangePairList", }, }, "getSwaps": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getSwaps", "request": undefined, "response": { "$ref": "#/components/schemas/ExchangeSwapList", }, }, }, "undescribedComponents": [], } `) }) test('returns exact chain and pair validation errors', async () => { const app = TestApp.create({ auth: false }) const invalidChain = await app.request('/v1/exchange/pairs?chainId=abc') const unsupportedChain = await app.request('/v1/exchange/pairs?chainId=999999') const invalidPair = await app.request('/v1/exchange/pairs/not-an-address') expect([ (await TestApp.json(invalidChain, Schema.ErrorResponse)).error.code, (await TestApp.json(unsupportedChain, Schema.ErrorResponse)).error.code, (await TestApp.json(invalidPair, Schema.ErrorResponse)).error.code, ]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported', 'pair_invalid']) }) }) describe('schema.createQuote.TypedData', () => { test('accepts provider-specific EIP-712 types', () => { expect( Exchanges.schema.createQuote.TypedData.safeParse({ domain: { chainId: 4217, name: 'Market Maker' }, message: { amount: '1000000', recipient: Relay.accounts[1]!.address }, primaryType: 'MarketOrder', types: { MarketOrder: [ { name: 'amount', type: 'uint256' }, { name: 'recipient', type: 'address' }, ], }, }).success, ).toBe(true) }) }) type Client = ReturnType type OrdersQuery = { chainId?: string cursor?: string include?: string limit?: string maker?: string order?: 'asc' | 'desc' side?: 'bid' | 'ask' sort?: 'tick' | 'time' } /** * Find a real DEX pair that currently has at least one resting order. The * exchange's small pair set means most pairs have empty books at any given * moment, so we scan the pairs listing until one returns orders for the * supplied query (scoped via `?base=`). Returns `null` when *every* scanned * pair returns a 200 with an empty page, which is rare but valid on quiet * testnets — callers should skip the assertion rather than fail. * * A non-200 response is treated as a hard failure: it surfaces real bugs (such * as upstream schema mismatches) instead of letting them silently degrade to a * skipped test. Empty 200 responses are tolerated and counted, with a final * `expect` ensuring at least one pair was scanned so a 0-pair listing also * fails loudly. */ async function findPairWithOrders( client: Client, query: OrdersQuery = {}, ): Promise<{ body: OrdersResponse; pair: PairsResponse['data'][number] } | null> { const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '10' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) expect(pairs.data.length).toBeGreaterThan(0) for (const pair of pairs.data) { const response = await client.v1.exchange.orders.$get( { query: { ...query, base: pair.base.address } }, TestApp.auth, ) // A 5xx from the orders endpoint indicates a real regression (e.g. an // upstream schema mismatch). Fail fast with the error body for context // instead of silently skipping the pair. if (response.status !== 200) { const body = await response.text() throw new Error(`orders ${response.status} for base=${pair.base.address}: ${body}`) } const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) if (body.data.length > 0) return { body, pair } } return null } /** * Convenience wrapper around `findPairWithOrders` for tests that only care * about the response body under a given query. Returns `null` when no pair * had orders. */ async function fetchOrdersForBookedPair(query: OrdersQuery): Promise { const found = await findPairWithOrders(TestApp.client(), query) return found?.body ?? null } describe('GET /exchange/pairs', () => { test('returns a page of trading pairs ordered newest-first by default', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs.$get({ query: { limit: '5' } }, TestApp.auth) const body = await TestApp.json(response, Exchanges.schema.getPairs.Response) expect(response.status).toBe(200) expect(body.data.length).toBeLessThanOrEqual(5) if (body.data.length > 0) { const pair = body.data[0]! expect(pair.key).toMatch(/^0x[0-9a-f]{64}$/) expect(pair.base.address).toMatch(/^0x[0-9a-f]{40}$/) expect(pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/) // Newest-first by default. for (let i = 1; i < body.data.length; i++) { expect(body.data[i - 1]!.blockNumber).toBeGreaterThanOrEqual(body.data[i]!.blockNumber) } } }, 30000) test('embeds an exact total count on demand via include=totalCount', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs.$get( { query: { include: 'totalCount', limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Exchanges.schema.getPairs.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('pairs_count;dur=')).toBe(true) expect(body.meta).toBeDefined() expect(Number.isInteger(body.meta?.totalCount)).toBe(true) expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length) // The count is sort-key-pruned on ClickHouse, so it is exact (never capped). expect(body.meta!.totalCountCapped).toBe(false) const bare = await client.v1.exchange.pairs.$get({ query: { limit: '5' } }, TestApp.auth) const bareBody = await TestApp.json(bare, Exchanges.schema.getPairs.Response) expect(bare.status).toBe(200) expect(bareBody.meta).toBeUndefined() }, 30000) test('serves a bounded positional page via `page`', async () => { const client = TestApp.client() // Ascending creation order anchors the feed at the first pair, so // positional pages are deterministic against the head page. const headResponse = await client.v1.exchange.pairs.$get( { query: { limit: '10', order: 'asc' } }, TestApp.auth, ) const head = await TestApp.json(headResponse, Exchanges.schema.getPairs.Response) expect(headResponse.status).toBe(200) if (head.data.length < 10) return const offsetResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5', order: 'asc', page: '2' } }, TestApp.auth, ) const page = await TestApp.json(offsetResponse, Exchanges.schema.getPairs.Response) expect(offsetResponse.status).toBe(200) expect(page.data.map((pair) => pair.key)).toEqual( head.data.slice(5, 10).map((pair) => pair.key), ) }, 30000) test('embeds token metadata for both sides when requested via `include=tokens`', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs.$get( { query: { limit: '5', include: 'tokens' } }, TestApp.auth, ) const body = await TestApp.json(response, Exchanges.schema.getPairs.Response) expect(response.status).toBe(200) if (body.data.length === 0) return const enriched = body.data.find((pair) => typeof pair.base.symbol === 'string') // At least one row should resolve metadata against the live RPC; both sides // share the same metadata path so when `base.symbol` is present, so is the // rest of the trimmed token reference. if (enriched) { expect(typeof enriched.base.symbol).toBe('string') expect(typeof enriched.base.decimals).toBe('number') expect(typeof enriched.quote.symbol).toBe('string') } }, 60000) test('ranks pairs by DEX-escrow base balance when `sort=liquidity`', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs.$get( { query: { limit: '5', sort: 'liquidity' } }, TestApp.auth, ) const body = await TestApp.json(response, Exchanges.schema.getPairs.Response) expect(response.status).toBe(200) expect(body.data.length).toBeLessThanOrEqual(5) if (body.data.length > 0) { for (const pair of body.data) { expect(typeof pair.liquidity).toBe('string') expect(pair.liquidity).toMatch(/^\d+$/) expect(BigInt(pair.liquidity!)).toBeGreaterThan(0n) } // Newest-first balance ordering. for (let i = 1; i < body.data.length; i++) { expect(BigInt(body.data[i - 1]!.liquidity!)).toBeGreaterThanOrEqual( BigInt(body.data[i]!.liquidity!), ) } } }, 30000) }) describe('GET /exchange/pairs/:base', () => { test('returns the pair identified by its base token', async () => { const client = TestApp.client() const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) if (pairs.data.length === 0) return const pair = pairs.data[0]! const response = await client.v1.exchange.pairs[':base'].$get( { param: { base: pair.base.address }, query: {} }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getPair.Response) expect(body.key).toBe(pair.key) expect(body.base.address).toBe(pair.base.address) expect(body.quote.address).toBe(pair.quote.address) expect(Number.isInteger(body.blockNumber)).toBe(true) expect(body.transactionHash).toMatch(/^0x[0-9a-f]{64}$/) expect(Number.isNaN(Date.parse(body.timestamp))).toBe(false) // Token metadata is opt-in: the base response carries bare addresses. expect(body.base.symbol).toBeUndefined() }, 30000) test('embeds token metadata when requested via `include=tokens`', async () => { const client = TestApp.client() const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) if (pairs.data.length === 0) return const pair = pairs.data[0]! const response = await client.v1.exchange.pairs[':base'].$get( { param: { base: pair.base.address }, query: { include: 'tokens' } }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getPair.Response) // Best-effort: at least one side should resolve metadata for a real pair. const sides = [body.base, body.quote] expect(sides.some((side) => typeof side.decimals === 'number')).toBe(true) }, 60000) test('returns 404 for an unknown base token', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs[':base'].$get( { param: { base: `0x${'1'.repeat(40)}` as const }, query: {} }, TestApp.auth, ) expect(response.status).toBe(404) }, 30000) }) describe('GET /exchange/pairs/:base/ohlc', () => { test('returns OHLC buckets for a real pair', async () => { const client = TestApp.client() const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) if (pairs.data.length === 0) return const pair = pairs.data[0]! const response = await client.v1.exchange.pairs[':base'].ohlc.$get( { param: { base: pair.base.address }, query: { interval: '1h', window: '7d' } }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getPairOhlc.Response) expect(body.interval).toBe('1h') expect(body.window).toBe('7d') expect(body.base.address).toBe(pair.base.address) expect(body.quote.address).toBe(pair.quote.address) expect(typeof body.truncated).toBe('boolean') expect(Array.isArray(body.data)).toBe(true) // Buckets, if any, are ordered oldest → newest with strictly increasing // timestamps and well-formed rate / volume shapes. for (let i = 0; i < body.data.length; i++) { const bucket = body.data[i]! expect(bucket.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/) expect(bucket.open).toMatch(/^\d+(\.\d+)?$/) expect(bucket.close).toMatch(/^\d+(\.\d+)?$/) expect(bucket.high).toMatch(/^\d+(\.\d+)?$/) expect(bucket.low).toMatch(/^\d+(\.\d+)?$/) expect(Number(bucket.high)).toBeGreaterThanOrEqual(Number(bucket.low)) expect(bucket.fillCount).toBeGreaterThan(0) expect(BigInt(bucket.volume.base)).toBeGreaterThanOrEqual(0n) expect(BigInt(bucket.volume.quote)).toBeGreaterThanOrEqual(0n) if (i > 0) { expect(Date.parse(bucket.timestamp)).toBeGreaterThan( Date.parse(body.data[i - 1]!.timestamp), ) } } }, 60000) test('rejects OHLC request whose `interval × window` exceeds the bucket cap', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs[':base'].ohlc.$get( { // `1m` × `24h` = 1440 buckets, well above the 500-bucket cap. param: { base: `0x${'1'.repeat(40)}` as const }, query: { interval: '1m', window: '24h' }, }, TestApp.auth, ) expect(response.status).toBe(400) }, 15000) test('returns 404 OHLC for an unknown pair', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs[':base'].ohlc.$get( { param: { base: `0x${'1'.repeat(40)}` as const }, query: {} }, TestApp.auth, ) expect(response.status).toBe(404) }, 30000) test('embeds token metadata on OHLC when requested via `include=tokens`', async () => { const client = TestApp.client() const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) if (pairs.data.length === 0) return const pair = pairs.data[0]! const response = await client.v1.exchange.pairs[':base'].ohlc.$get( { param: { base: pair.base.address }, query: { include: 'tokens', interval: '1h', window: '24h' }, }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getPairOhlc.Response) // Best-effort: at least one side should resolve metadata for a real pair. const sides = [body.base, body.quote] expect(sides.some((side) => typeof side.decimals === 'number')).toBe(true) }, 60000) }) describe('GET /exchange/orders?base=… (pair-scoped orders)', () => { test('returns a page of resting orders for a real pair', async () => { const client = TestApp.client() const found = await findPairWithOrders(client, { limit: '5' }) if (!found) return const { body, pair } = found expect(body.data.length).toBeGreaterThan(0) expect(body.data.length).toBeLessThanOrEqual(5) expect(typeof body.truncated).toBe('boolean') // `nextCursor` is `null` when the page exhausts the snapshot. expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true) for (const order of body.data) { expect(order.orderId).toMatch(/^\d+$/) expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/) expect(order.amount).toMatch(/^\d+$/) expect(order.remaining).toMatch(/^\d+$/) expect(BigInt(order.remaining)).toBeGreaterThan(0n) expect(BigInt(order.remaining)).toBeLessThanOrEqual(BigInt(order.amount)) expect(['bid', 'ask']).toContain(order.side) expect(Number.isInteger(order.tick)).toBe(true) expect(order.rate).toMatch(/^\d+(\.\d+)?$/) // `price` is quote-per-base regardless of side; `rate` is the // taker-perspective ratio (price for bids, 1/price for asks). A tick=0 // order — peg — must render both as exactly 1.00000. expect(order.price).toMatch(/^\d+(\.\d+)?$/) if (order.tick === 0) { expect(Number(order.price)).toBe(1) expect(Number(order.rate)).toBe(1) } expect(order.transactionHash).toMatch(/^0x[0-9a-f]{64}$/) expect(Number.isInteger(order.blockNumber)).toBe(true) expect(order.blockNumber).toBeGreaterThanOrEqual(0) expect(Number.isInteger(order.logIndex)).toBe(true) expect(order.logIndex).toBeGreaterThanOrEqual(0) expect(Number.isNaN(Date.parse(order.placedAt))).toBe(false) // Every row is annotated with the pair we scoped to. expect(order.pair.key).toBe(pair.key) expect(order.pair.base.address).toBe(pair.base.address) expect(order.pair.quote.address).toBe(pair.quote.address) } }, 120000) test('sorts resting orders by tick descending by default', async () => { const body = await fetchOrdersForBookedPair({ limit: '10' }) if (!body) return for (let i = 1; i < body.data.length; i++) expect(body.data[i - 1]!.tick).toBeGreaterThanOrEqual(body.data[i]!.tick) }, 120000) test('sorts resting orders by tick ascending when `order=asc`', async () => { const body = await fetchOrdersForBookedPair({ limit: '10', order: 'asc' }) if (!body) return for (let i = 1; i < body.data.length; i++) expect(body.data[i - 1]!.tick).toBeLessThanOrEqual(body.data[i]!.tick) }, 120000) test('sorts resting orders by placement time when `sort=time`', async () => { const body = await fetchOrdersForBookedPair({ limit: '10', sort: 'time' }) if (!body) return for (let i = 1; i < body.data.length; i++) { const previous = body.data[i - 1]! const current = body.data[i]! // Default `order=desc` against `sort=time`: newest placement first, // tie-breaking on log index. expect( previous.blockNumber > current.blockNumber || (previous.blockNumber === current.blockNumber && previous.logIndex >= current.logIndex), ).toBe(true) } }, 120000) test('filters resting orders by `side`', async () => { const bids = await fetchOrdersForBookedPair({ limit: '10', side: 'bid' }) if (bids) for (const order of bids.data) expect(order.side).toBe('bid') // Asks are not always present alongside bids on the same pair, so this // assertion runs against whichever pair happens to have ask orders. const asks = await fetchOrdersForBookedPair({ limit: '10', side: 'ask' }) if (asks) for (const order of asks.data) expect(order.side).toBe('ask') }, 180000) test('filters resting orders to a single maker', async () => { // Seed off the default-sort head page to discover a maker that has at // least one resting order on a real pair, then filter to just them. const seed = await findPairWithOrders(TestApp.client(), { limit: '5' }) if (!seed) return const maker = seed.body.data[0]!.maker const client = TestApp.client() const response = await client.v1.exchange.orders.$get( { query: { base: seed.pair.base.address, limit: '10', maker } }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) expect(body.data.length).toBeGreaterThan(0) for (const order of body.data) expect(order.maker).toBe(maker) }, 120000) test('embeds pair token metadata per row when requested via `include=tokens`', async () => { const seed = await findPairWithOrders(TestApp.client(), { include: 'tokens', limit: '5', }) if (!seed) return const { body, pair } = seed // At least one side resolves metadata against the live RPC; both sides // share the same resolution path so this gates the embedding shape. const row = body.data[0]! expect(row.pair.base.address).toBe(pair.base.address) expect(row.pair.quote.address).toBe(pair.quote.address) const sides = [row.pair.base, row.pair.quote] expect(sides.some((side) => typeof side.symbol === 'string')).toBe(true) }, 120000) test('paginates resting orders without skipping or duplicating rows', async () => { const client = TestApp.client() const seed = await findPairWithOrders(client, { limit: '5' }) if (!seed) return if (seed.body.nextCursor === null) return const next = await client.v1.exchange.orders.$get( { query: { base: seed.pair.base.address, cursor: seed.body.nextCursor, limit: '5' } }, TestApp.auth, ) expect(next.status).toBe(200) const second = await TestApp.json(next, Exchanges.schema.getOrders.Response) const firstIds = new Set(seed.body.data.map((order) => order.orderId)) for (const order of second.data) expect(firstIds.has(order.orderId)).toBe(false) }, 180000) test('returns 404 for an unknown pair on the orders endpoint', async () => { const client = TestApp.client() const response = await client.v1.exchange.orders.$get( { query: { base: `0x${'1'.repeat(40)}` as const } }, TestApp.auth, ) expect(response.status).toBe(404) }, 30000) }) describe('GET /exchange/pairs/:base/depth', () => { test('returns orderbook depth when token metadata is requested', async () => { const client = TestApp.client() const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) if (pairs.data.length === 0) return // The RPC fans out ~400 `getTickLevel` reads through a single deployless // multicall; on testnet that occasionally exceeds the upstream RPC's // multicall return-size budget and surfaces as 502 from our handler. // Treat that as "skip" so the test stays green even when the upstream // throttles the multicall response. let body: z.output | null = null let pair: (typeof pairs.data)[number] | null = null let timing: string | null = null for (const candidate of pairs.data) { const response = await client.v1.exchange.pairs[':base'].depth.$get( { param: { base: candidate.base.address }, query: { include: 'tokens' } }, TestApp.auth, ) if (response.status === 502) return expect(response.status).toBe(200) body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response) pair = candidate timing = response.headers.get('server-timing') if (body.bids.length > 0 || body.asks.length > 0) break } if (!body || !pair) return expect(body.base.address).toBe(pair.base.address) expect(body.quote.address).toBe(pair.quote.address) expect(timing).toContain('pair_lookup;dur=') expect(timing).not.toContain('pair_index;dur=') // Asks are emitted lowest-tick first (best ask outward); bids are // emitted highest-tick first (best bid outward). Per-row `cumulativeSize` // must equal the running sum of `size` from the head of the same side. const checkSide = (side: typeof body.asks, label: 'asks' | 'bids') => { let expected = 0n let previousTick: number | undefined for (const level of side) { expect(level.size).toMatch(/^\d+$/) expect(level.price).toMatch(/^\d+(\.\d+)?$/) expect(BigInt(level.size)).toBeGreaterThan(0n) expected += BigInt(level.size) expect(BigInt(level.cumulativeSize)).toBe(expected) // Ticks are monotone outward from peg per side. if (previousTick !== undefined) { if (label === 'asks') expect(level.tick).toBeGreaterThan(previousTick) else expect(level.tick).toBeLessThan(previousTick) } previousTick = level.tick } } checkSide(body.asks, 'asks') checkSide(body.bids, 'bids') }, 60000) test('caps depth levels per side when `levels` is set', async () => { const client = TestApp.client() const pairsResponse = await client.v1.exchange.pairs.$get( { query: { limit: '5' } }, TestApp.auth, ) const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response) if (pairs.data.length === 0) return for (const pair of pairs.data) { const response = await client.v1.exchange.pairs[':base'].depth.$get( { param: { base: pair.base.address }, query: { levels: '3' } }, TestApp.auth, ) if (response.status === 502) return expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response) expect(body.asks.length).toBeLessThanOrEqual(3) expect(body.bids.length).toBeLessThanOrEqual(3) } }, 60000) test('returns 404 for an unknown depth pair', async () => { const client = TestApp.client() const response = await client.v1.exchange.pairs[':base'].depth.$get( { param: { base: `0x${'1'.repeat(40)}` as const }, query: {} }, TestApp.auth, ) expect(response.status).toBe(404) }, 30000) }) describe('GET /exchange/swaps', () => { type GlobalSwapsResponse = z.output test('validates denominations without swap rows', async () => { const client = TestApp.client({ fx: { oracle: fixed } }) const response = await client.v1.exchange.swaps.$get( { query: { transactionHash: zeroHash, 'valuation.currency': 'JPY' } }, TestApp.auth, ) expect(response.status).toBe(400) }) test('parses granular token includes', () => { expect( Exchanges.schema.getSwaps.Query.parse({ include: 'token.logoUri,token.verified', }).include, ).toMatchInlineSnapshot(` [ "token.logoUri", "token.verified", ] `) expect(Exchanges.schema.getSwaps.Query.safeParse({ include: 'tokens' }).success).toBe(false) }) test.skipIf(Runtime.get().mode !== 'localnet')( 'values swap legs in a requested denomination', async () => { const db = TestApp.database() await TestApp.verifiedSeed(db, Runtime.get().chainId) const client = TestApp.client({ db, fx: { oracle: fixed } }) const response = await client.v1.exchange.swaps.$get( { query: { limit: '10', 'valuation.currency': 'AUD' } }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response) expect(body.data.length).toBeGreaterThan(0) // Curated legs value at the fixed 1.6 USD -> AUD rate; uncurated // created-token legs stay null. const legs = body.data.flatMap((swap) => [ swap.sourceAmount, swap.destinationAmount, ...swap.fills.flatMap((fill) => [fill.sourceAmount, fill.destinationAmount]), ]) const valued = legs.filter((leg) => leg.valuation) expect(valued.length).toBeGreaterThan(0) for (const leg of valued) expect(leg.valuation).toEqual({ amount: core_Value.format((BigInt(leg.baseUnits) * 16n) / 10n, 6), currency: 'AUD', }) expect(body.meta?.valuation).toMatchInlineSnapshot(` { "asOf": "2026-01-01T00:00:00.000Z", "basis": "nominal", "source": "fixed", } `) }, ) test('returns a global, newest-first page of swaps across all pairs', async () => { const client = TestApp.client() const response = await client.v1.exchange.swaps.$get({ query: { limit: '10' } }, TestApp.auth) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response) expect(body.data.length).toBeGreaterThan(0) expect(body.data.length).toBeLessThanOrEqual(10) expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=') for (const swap of body.data) { expect(swap.taker).toMatch(/^0x[0-9a-f]{40}$/) expect(swap.transactionHash).toMatch(/^0x[0-9a-f]{64}$/) expect(['exactSource', 'exactDestination', null]).toContain(swap.mode) expect(swap.sourceToken.address).toMatch(/^0x[0-9a-f]{40}$/) expect(swap.sourceToken).not.toHaveProperty('amount') expect(typeof swap.sourceToken.currency).toBe('string') expect(typeof swap.sourceToken.decimals).toBe('number') expect(typeof swap.sourceToken.name).toBe('string') expect(typeof swap.sourceToken.symbol).toBe('string') expect(swap.sourceToken.logoUri).toBeUndefined() expect(swap.sourceToken.verified).toBeUndefined() expect(swap.sourceAmount.baseUnits).toMatch(/^\d+$/) expect(swap.sourceAmount.currency).toBe(swap.sourceToken.currency) expect(swap.sourceAmount.decimals).toBe(swap.sourceToken.decimals) expect(swap.sourceAmount.formatted).toMatch(/^\d+(\.\d+)?$/) expect('valuation' in swap.sourceAmount).toBe(false) expect(swap.destinationToken.address).toMatch(/^0x[0-9a-f]{40}$/) expect(swap.destinationToken).not.toHaveProperty('amount') expect(typeof swap.destinationToken.currency).toBe('string') expect(typeof swap.destinationToken.decimals).toBe('number') expect(typeof swap.destinationToken.name).toBe('string') expect(typeof swap.destinationToken.symbol).toBe('string') expect(swap.destinationToken.logoUri).toBeUndefined() expect(swap.destinationToken.verified).toBeUndefined() expect(swap.destinationAmount.baseUnits).toMatch(/^\d+$/) expect(swap.destinationAmount.currency).toBe(swap.destinationToken.currency) expect(swap.destinationAmount.decimals).toBe(swap.destinationToken.decimals) expect(swap.destinationAmount.formatted).toMatch(/^\d+(\.\d+)?$/) expect('valuation' in swap.destinationAmount).toBe(false) expect(swap.rate).toMatch(/^\d+(\.\d+)?$/) expect(swap.filledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/) // `route` spans source → destination; its ends mirror the side tokens. expect(swap.route.length).toBeGreaterThanOrEqual(2) expect(swap.route[0]).toBe(swap.sourceToken.address) expect(swap.route.at(-1)).toBe(swap.destinationToken.address) // Per-fill detail is nested, in execution order, and chains hop-wise: // every fill trades adjacent tokens of the route. expect(swap.fills.length).toBeGreaterThan(0) for (const fill of swap.fills) { expect(fill.orderId).toMatch(/^\d+$/) expect(fill.maker).toMatch(/^0x[0-9a-f]{40}$/) expect(fill.price).toMatch(/^\d+(\.\d+)?$/) expect(swap.route).toContain(fill.sourceToken.address) expect(swap.route).toContain(fill.destinationToken.address) expect(fill.sourceToken).not.toHaveProperty('amount') expect(fill.sourceAmount.baseUnits).toMatch(/^\d+$/) expect(fill.sourceAmount.currency).toBe(fill.sourceToken.currency) expect(fill.sourceAmount.decimals).toBe(fill.sourceToken.decimals) expect('valuation' in fill.sourceAmount).toBe(false) expect(fill.destinationToken).not.toHaveProperty('amount') expect(fill.destinationAmount.baseUnits).toMatch(/^\d+$/) expect(fill.destinationAmount.currency).toBe(fill.destinationToken.currency) expect(fill.destinationAmount.decimals).toBe(fill.destinationToken.decimals) expect('valuation' in fill.destinationAmount).toBe(false) } for (let i = 1; i < swap.fills.length; i++) expect(swap.fills[i]!.logIndex).toBeGreaterThan(swap.fills[i - 1]!.logIndex) } // Newest-first (rows order by block, then first fill within the block). for (let i = 1; i < body.data.length; i++) { const prev = body.data[i - 1]! const curr = body.data[i]! expect( prev.blockNumber > curr.blockNumber || (prev.blockNumber === curr.blockNumber && prev.fills[0]!.logIndex >= curr.fills[0]!.logIndex), ).toBe(true) } }, 60000) test('paginates with an opaque cursor without skipping or duplicating rows', async () => { const client = TestApp.client() const first = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth) const firstBody = await TestApp.json(first, Exchanges.schema.getSwaps.Response) if (firstBody.nextCursor === null) return const second = await client.v1.exchange.swaps.$get( { query: { cursor: firstBody.nextCursor, limit: '5' } }, TestApp.auth, ) const secondBody = await TestApp.json(second, Exchanges.schema.getSwaps.Response) expect(second.headers.get('cache-control')).toMatchInlineSnapshot( `"private, max-age=60, stale-while-revalidate=300"`, ) // A swap row is unique on `(block, first fill logIndex)`. const key = (swap: GlobalSwapsResponse['data'][number]) => `${swap.blockNumber}:${swap.fills[0]!.logIndex}` const firstKeys = new Set(firstBody.data.map(key)) for (const swap of secondBody.data) expect(firstKeys.has(key(swap))).toBe(false) }, 60000) test('filters the global feed by transactionHash', async () => { const client = TestApp.client() const seed = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth) const seedBody = await TestApp.json(seed, Exchanges.schema.getSwaps.Response) const row = seedBody.data[0] if (!row) return const response = await client.v1.exchange.swaps.$get( { query: { limit: '5', transactionHash: row.transactionHash } }, TestApp.auth, ) const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response) // The seeded swap's transaction is on the head page, so it must match, // and every returned swap belongs to that transaction. expect(body.data.length).toBeGreaterThan(0) for (const swap of body.data) expect(swap.transactionHash).toBe(row.transactionHash) }, 60000) test('filters the global feed by maker', async () => { const client = TestApp.client() const seed = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth) const seedBody = await TestApp.json(seed, Exchanges.schema.getSwaps.Response) const row = seedBody.data[0] if (!row) return const maker = row.fills[0]!.maker const response = await client.v1.exchange.swaps.$get( { query: { limit: '5', maker } }, TestApp.auth, ) const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response) for (const swap of body.data) expect(swap.fills.some((fill) => fill.maker === maker)).toBe(true) }, 60000) test('includes curated token fields on swap sides and fills', async () => { const client = TestApp.client() const response = await client.v1.exchange.swaps.$get( { query: { include: 'token.logoUri,token.verified', limit: '10' } }, TestApp.auth, ) expect(response.status).toBe(200) expect(response.headers.get('server-timing')).not.toContain('token_logo_uri;dur=') const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response) if (body.data.length === 0) return const enriched = body.data[0]! expect(typeof enriched.sourceToken.verified).toBe('boolean') expect(typeof enriched.destinationToken.verified).toBe('boolean') for (const fill of enriched.fills) { expect(typeof fill.sourceToken.verified).toBe('boolean') expect(typeof fill.destinationToken.verified).toBe('boolean') } }, 60000) }) describe('POST and QUERY /exchange/quotes', () => { test('POST and QUERY accept the same body and return equivalent quote errors', async () => { const client = TestApp.client() const destinationToken = Schema.TokenAddress.parse(Addresses.pathUsd) const request = { json: { account: Relay.accounts[1]!.address, amount: (2n ** 128n).toString(), destinationToken, mode: 'exactSource' as const, slippageBps: 50, sourceToken: '0x20c0000000000000000000008f5425160ebe5525' as const, }, query: {}, } const [post, query] = await Promise.all([ client.v1.exchange.quotes.$post(request, TestApp.auth), client.v1.exchange.quotes.$query(request, TestApp.auth), ]) expect([post.status, query.status]).toEqual([404, 404]) expect([post.headers.get('cache-control'), query.headers.get('cache-control')]).toEqual([ 'no-store', 'no-store', ]) expect([await post.json(), await query.json()]).toMatchObject([ { error: { code: 'quote_not_available' } }, { error: { code: 'quote_not_available' } }, ]) }) test('validates denominations without verification data', async () => { const db = () => Db.postgres({ connectionString: 'postgresql://postgres:postgres@127.0.0.1:1/none' }) const client = TestApp.client({ db, fx: { oracle: fixed } }) const response = await client.v1.exchange.quotes.$post( { json: { account: Relay.accounts[1]!.address, amount: '1', destinationToken: Addresses.pathUsd, mode: 'exactSource', slippageBps: 50, sourceToken: '0x20c0000000000000000000008f5425160ebe5525', }, query: { 'valuation.currency': 'JPY' }, }, TestApp.auth, ) expect(response.status).toBe(400) }) test('parses granular token includes', () => { expect( Exchanges.schema.createQuote.Query.parse({ include: 'token.logoUri,token.verified', }).include, ).toMatchInlineSnapshot(` [ "token.logoUri", "token.verified", ] `) expect(Exchanges.schema.createQuote.Query.safeParse({ include: 'tokens' }).success).toBe(false) }) test.each([ { destinationToken: '0x20c0000000000000000000008f5425160ebe5525' as const, label: 'identical tokens', slippageBps: 50, sourceToken: '0x20c0000000000000000000008f5425160ebe5525' as const, }, { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50' as const, label: 'invalid slippage', slippageBps: 10_000, sourceToken: '0x20c0000000000000000000008f5425160ebe5525' as const, }, ])('rejects $label', async ({ destinationToken, slippageBps, sourceToken }) => { const response = await TestApp.client().v1.exchange.quotes.$post( { json: { account: Relay.accounts[1]!.address, amount: '1000000', destinationToken, mode: 'exactSource', slippageBps, sourceToken, }, query: {}, }, TestApp.auth, ) expect(response.status).toBe(400) expect(await response.json()).toMatchObject({ error: { code: 'body_invalid' } }) }) test('publishes valid request and executable call examples', async () => { const app = TestApp.create({ auth: false }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operation = spec.paths['/v1/exchange/quotes']?.post if (!operation) throw new Error('Missing create quote OpenAPI operation') const requestSchema = resolveSchema( spec, operation.requestBody?.content?.['application/json']?.schema, ) const requestExample = Object.fromEntries( ['account', 'amount', 'destinationToken', 'mode', 'slippageBps', 'sourceToken'].map( (property) => [property, requestSchema?.properties?.[property]?.examples?.[0]], ), ) expect(requestExample).toMatchObject({ amount: '1000000', destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', mode: 'exactSource', slippageBps: 50, sourceToken: '0x20c0000000000000000000008f5425160ebe5525', }) expect(requestExample['account']).toMatch(/^0x[\da-f]{40}$/) expect(Exchanges.schema.createQuote.Request.safeParse(requestExample).success).toBe(true) const responseSchema = resolveSchema( spec, operation.responses?.['200']?.content?.['application/json']?.schema, ) const variants = responseSchema?.anyOf if (!variants) throw new Error('Missing create quote response variants') for (const [variants_amount, names] of [ [variants.slice(0, 3), ['destinationAmount', 'destinationAmountMin', 'sourceAmount']], [variants.slice(3), ['destinationAmount', 'sourceAmount', 'sourceAmountMax']], ] as const) for (const variant_ref of variants_amount) { const variant = resolveSchema(spec, variant_ref) for (const name of names) { const amount = resolveSchema(spec, variant.properties?.[name]) expect(amount?.required).toEqual(['baseUnits', 'currency', 'decimals', 'formatted']) expect( Object.fromEntries( ['baseUnits', 'currency', 'decimals', 'formatted'].map((property) => [ property, amount?.properties?.[property]?.examples?.[0], ]), ), ).toEqual({ baseUnits: '1000000', currency: 'USD', decimals: 6, formatted: '1' }) } } for (const variant_ref of variants) { const variant = resolveSchema(spec, variant_ref) for (const name of ['destinationToken', 'sourceToken']) { const token = resolveSchema(spec, variant.properties?.[name]) expect(token?.required).toEqual(['address', 'currency', 'decimals', 'name', 'symbol']) expect(token?.properties).not.toHaveProperty('amount') expect(token?.properties).not.toHaveProperty('maximumAmount') expect(token?.properties).not.toHaveProperty('minimumAmount') expect(token?.required).not.toContain('logoUri') expect(token?.required).not.toContain('verified') } } const ready = variants .map((variant) => resolveSchema(spec, variant)) .find((variant) => variant.properties?.['transaction']) const transaction = resolveSchema(spec, ready?.properties?.['transaction']) const callsExample = transaction.properties?.['calls']?.examples?.[0] if (!Array.isArray(callsExample)) throw new Error('Missing create quote call examples') const calls = callsExample.map((call) => Exchanges.schema.createQuote.Call.parse(call)) expect(decodeFunctionData({ abi: Abis.tip20, data: calls[0]!.data })).toEqual({ args: [Address.checksum(Addresses.stablecoinDex), 1_000_000n], functionName: 'approve', }) expect(decodeFunctionData({ abi: Abis.stablecoinDex, data: calls[1]!.data })).toEqual({ args: [ Address.checksum('0x20c0000000000000000000008f5425160ebe5525'), Address.checksum('0x20c000000000000000000000b9537d11c60e8b50'), 1_000_000n, 995_000n, ], functionName: 'swapExactAmountIn', }) const signatureRequired = variants .map((variant) => resolveSchema(spec, variant)) .find((variant) => variant.properties?.['typedData']) const typedData = resolveSchema(spec, signatureRequired?.properties?.['typedData']) expect(typedData?.properties?.['primaryType']?.examples).toEqual(['PermitSingle']) expect(typedData?.properties?.['domain']?.examples?.[0]).toMatchObject({ chainId: 4217, name: 'Permit2', }) }) describe.skipIf(Runtime.get().mode !== 'localnet')('localnet quotes', () => { const account = Relay.accounts[1]! let baseToken: z.output beforeAll(async () => { const rpc = Relay.getClient({ account }) const created = await Actions.token.createSync(rpc, { currency: 'USD', name: 'Quote Base', symbol: 'QBASE', }) baseToken = Schema.TokenAddress.parse(created.token) await Actions.token.grantRolesSync(rpc, { roles: ['issuer', 'pause', 'unpause'], to: account.address, token: baseToken, }) await Actions.token.mintSync(rpc, { amount: 2_000_000_000n, to: account.address, token: baseToken, }) await Actions.token.approveSync(rpc, { amount: 1_000_000_000n, spender: Addresses.stablecoinDex, token: baseToken, }) await Actions.token.approveSync(rpc, { amount: 1_000_000_000n, spender: Addresses.stablecoinDex, token: Addresses.pathUsd, }) await Actions.dex.createPairSync(rpc, { base: baseToken }) await Actions.dex.placeSync(rpc, { amount: 1_000_000_000n, tick: 0, token: baseToken, type: 'buy', }) await Actions.dex.placeSync(rpc, { amount: 1_000_000_000n, tick: -10, token: baseToken, type: 'sell', }) }, 120_000) test('quotes exact-source liquidity and returns approval then sell calls', async () => { // Pin an empty curated list so optional curated fields stay deterministic. const db = TestApp.database() await TestApp.verifiedSeed(db, Runtime.get().chainId, { tokens: [] }) const client = TestApp.client({ db }) const request = { account: account.address, amount: '1', destinationToken: Addresses.pathUsd, mode: 'exactSource', slippageBps: 50, sourceToken: baseToken, } as const const response = await client.v1.exchange.quotes.$post( { json: request, query: { include: 'token.logoUri,token.verified' } }, TestApp.auth, ) expect(response.status).toBe(200) if (response.status !== 200) throw new Error(await response.text()) const body = await TestApp.json(response, Exchanges.schema.createQuote.Response) expect(body.status).toBe('ready') if (body.status !== 'ready') throw new Error('Expected an executable quote') if (body.mode !== 'exactSource') throw new Error('Expected an exact-source exchange quote') expect(body.destinationAmount).toEqual({ baseUnits: '1', currency: 'USD', decimals: 6, formatted: '0.000001', }) expect(body.destinationToken).toMatchObject({ address: Addresses.pathUsd, }) expect(body.destinationToken).not.toHaveProperty('amount') expect(body.destinationToken).not.toHaveProperty('minimumAmount') expect(typeof body.destinationToken.symbol).toBe('string') expect(typeof body.destinationToken.verified).toBe('boolean') expect(body.destinationAmountMin).toEqual(body.destinationAmount) expect(body.sourceAmount).toEqual({ baseUnits: '1', currency: 'USD', decimals: 6, formatted: '0.000001', }) expect(body.sourceToken).toMatchObject({ address: baseToken, name: 'Quote Base', symbol: 'QBASE', }) expect(body.sourceToken).not.toHaveProperty('amount') expect(typeof body.sourceToken.verified).toBe('boolean') expect(body.transaction.calls).toHaveLength(2) expect(body.transaction.calls[0]!.to).toBe(baseToken) expect(body.transaction.calls[1]!.to).toBe(Addresses.stablecoinDex.toLowerCase()) expect( decodeFunctionData({ abi: Abis.tip20, data: body.transaction.calls[0]!.data }), ).toEqual({ args: [Address.checksum(Addresses.stablecoinDex), 1n], functionName: 'approve', }) expect( decodeFunctionData({ abi: Abis.stablecoinDex, data: body.transaction.calls[1]!.data }), ).toEqual({ args: [Address.checksum(baseToken), Address.checksum(Addresses.pathUsd), 1n, 1n], functionName: 'swapExactAmountIn', }) }, 60_000) test('quotes exact-destination liquidity and returns approval then buy calls', async () => { // Pin an empty curated list so optional curated fields stay deterministic. const db = TestApp.database() await TestApp.verifiedSeed(db, Runtime.get().chainId, { tokens: [] }) const client = TestApp.client({ db }) const request = { account: account.address, amount: '1', destinationToken: baseToken, mode: 'exactDestination', slippageBps: 50, sourceToken: Addresses.pathUsd, } as const const response = await client.v1.exchange.quotes.$post( { json: request, query: {} }, TestApp.auth, ) expect(response.status).toBe(200) if (response.status !== 200) throw new Error(await response.text()) const body = await TestApp.json(response, Exchanges.schema.createQuote.Response) expect(body.status).toBe('ready') if (body.status !== 'ready') throw new Error('Expected an executable quote') if (body.mode !== 'exactDestination') throw new Error('Expected an exact-destination exchange quote') expect(body.destinationAmount).toEqual({ baseUnits: '1', currency: 'USD', decimals: 6, formatted: '0.000001', }) expect(body.destinationToken).toMatchObject({ address: baseToken, name: 'Quote Base', symbol: 'QBASE', }) expect(body.destinationToken).not.toHaveProperty('amount') expect(body.destinationToken).not.toHaveProperty('logoUri') expect(body.destinationToken).not.toHaveProperty('verified') expect(body.sourceAmountMax).toEqual({ baseUnits: '2', currency: 'USD', decimals: 6, formatted: '0.000002', }) expect(body.sourceAmount).toEqual(body.destinationAmount) expect(body.sourceToken).toMatchObject({ address: Addresses.pathUsd, }) expect(body.sourceToken).not.toHaveProperty('amount') expect(body.sourceToken).not.toHaveProperty('maximumAmount') expect(typeof body.sourceToken.symbol).toBe('string') expect(body.sourceToken).not.toHaveProperty('logoUri') expect(body.sourceToken).not.toHaveProperty('verified') expect(body.transaction.calls).toHaveLength(2) expect(body.transaction.calls[0]!.to).toBe(Addresses.pathUsd) expect(body.transaction.calls[1]!.to).toBe(Addresses.stablecoinDex.toLowerCase()) expect( decodeFunctionData({ abi: Abis.tip20, data: body.transaction.calls[0]!.data }), ).toEqual({ args: [Address.checksum(Addresses.stablecoinDex), 2n], functionName: 'approve', }) expect( decodeFunctionData({ abi: Abis.stablecoinDex, data: body.transaction.calls[1]!.data }), ).toEqual({ args: [Address.checksum(Addresses.pathUsd), Address.checksum(baseToken), 1n, 2n], functionName: 'swapExactAmountOut', }) }, 60_000) test('values quoted amounts in a requested denomination', async () => { // Curate only pathUSD: its leg values at the fixed 1.6 USD -> AUD rate // while the uncurated base token leg stays null. const db = TestApp.database() await TestApp.verifiedSeed(db, Runtime.get().chainId, { tokens: [ { address: Addresses.pathUsd, currency: 'USD', decimals: 6, name: 'PathUSD', symbol: 'pathUSD', }, ], }) const client = TestApp.client({ db, fx: { oracle: fixed } }) const response = await client.v1.exchange.quotes.$post( { json: { account: account.address, amount: '1000000', destinationToken: Addresses.pathUsd, mode: 'exactSource', slippageBps: 50, sourceToken: baseToken, }, query: { 'valuation.currency': 'aud' }, }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.createQuote.Response) expect(body.sourceAmount.valuation).toBeNull() expect(body.destinationAmount.valuation).toEqual({ amount: core_Value.format((BigInt(body.destinationAmount.baseUnits) * 16n) / 10n, 6), currency: 'AUD', }) expect(body.meta?.valuation).toMatchInlineSnapshot(` { "asOf": "2026-01-01T00:00:00.000Z", "basis": "nominal", "source": "fixed", } `) }, 60_000) test('returns a client error when the quote amount overflows', async () => { const response = await TestApp.client().v1.exchange.quotes.$post( { json: { account: account.address, amount: (2n ** 128n - 1n).toString(), destinationToken: baseToken, mode: 'exactSource', slippageBps: 50, sourceToken: Addresses.pathUsd, }, query: {}, }, TestApp.auth, ) expect(response.status).toBe(400) expect(await response.json()).toMatchObject({ error: { code: 'quote_amount_out_of_range' }, }) }, 60_000) test('returns quote unavailable when a route token is paused', async () => { const rpc = Relay.getClient({ account }) await Actions.token.pauseSync(rpc, { token: baseToken }) try { const response = await TestApp.client().v1.exchange.quotes.$post( { json: { account: account.address, amount: '1', destinationToken: Addresses.pathUsd, mode: 'exactSource', slippageBps: 50, sourceToken: baseToken, }, query: {}, }, TestApp.auth, ) expect(response.status).toBe(404) expect(await response.json()).toMatchObject({ error: { code: 'quote_not_available' } }) } finally { await Actions.token.unpauseSync(rpc, { token: baseToken }) } }, 60_000) }) }) describe('GET /exchange/orders', () => { type GlobalOrdersResponse = z.output test('returns a global page of resting orders across all pairs', async () => { const client = TestApp.client() const response = await client.v1.exchange.orders.$get({ query: { limit: '10' } }, TestApp.auth) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) expect(body.data.length).toBeLessThanOrEqual(10) if (body.data.length === 0) return for (const order of body.data) { expect(order.orderId).toMatch(/^\d+$/) expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/) expect(order.transactionHash).toMatch(/^0x[0-9a-f]{64}$/) expect(order.amount).toMatch(/^\d+$/) expect(order.remaining).toMatch(/^\d+$/) expect(['bid', 'ask']).toContain(order.side) expect(order.rate).toMatch(/^\d+(\.\d+)?$/) expect(order.placedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/) // Each row carries the pair it belongs to (a global page spans many pairs). expect(order.pair.key).toMatch(/^0x[0-9a-f]{64}$/) expect(order.pair.base.address).toMatch(/^0x[0-9a-f]{40}$/) expect(order.pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/) } }, 60000) test('embeds a capped total count on demand via include=totalCount', async () => { const client = TestApp.client() const response = await client.v1.exchange.orders.$get( { query: { include: 'totalCount', limit: '5' } }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) expect(body.meta).toBeDefined() expect(Number.isInteger(body.meta?.totalCount)).toBe(true) expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length) expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap) // The in-memory total is capped when the page was truncated or hit the cap. expect(body.meta!.totalCountCapped).toBe( body.meta!.totalCount >= Schema.countCap || body.truncated, ) const bare = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth) const bareBody = await TestApp.json(bare, Exchanges.schema.getOrders.Response) expect(bare.status).toBe(200) expect(bareBody.meta).toBeUndefined() }, 60000) test('paginates with an opaque cursor without skipping or duplicating rows', async () => { const client = TestApp.client() const first = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth) const firstBody = await TestApp.json(first, Exchanges.schema.getOrders.Response) if (firstBody.nextCursor === null) return const second = await client.v1.exchange.orders.$get( { query: { cursor: firstBody.nextCursor, limit: '5' } }, TestApp.auth, ) const secondBody = await TestApp.json(second, Exchanges.schema.getOrders.Response) const key = (order: GlobalOrdersResponse['data'][number]) => order.orderId const firstKeys = new Set(firstBody.data.map(key)) for (const order of secondBody.data) expect(firstKeys.has(key(order))).toBe(false) }, 60000) test('filters the global feed by maker', async () => { const client = TestApp.client() const seed = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth) const seedBody = await TestApp.json(seed, Exchanges.schema.getOrders.Response) const row = seedBody.data[0] if (!row) return const response = await client.v1.exchange.orders.$get( { query: { limit: '5', maker: row.maker } }, TestApp.auth, ) const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) for (const order of body.data) expect(order.maker).toBe(row.maker) }, 60000) test('embeds pair token metadata when requested via `include=tokens`', async () => { const client = TestApp.client() const response = await client.v1.exchange.orders.$get( { query: { include: 'tokens', limit: '10' } }, TestApp.auth, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) if (body.data.length === 0) return const enriched = body.data.find((order) => typeof order.pair.base.symbol === 'string') if (enriched) { expect(typeof enriched.pair.base.symbol).toBe('string') expect(typeof enriched.pair.base.decimals).toBe('number') } }, 60000) }) describe('GET /exchange/orders/:orderId', () => { test('rejects a non-decimal order id with 400', async () => { const client = TestApp.client() const response = await client.v1.exchange.orders[':orderId'].$get( { param: { orderId: 'not-a-number' }, query: {} }, TestApp.auth, ) expect(response.status).toBe(400) }, 30000) test('returns 404 for an unknown order id', async () => { const client = TestApp.client() // A very large id is overwhelmingly likely to not exist on testnet. The // on-chain `dex.getOrder` view reverts with `OrderDoesNotExist()` for ids // it does not hold, which the handler maps to a 404 (not a 502). const unknownOrderId = '99999999999999999999' const response = await client.v1.exchange.orders[':orderId'].$get( { param: { orderId: unknownOrderId }, query: {} }, TestApp.auth, ) // Surface a genuine RPC transport failure as a transient skip; the unknown // id itself must resolve to a clean 404. if (response.status === 502) return expect(response.status).toBe(404) }, 30000) test('returns live order state and pair for a real id', async () => { const client = TestApp.client() const sampleOrderId = await sampleOrderIdFromOrders(client) if (!sampleOrderId) return const response = await client.v1.exchange.orders[':orderId'].$get( { param: { orderId: sampleOrderId }, query: {} }, TestApp.auth, ) // The on-chain `dex.getOrder` view is a single RPC; surface 502 as a // transient-skip so the test stays green when the route's contract is // intact. if (response.status === 502) return expect(response.status).toBe(200) const order = await TestApp.json(response, Exchanges.schema.getOrder.Response) expect(order.orderId).toBe(sampleOrderId) expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/) expect(order.amount).toMatch(/^\d+$/) expect(order.remaining).toMatch(/^\d+$/) expect(['exactSource', 'exactDestination']).toContain(order.mode) expect(order.rate).toMatch(/^\d+(\.\d+)?$/) expect(order.price).toMatch(/^\d+(\.\d+)?$/) expect(order.pair.key).toMatch(/^0x[0-9a-f]{64}$/) expect(order.pair.base.address).toMatch(/^0x[0-9a-f]{40}$/) expect(order.pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/) }, 60000) test('embeds pair token metadata when requested via `include=tokens`', async () => { const client = TestApp.client() const sampleOrderId = await sampleOrderIdFromOrders(client) if (!sampleOrderId) return const response = await client.v1.exchange.orders[':orderId'].$get( { param: { orderId: sampleOrderId }, query: { include: 'tokens' } }, TestApp.auth, ) if (response.status === 502) return expect(response.status).toBe(200) const order = await TestApp.json(response, Exchanges.schema.getOrder.Response) // Both pair tokens share the same metadata path: when one resolves a // symbol, both sides of the trimmed token reference are present. if (typeof order.pair.base.symbol === 'string') { expect(typeof order.pair.base.decimals).toBe('number') expect(typeof order.pair.quote.symbol).toBe('string') } }, 60000) }) describe('GET /exchange/orders/:orderId/fills', () => { test('rejects a non-decimal order id with 400', async () => { const client = TestApp.client() const response = await client.v1.exchange.orders[':orderId'].fills.$get( { param: { orderId: 'not-a-number' }, query: {} }, TestApp.auth, ) expect(response.status).toBe(400) }, 30000) test('returns a paginated newest-first fill page for a real order id', async () => { const client = TestApp.client() const sampleOrderId = await sampleOrderIdFromSwaps(client) if (!sampleOrderId) return const response = await client.v1.exchange.orders[':orderId'].fills.$get( { param: { orderId: sampleOrderId }, query: { limit: '5' } }, TestApp.auth, ) // Treat transient indexer 502s as a skip — see order-detail test. if (response.status === 502) return expect(response.status).toBe(200) const page = await TestApp.json(response, Exchanges.schema.getOrderFills.Response) // The sample order was discovered via the swap feed, so it has at least // one fill. expect(page.data.length).toBeGreaterThan(0) for (const fill of page.data) { expect(fill.amountFilled).toMatch(/^\d+$/) expect(fill.taker).toMatch(/^0x[0-9a-f]{40}$/) expect(fill.transactionHash).toMatch(/^0x[0-9a-f]{64}$/) expect(fill.filledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/) } // Newest-first ordering. for (let i = 1; i < page.data.length; i++) { const prev = page.data[i - 1]! const curr = page.data[i]! expect( prev.blockNumber > curr.blockNumber || (prev.blockNumber === curr.blockNumber && prev.logIndex >= curr.logIndex), ).toBe(true) } // The base page does not run or embed the count. expect(page.meta).toBeUndefined() }, 60000) test('embeds an exact fill total on demand via include=totalCount', async () => { const client = TestApp.client() const sampleOrderId = await sampleOrderIdFromSwaps(client) if (!sampleOrderId) return const response = await client.v1.exchange.orders[':orderId'].fills.$get( { param: { orderId: sampleOrderId }, query: { include: 'totalCount', limit: '5' } }, TestApp.auth, ) if (response.status === 502) return expect(response.status).toBe(200) const page = await TestApp.json(response, Exchanges.schema.getOrderFills.Response) expect(response.headers.get('server-timing')?.includes('order_fills_count;dur=')).toBe(true) expect(Number.isInteger(page.meta?.totalCount)).toBe(true) expect(page.meta!.totalCount).toBeGreaterThanOrEqual(page.data.length) // The count is sort-key-pruned on ClickHouse, so it is exact (never capped). expect(page.meta!.totalCountCapped).toBe(false) }, 60000) }) /** * Discovers a real order id from the recent global swap feed so the * order-fills test can run against live testnet data without hard-coding * ids (a swap-feed id is guaranteed to have at least one fill). Returns * `null` when the feed is empty. */ async function sampleOrderIdFromSwaps(client: ReturnType) { const response = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth) if (response.status !== 200) return null const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response) return body.data[0]?.fills[0]?.orderId ?? null } /** * Discovers a live resting-order id from the global orders feed for the * order-detail tests. Unlike the swap feed — which (since the point-in-time * state rework) also surfaces fills on flip-only orders and on books missing * from `dex_pairs` — the orders feed only lists ids the order-detail * endpoint can resolve. Returns `null` when the book is empty. */ async function sampleOrderIdFromOrders(client: ReturnType) { const response = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth) if (response.status !== 200) return null const body = await TestApp.json(response, Exchanges.schema.getOrders.Response) return body.data[0]?.orderId ?? null }