diff --git a/core/specs/polymarket/PolymarketClobAPI.yaml b/core/specs/polymarket/PolymarketClobAPI.yaml index f9d0558b..3c677fb1 100644 --- a/core/specs/polymarket/PolymarketClobAPI.yaml +++ b/core/specs/polymarket/PolymarketClobAPI.yaml @@ -954,6 +954,7 @@ paths: # ---------------------------------------------------------------------------- /geoblock: get: + operationId: getGeoblock summary: Check Geoblock Status description: Check the geographic eligibility of the requesting IP address. tags: [System] diff --git a/core/src/BaseExchange.ts b/core/src/BaseExchange.ts index b12b095a..b5800673 100644 --- a/core/src/BaseExchange.ts +++ b/core/src/BaseExchange.ts @@ -31,6 +31,11 @@ export interface ApiEndpoint { isPrivate?: boolean; /** Identifier used to generate the implicit API method name. */ operationId?: string; + /** + * When set, requests use this base URL instead of the descriptor default + * (OpenAPI path- or operation-level `servers` override). + */ + baseUrl?: string; } export interface ApiDescriptor { @@ -1229,7 +1234,11 @@ export abstract class PredictionMarketExchange { if (name in this) { continue; } - (this as any)[name] = this.createImplicitMethod(name, endpoint, descriptor.baseUrl); + (this as any)[name] = this.createImplicitMethod( + name, + endpoint, + endpoint.baseUrl ?? descriptor.baseUrl + ); } } @@ -1255,7 +1264,7 @@ export abstract class PredictionMarketExchange { private createImplicitMethod( name: string, endpoint: ApiEndpoint, - baseUrl: string + resolvedBaseUrl: string ): (params?: Record) => Promise { return async (params?: Record): Promise => { const allParams = { ...(params || {}) }; @@ -1279,7 +1288,7 @@ export abstract class PredictionMarketExchange { headers = this.sign(endpoint.method, resolvedPath, allParams); } - const url = `${baseUrl}${resolvedPath}`; + const url = `${resolvedBaseUrl}${resolvedPath}`; const method = endpoint.method.toUpperCase(); try { diff --git a/core/src/exchanges/polymarket/api-clob.ts b/core/src/exchanges/polymarket/api-clob.ts index dd8443c3..17c2298c 100644 --- a/core/src/exchanges/polymarket/api-clob.ts +++ b/core/src/exchanges/polymarket/api-clob.ts @@ -499,6 +499,7 @@ export const polymarketClobSpec = { }, "/geoblock": { "get": { + "operationId": "getGeoblock", "summary": "Check Geoblock Status", "tags": [ "System" diff --git a/core/src/utils/openapi.ts b/core/src/utils/openapi.ts index f3c48d8a..d65d04d2 100644 --- a/core/src/utils/openapi.ts +++ b/core/src/utils/openapi.ts @@ -42,12 +42,27 @@ export function parseOpenApiSpec(spec: any, baseUrl?: string): ApiDescriptor { const topLevelSecurity = !!(spec.security && spec.security.length > 0); const paths = spec.paths || {}; - for (const [path, methods] of Object.entries(paths)) { - for (const [httpMethod, operation] of Object.entries(methods)) { - // Skip non-HTTP-method keys like "parameters" + for (const [path, pathItem] of Object.entries(paths)) { + if (!pathItem || typeof pathItem !== 'object') { + continue; + } + const pathServerUrl = Array.isArray(pathItem.servers) && pathItem.servers[0]?.url + ? String(pathItem.servers[0].url).replace(/\/$/, '') + : undefined; + + for (const [httpMethod, operation] of Object.entries(pathItem)) { + // Skip non-HTTP-method keys like "parameters", "servers" if (!['get', 'post', 'put', 'patch', 'delete'].includes(httpMethod.toLowerCase())) { continue; } + if (!operation || typeof operation !== 'object') { + continue; + } + + const opServerUrl = Array.isArray(operation.servers) && operation.servers[0]?.url + ? String(operation.servers[0].url).replace(/\/$/, '') + : undefined; + const endpointBaseUrl = opServerUrl || pathServerUrl; const name = operation.operationId || generateMethodName(httpMethod, path); const isPrivate = operation.security !== undefined @@ -59,6 +74,7 @@ export function parseOpenApiSpec(spec: any, baseUrl?: string): ApiDescriptor { path, isPrivate, operationId: operation.operationId, + ...(endpointBaseUrl ? { baseUrl: endpointBaseUrl } : {}), }; } } diff --git a/core/test/unit/openapi.test.ts b/core/test/unit/openapi.test.ts new file mode 100644 index 00000000..5bf004c8 --- /dev/null +++ b/core/test/unit/openapi.test.ts @@ -0,0 +1,45 @@ +import { polymarketClobSpec } from '../../src/exchanges/polymarket/api-clob'; +import { parseOpenApiSpec } from '../../src/utils/openapi'; + +describe('parseOpenApiSpec', () => { + it('Polymarket getGeoblock uses website API host (not CLOB)', () => { + const d = parseOpenApiSpec(polymarketClobSpec); + expect(d.endpoints.getGeoblock.baseUrl).toBe('https://polymarket.com/api'); + expect(d.endpoints.getGeoblock.path).toBe('/geoblock'); + }); + + it('uses operation-level servers as endpoint baseUrl override', () => { + const spec = { + openapi: '3.0.3', + servers: [{ url: 'https://primary.example.com' }], + paths: { + '/x': { + get: { + operationId: 'getX', + servers: [{ url: 'https://override.example.com/' }], + }, + }, + }, + }; + + const d = parseOpenApiSpec(spec); + expect(d.baseUrl).toBe('https://primary.example.com'); + expect(d.endpoints.getX.baseUrl).toBe('https://override.example.com'); + }); + + it('inherits path-level servers when the operation omits servers', () => { + const spec = { + openapi: '3.0.3', + servers: [{ url: 'https://primary.example.com' }], + paths: { + '/y': { + servers: [{ url: 'https://path-level.example.com' }], + get: { operationId: 'getY' }, + }, + }, + }; + + const d = parseOpenApiSpec(spec); + expect(d.endpoints.getY.baseUrl).toBe('https://path-level.example.com'); + }); +});