Skip to content

Commit 2958523

Browse files
author
Codex
committed
fix(ts-sdk): use shared transport for market comparison
1 parent 4a367d8 commit 2958523

2 files changed

Lines changed: 138 additions & 21 deletions

File tree

sdks/typescript/pmxt/router.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -477,32 +477,12 @@ export class Router extends Exchange {
477477
if (params.url) query.url = params.url;
478478

479479
try {
480-
const url = `${this.config.basePath}/api/${this.exchangeName}/compareMarketPrices`;
481-
const response = await fetch(url, {
482-
method: 'POST',
483-
headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
484-
body: JSON.stringify({ args: [query], credentials: this.getCredentials() }),
485-
signal: AbortSignal.timeout(30_000),
486-
});
487-
if (!response.ok) {
488-
const body = await response.json().catch(() => ({}));
489-
if (body.error && typeof body.error === 'object') {
490-
const { fromServerError } = await import('./errors.js');
491-
throw fromServerError(body.error);
492-
}
493-
throw new Error(body.error?.message || response.statusText);
494-
}
495-
const json = await response.json();
480+
const json = await this.sidecarReadRequest('compareMarketPrices', query, [query]);
496481
const data = this.handleResponse(json);
497482
if (!data) return [];
498483
return (data as any[]).map((r) => {
499484
const marketPayload = r.market || {};
500485
const market = convertMarket(marketPayload);
501-
// Hosted /api/router response carries the live bid/ask on the
502-
// nested market (and the venue via market.sourceExchange). The
503-
// top-level bestBid/bestAsk/venue fields are legacy and often
504-
// null on the wire, so prefer the market fields and fall back
505-
// to the top-level only when needed.
506486
const bestBid =
507487
r.bestBid ?? (market as any).bestBid ?? marketPayload.bestBid ?? null;
508488
const bestAsk =
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { Router } from '../pmxt/router';
2+
3+
interface CapturedFetch {
4+
url: string;
5+
init?: RequestInit;
6+
}
7+
8+
function installFetchSpy(
9+
handler: (req: CapturedFetch, callIndex: number) => Promise<Response> | Response,
10+
): jest.SpyInstance {
11+
const captured: CapturedFetch[] = [];
12+
const spy = jest.spyOn(global, 'fetch').mockImplementation(async (input, init) => {
13+
const url = typeof input === 'string' ? input : (input as URL | Request).toString();
14+
const req: CapturedFetch = { url, init };
15+
captured.push(req);
16+
return handler(req, captured.length);
17+
});
18+
(spy as unknown as { captured: CapturedFetch[] }).captured = captured;
19+
return spy;
20+
}
21+
22+
function captured(spy: jest.SpyInstance): CapturedFetch[] {
23+
return (spy as unknown as { captured: CapturedFetch[] }).captured;
24+
}
25+
26+
function jsonResponse(payload: unknown, status = 200): Response {
27+
return new Response(JSON.stringify(payload), {
28+
status,
29+
headers: { 'Content-Type': 'application/json' },
30+
});
31+
}
32+
33+
class DynamicResolveBaseRouter extends Router {
34+
public baseUrls: string[] = [];
35+
private _baseCallCount = 0;
36+
37+
public override resolveBaseUrl(): string {
38+
return this.baseUrls[this._baseCallCount++]
39+
?? this.baseUrls[this.baseUrls.length - 1]
40+
?? super.resolveBaseUrl();
41+
}
42+
}
43+
44+
afterEach(() => {
45+
jest.restoreAllMocks();
46+
});
47+
48+
describe('Router.compareMarketPrices transport contracts', () => {
49+
it('resolves base URL per invocation before dispatching sidecar reads', async () => {
50+
const spy = installFetchSpy(() => jsonResponse({ success: true, data: [] }));
51+
const router = new DynamicResolveBaseRouter({ autoStartServer: false });
52+
router.baseUrls = ['http://localhost:4011', 'http://localhost:4012'];
53+
54+
await router.compareMarketPrices({ marketId: 'mkt-1' });
55+
await router.compareMarketPrices({ marketId: 'mkt-1' });
56+
57+
const reqs = captured(spy);
58+
expect(reqs).toHaveLength(2);
59+
expect(reqs[0].url).toBe('http://localhost:4011/api/router/compareMarketPrices?marketId=mkt-1');
60+
expect(reqs[0].init?.method).toBe('GET');
61+
expect(reqs[1].url).toBe('http://localhost:4012/api/router/compareMarketPrices?marketId=mkt-1');
62+
expect(reqs[1].init?.method).toBe('GET');
63+
});
64+
65+
it('retries once after a connection error before returning a result', async () => {
66+
const router = new DynamicResolveBaseRouter({ autoStartServer: false });
67+
router.baseUrls = ['http://localhost:4013'];
68+
const ensureServer = jest
69+
.spyOn((router as any).serverManager, 'ensureServerRunning')
70+
.mockResolvedValue(undefined);
71+
72+
let calls = 0;
73+
const spy = installFetchSpy(() => {
74+
calls += 1;
75+
if (calls === 1) {
76+
throw new Error('ECONNREFUSED');
77+
}
78+
return jsonResponse({ success: true, data: [] });
79+
});
80+
81+
await router.compareMarketPrices({ marketId: 'mkt-2' });
82+
83+
const reqs = captured(spy);
84+
expect(reqs).toHaveLength(2);
85+
expect(reqs[0].init?.method).toBe('GET');
86+
expect(reqs[1].init?.method).toBe('GET');
87+
expect(ensureServer).toHaveBeenCalledTimes(1);
88+
});
89+
90+
it('falls back to POST on 405 with exact args payload and preserves nested market mapping', async () => {
91+
const router = new DynamicResolveBaseRouter({ autoStartServer: false });
92+
router.baseUrls = ['http://localhost:4014'];
93+
94+
const spy = installFetchSpy((_req, callIndex) => {
95+
if (callIndex === 1) {
96+
return jsonResponse({ success: false, error: 'GET unsupported' }, 405);
97+
}
98+
return jsonResponse({
99+
success: true,
100+
data: [
101+
{
102+
market: {
103+
marketId: 'mkt-3',
104+
sourceExchange: 'polymarket',
105+
bestBid: 0.24,
106+
bestAsk: 0.76,
107+
},
108+
confidence: 0.91,
109+
relation: 'identity',
110+
reasoning: 'identity match',
111+
},
112+
],
113+
});
114+
});
115+
116+
const out = await router.compareMarketPrices({ marketId: 'mkt-3' });
117+
const reqs = captured(spy);
118+
119+
expect(reqs).toHaveLength(2);
120+
expect(reqs[0].url).toBe('http://localhost:4014/api/router/compareMarketPrices?marketId=mkt-3');
121+
expect(reqs[0].init?.method).toBe('GET');
122+
expect(reqs[1].url).toBe('http://localhost:4014/api/router/compareMarketPrices');
123+
expect(reqs[1].init?.method).toBe('POST');
124+
const body = JSON.parse((reqs[1].init?.body as string) ?? '{}');
125+
expect(body).toEqual({ args: [{ marketId: 'mkt-3' }] });
126+
127+
expect(out).toHaveLength(1);
128+
const row = out[0] as any;
129+
expect(row.market.sourceExchange).toBe('polymarket');
130+
expect(row.bestBid).toBe(0.24);
131+
expect(row.bestAsk).toBe(0.76);
132+
expect(row.venue).toBe('polymarket');
133+
expect(row.relation).toBe('identity');
134+
expect(row.confidence).toBe(0.91);
135+
expect(row.reasoning).toBe('identity match');
136+
});
137+
});

0 commit comments

Comments
 (0)