From 8f8f6dee4595b7f909cdfd98afaa8b493621b559 Mon Sep 17 00:00:00 2001 From: Purin1410 Date: Wed, 12 Aug 2026 02:24:31 +0700 Subject: [PATCH] fix(ts-sdk): use shared transport for market comparison --- sdks/typescript/pmxt/router.ts | 22 +-- .../tests/router-compareMarketPrices.test.ts | 137 ++++++++++++++++++ 2 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 sdks/typescript/tests/router-compareMarketPrices.test.ts diff --git a/sdks/typescript/pmxt/router.ts b/sdks/typescript/pmxt/router.ts index 7cc9ca3d..835c3395 100644 --- a/sdks/typescript/pmxt/router.ts +++ b/sdks/typescript/pmxt/router.ts @@ -477,32 +477,12 @@ export class Router extends Exchange { if (params.url) query.url = params.url; try { - const url = `${this.config.basePath}/api/${this.exchangeName}/compareMarketPrices`; - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args: [query], credentials: this.getCredentials() }), - signal: AbortSignal.timeout(30_000), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === 'object') { - const { fromServerError } = await import('./errors.js'); - throw fromServerError(body.error); - } - throw new Error(body.error?.message || response.statusText); - } - const json = await response.json(); + const json = await this.sidecarReadRequest('compareMarketPrices', query, [query]); const data = this.handleResponse(json); if (!data) return []; return (data as any[]).map((r) => { const marketPayload = r.market || {}; const market = convertMarket(marketPayload); - // Hosted /api/router response carries the live bid/ask on the - // nested market (and the venue via market.sourceExchange). The - // top-level bestBid/bestAsk/venue fields are legacy and often - // null on the wire, so prefer the market fields and fall back - // to the top-level only when needed. const bestBid = r.bestBid ?? (market as any).bestBid ?? marketPayload.bestBid ?? null; const bestAsk = diff --git a/sdks/typescript/tests/router-compareMarketPrices.test.ts b/sdks/typescript/tests/router-compareMarketPrices.test.ts new file mode 100644 index 00000000..260bc343 --- /dev/null +++ b/sdks/typescript/tests/router-compareMarketPrices.test.ts @@ -0,0 +1,137 @@ +import { Router } from '../pmxt/router'; + +interface CapturedFetch { + url: string; + init?: RequestInit; +} + +function installFetchSpy( + handler: (req: CapturedFetch, callIndex: number) => Promise | Response, +): jest.SpyInstance { + const captured: CapturedFetch[] = []; + const spy = jest.spyOn(global, 'fetch').mockImplementation(async (input, init) => { + const url = typeof input === 'string' ? input : (input as URL | Request).toString(); + const req: CapturedFetch = { url, init }; + captured.push(req); + return handler(req, captured.length); + }); + (spy as unknown as { captured: CapturedFetch[] }).captured = captured; + return spy; +} + +function captured(spy: jest.SpyInstance): CapturedFetch[] { + return (spy as unknown as { captured: CapturedFetch[] }).captured; +} + +function jsonResponse(payload: unknown, status = 200): Response { + return new Response(JSON.stringify(payload), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +class DynamicResolveBaseRouter extends Router { + public baseUrls: string[] = []; + private _baseCallCount = 0; + + public override resolveBaseUrl(): string { + return this.baseUrls[this._baseCallCount++] + ?? this.baseUrls[this.baseUrls.length - 1] + ?? super.resolveBaseUrl(); + } +} + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('Router.compareMarketPrices transport contracts', () => { + it('resolves base URL per invocation before dispatching sidecar reads', async () => { + const spy = installFetchSpy(() => jsonResponse({ success: true, data: [] })); + const router = new DynamicResolveBaseRouter({ autoStartServer: false }); + router.baseUrls = ['http://localhost:4011', 'http://localhost:4012']; + + await router.compareMarketPrices({ marketId: 'mkt-1' }); + await router.compareMarketPrices({ marketId: 'mkt-1' }); + + const reqs = captured(spy); + expect(reqs).toHaveLength(2); + expect(reqs[0].url).toBe('http://localhost:4011/api/router/compareMarketPrices?marketId=mkt-1'); + expect(reqs[0].init?.method).toBe('GET'); + expect(reqs[1].url).toBe('http://localhost:4012/api/router/compareMarketPrices?marketId=mkt-1'); + expect(reqs[1].init?.method).toBe('GET'); + }); + + it('retries once after a connection error before returning a result', async () => { + const router = new DynamicResolveBaseRouter({ autoStartServer: false }); + router.baseUrls = ['http://localhost:4013']; + const ensureServer = jest + .spyOn((router as any).serverManager, 'ensureServerRunning') + .mockResolvedValue(undefined); + + let calls = 0; + const spy = installFetchSpy(() => { + calls += 1; + if (calls === 1) { + throw new Error('ECONNREFUSED'); + } + return jsonResponse({ success: true, data: [] }); + }); + + await router.compareMarketPrices({ marketId: 'mkt-2' }); + + const reqs = captured(spy); + expect(reqs).toHaveLength(2); + expect(reqs[0].init?.method).toBe('GET'); + expect(reqs[1].init?.method).toBe('GET'); + expect(ensureServer).toHaveBeenCalledTimes(1); + }); + + it('falls back to POST on 405 with exact args payload and preserves nested market mapping', async () => { + const router = new DynamicResolveBaseRouter({ autoStartServer: false }); + router.baseUrls = ['http://localhost:4014']; + + const spy = installFetchSpy((_req, callIndex) => { + if (callIndex === 1) { + return jsonResponse({ success: false, error: 'GET unsupported' }, 405); + } + return jsonResponse({ + success: true, + data: [ + { + market: { + marketId: 'mkt-3', + sourceExchange: 'polymarket', + bestBid: 0.24, + bestAsk: 0.76, + }, + confidence: 0.91, + relation: 'identity', + reasoning: 'identity match', + }, + ], + }); + }); + + const out = await router.compareMarketPrices({ marketId: 'mkt-3' }); + const reqs = captured(spy); + + expect(reqs).toHaveLength(2); + expect(reqs[0].url).toBe('http://localhost:4014/api/router/compareMarketPrices?marketId=mkt-3'); + expect(reqs[0].init?.method).toBe('GET'); + expect(reqs[1].url).toBe('http://localhost:4014/api/router/compareMarketPrices'); + expect(reqs[1].init?.method).toBe('POST'); + const body = JSON.parse((reqs[1].init?.body as string) ?? '{}'); + expect(body).toEqual({ args: [{ marketId: 'mkt-3' }] }); + + expect(out).toHaveLength(1); + const row = out[0] as any; + expect(row.market.sourceExchange).toBe('polymarket'); + expect(row.bestBid).toBe(0.24); + expect(row.bestAsk).toBe(0.76); + expect(row.venue).toBe('polymarket'); + expect(row.relation).toBe('identity'); + expect(row.confidence).toBe(0.91); + expect(row.reasoning).toBe('identity match'); + }); +});