From 7a54c84e280feccd092125c05c93e54b74c406b1 Mon Sep 17 00:00:00 2001 From: AbhilashG12 Date: Thu, 9 Jul 2026 18:39:51 +0530 Subject: [PATCH] feat(router): add SQL query support to both SDKs - Add sql(query: string) method to Router class - Add SqlResult, SqlMeta, SqlColumn types - Export types from both SDKs Fixes #1032 --- sdks/python/pmxt/__init__.py | Bin 6477 -> 13502 bytes sdks/python/pmxt/router.py | Bin 24450 -> 50208 bytes sdks/typescript/pmxt/client.ts | 7201 ++++++++++++++++---------------- sdks/typescript/pmxt/router.ts | Bin 24208 -> 54138 bytes 4 files changed, 3602 insertions(+), 3599 deletions(-) diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index fe8cb55471b86c7afd10257185e25320e26cb9d3..913475741eaea4044e00ae15a8cfb7e69871c301 100644 GIT binary patch literal 13502 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 DU&7@H diff --git a/sdks/python/pmxt/router.py b/sdks/python/pmxt/router.py index c070a162c0af56ae2f5132dd01a7e0df707f7869..d419fd4b5679fe907dff95b30f4ad8127dd53f64 100644 GIT binary patch literal 50208 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 ID3=HS2B@f1Qvd(} diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index 057cf1e3..b0fb9e1e 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -1,3599 +1,3602 @@ -/** - * Exchange client implementations. - * - * This module provides clean, TypeScript-friendly wrappers around the auto-generated - * OpenAPI client, matching the Python API exactly. - */ - -import { - Configuration, - CreateOrderRequest, - DefaultApi, - ExchangeCredentials, - BuildOrderRequest, - SubmitOrderRequest, -} from "../generated/src/index.js"; - -import { - Balance, - BuiltOrder, - CreateOrderInput, - EventFetchParams, - EventFilterCriteria, - EventFilterFunction, - ExecutionPriceResult, - FetchOrderBookParams, - MarketFetchParams, - MarketFilterCriteria, - MarketFilterFunction, - MarketList, - MarketOutcome, - MyTradesParams, - Order, - OrderBook, - OrderHistoryParams, - OrderLevel, - PaginatedMarketsResult, - PaginatedEventsResult, - Position, - SeriesFetchParams, - PriceCandle, - SubscribedAddressSnapshot, - SubscriptionOption, - Trade, - UnifiedEvent, - UnifiedMarket, - UnifiedSeries, - UserTrade, - FirehoseEvent, - FetchMatchedMarketClustersParams, -} from "./models.js"; - -import { ServerManager } from "./server-manager.js"; -import { buildArgsWithOptionalOptions } from "./args.js"; -import { PmxtError, fromServerError, InvalidOrder, NotSupported } from "./errors.js"; -import { ENV, LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js"; -import { SidecarWsClient } from "./ws-client.js"; -import { logger } from "./logger.js"; - -// Hosted-mode trading dispatch. -// These modules are introduced as part of the hosted trading mode rollout. -// Some of them may be authored by parallel agents; until they all land, the -// import names below are the conventional ones from the plan. Cross-module -// "Cannot find module" errors during the parallel landing window resolve -// once the matching files exist. -import { - HOSTED_TRADING_VENUES, - _tradingRequest, - resolveWalletAddress, - ensureHostedTradingSupported, - formatRoutePath, - HOSTED_METHOD_ROUTES, -} from "./hosted-routing.js"; -import { - orderFromV0, - positionFromV0, - balanceFromV0, - userTradeFromV0, - to6dec, -} from "./hosted-mappers.js"; -import { - validateTypedData, - validateEconomics, - verifySignature, -} from "./hosted-typed-data.js"; -import type { HostedRoute } from "./hosted-typed-data.js"; -import type { Signer, TypedData } from "./signers.js"; -import { signerFromPrivateKey, EthersSigner } from "./signers.js"; -import { Escrow } from "./escrow.js"; -import { - MissingWalletAddress, - InvalidSignature as HostedInvalidSignature, -} from "./hosted-errors.js"; - -interface RawWebSocketLike { - send(data: string): void; -} - -interface SidecarWsClientInternals { - ensureConnected(): Promise; - ws: RawWebSocketLike | null; - activeSubs: Map; - subscriptions: Map void) | null }>; - dataQueues: Map; - dataStore: Map; -} - -/** - * Resolve a MarketOutcome shorthand to a plain outcome ID string. - * Accepts either a raw string ID or a MarketOutcome object. - */ -function resolveOutcomeId(input: string | MarketOutcome): string { - if (typeof input === 'string') return input; - return input.outcomeId; -} - -/** - * Build a URL-encoded query string from a plain record. - * - * - `undefined` / `null` values are skipped (they shouldn't appear in the URL). - * - Arrays are serialised as repeated `key=v1&key=v2` pairs. - * - Nested objects are skipped here; callers should route such queries through - * POST instead (see `queryHasNestedObject`). - */ -function buildSidecarQueryString(query: Record): string { - const parts: string[] = []; - for (const [key, value] of Object.entries(query)) { - if (value === undefined || value === null) continue; - if (Array.isArray(value)) { - for (const v of value) { - if (v === undefined || v === null) continue; - parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`); - } - } else if (typeof value === 'object') { - // Nested objects don't round-trip through query strings. Caller - // should have detected this and POSTed instead. - continue; - } else { - parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - } - } - return parts.join('&'); -} - -/** - * True if any top-level value in the query is a nested object (not an array). - * Such queries can't be safely expressed in a query string, so we fall back - * to POST to preserve the original argument shape. - */ -function queryHasNestedObject(query: Record): boolean { - for (const value of Object.values(query)) { - if (value === undefined || value === null) continue; - if (typeof value === 'object' && !Array.isArray(value)) return true; - } - return false; -} - -// Converter functions -function convertMarket(raw: any): UnifiedMarket { - const market: UnifiedMarket = { - ...raw, - resolutionDate: raw.resolutionDate ? new Date(raw.resolutionDate) : undefined, - outcomes: (raw.outcomes || []).map((o: any) => ({ ...o })), - yes: raw.yes ? { ...raw.yes } : undefined, - no: raw.no ? { ...raw.no } : undefined, - up: raw.up ? { ...raw.up } : undefined, - down: raw.down ? { ...raw.down } : undefined, - }; - Object.defineProperty(market, 'question', { - get() { return this.title; }, - enumerable: false, - configurable: true, - }); - return market; -} - - -function convertCandle(raw: any): PriceCandle { - return { ...raw }; -} - -function convertOrderBook(raw: any): OrderBook { - return { - ...raw, - bids: (raw.bids || []).map((b: any) => ({ ...b })), - asks: (raw.asks || []).map((a: any) => ({ ...a })), - }; -} - -function convertTrade(raw: any): Trade { - return { ...raw, side: raw.side || "unknown" }; -} - -function convertOrder(raw: any): Order { - return { ...raw }; -} - -function convertPosition(raw: any): Position { - return { ...raw }; -} - -function convertBalance(raw: any): Balance { - return { ...raw }; -} - -function convertUserTrade(raw: any): UserTrade { - return { ...raw, side: raw.side || "unknown" }; -} - -function convertEvent(raw: any): UnifiedEvent { - const markets = MarketList.from((raw.markets || []).map(convertMarket)) as MarketList; - return { ...raw, markets }; -} - -function convertSeries(raw: any): UnifiedSeries { - const events = Array.isArray(raw.events) ? raw.events.map(convertEvent) : undefined; - return { ...raw, ...(events !== undefined ? { events } : {}) }; -} - -function convertSubscriptionSnapshot(raw: any): SubscribedAddressSnapshot { - return { - ...raw, - trades: (raw.trades ?? []).map(convertTrade), - balances: (raw.balances ?? []).map(convertBalance), - positions: (raw.positions ?? []).map(convertPosition), - }; -} - -/** - * Base exchange client options. - */ -export interface ExchangeOptions { - /** Venue-specific API key (e.g. Polymarket CLOB key). Optional. */ - apiKey?: string; - - /** Venue-specific API secret for HMAC/CLOB-authenticated exchanges. Optional. */ - apiSecret?: string; - - /** Venue-specific private key. Optional. */ - privateKey?: string; - - /** - * Hosted pmxt API key. - * - * When set (either as this kwarg or via the `PMXT_API_KEY` env - * variable), and no explicit `baseUrl` / `PMXT_BASE_URL` is set, - * the Exchange will default to the hosted pmxt endpoint - * (`https://api.pmxt.dev`) instead of the local sidecar, and send - * `Authorization: Bearer ` on every request. - * - * The local sidecar ignores this header, so it is safe to set in - * both local and hosted modes. - */ - pmxtApiKey?: string; - - /** - * Base URL of the pmxt server. - * - * Resolution precedence: - * 1. Explicit `baseUrl` kwarg. - * 2. `PMXT_BASE_URL` environment variable. - * 3. `HOSTED_URL` when `pmxtApiKey` (kwarg or env) is present. - * 4. Local sidecar (`http://localhost:3847`). - */ - baseUrl?: string; - - /** - * Automatically start the local sidecar if it is not running. - * - * Default: `true` when the resolved base URL is the local sidecar, - * `false` otherwise. Explicit `true` / `false` always wins. - */ - autoStartServer?: boolean; - - /** Optional Polymarket Proxy/Smart Wallet address */ - proxyAddress?: string; - - /** Optional signature type (0=EOA, 1=Proxy) */ - signatureType?: number; - - /** - * EVM wallet address used for hosted reads/writes. Required for hosted - * endpoints that operate on a wallet (balances, positions, trades, open - * orders). When omitted, hosted reads raise {@link MissingWalletAddress}. - */ - walletAddress?: string; - - /** - * External signer used for hosted writes. When `privateKey` is supplied - * without `signer` in hosted mode, an internal {@link EthersSigner} is - * built from it lazily. - */ - signer?: Signer; -} - -/** - * Base class for prediction market exchanges. - * - * This provides a unified interface for interacting with different - * prediction market platforms (Polymarket, Kalshi, etc.). - */ -export abstract class Exchange { - private static readonly OBDATA_WATCH_ALL_SOURCES = new Set([ - "polymarket", - "limitless", - "kalshi", - "opinion", - ]); - - // Match a canonical 8-4-4-4-12 UUID string. The hosted catalog emits - // UUIDs in this exact shape, so a regex is both faster and stricter - // than `crypto.randomUUID()`-style parsing. - private static readonly _UUID_RE = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - - /** True iff `value` parses as a canonical catalog UUID string. */ - protected static _isCatalogUuid(value: string): boolean { - return Exchange._UUID_RE.test(value); - } - - // Public so structural interfaces like `HostedClientLike` - // (./hosted-routing) can read the venue name and hosted credentials - // without violating protected-access on this base class. - public exchangeName: string; - public pmxtApiKey?: string; - protected apiKey?: string; - protected apiSecret?: string; - protected privateKey?: string; - protected proxyAddress?: string; - protected signatureType?: number; - protected api: DefaultApi; - protected config: Configuration; - protected serverManager: ServerManager; - protected initPromise: Promise; - protected isHosted: boolean; - - /** Wallet address used for hosted endpoints that operate on a wallet. */ - public walletAddress?: string; - - /** External signer used for hosted writes. */ - public signer?: Signer; - - /** Escrow namespace — populated in hosted mode for trading-allowlisted venues. */ - public escrow?: Escrow; - private _hostedAccount?: { depositWallet?: string; signatureType?: number }; - private _accountDiscoveryPromise?: Promise; - - /** - * Sticky flag: set to `true` the first time a GET read is rejected by - * the sidecar with 404/405 (i.e. an older pmxt-core that only supports - * POST). While false, read methods try GET first; once flipped they - * POST directly and skip the GET probe for the lifetime of this client. - */ - private _getReadsUnsupported: boolean = false; - - /** Shared WebSocket client for streaming methods (lazy). */ - private _wsClient: SidecarWsClient | null = null; - /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */ - private _wsUnsupported: boolean = false; - private readonly _useSidecarLockBaseUrl: boolean; - - constructor(exchangeName: string, options: ExchangeOptions = {}) { - this.exchangeName = exchangeName.toLowerCase(); - this.apiKey = options.apiKey; - this.apiSecret = options.apiSecret; - this.privateKey = options.privateKey; - this.proxyAddress = options.proxyAddress; - this.signatureType = options.signatureType; - this.walletAddress = options.walletAddress; - this.signer = options.signer; - - // Resolve base URL + hosted API key via the shared precedence - // rules. See constants.ts for the full resolution table. - const resolved = resolvePmxtBaseUrl({ - baseUrl: options.baseUrl, - pmxtApiKey: options.pmxtApiKey, - }); - const baseUrl = resolved.baseUrl; - this.pmxtApiKey = resolved.pmxtApiKey; - this.isHosted = resolved.isHosted; - const hasBaseUrlOverride = Boolean( - options.baseUrl || - (typeof process !== "undefined" && process.env[ENV.BASE_URL]), - ); - this._useSidecarLockBaseUrl = !this.isHosted && !hasBaseUrlOverride; - - // Hosted trading bridge: if the caller passed a privateKey but no - // explicit signer, lazily wrap it in an EthersSigner so that - // `pmxt.Polymarket(pmxtApiKey, privateKey)` just works without the - // user touching `signer`. EthersSigner's constructor synchronously - // builds an ethers.Wallet, so this remains a synchronous bridge — - // the constructor stays sync. - if (this.pmxtApiKey && this.privateKey && !this.signer) { - try { - this.signer = new EthersSigner(this.privateKey); - } catch { - // ethers not installed — defer the error to the first - // hosted write that actually needs the signer. Read-only - // hosted callers don't need ethers. - } - } - - // Instantiate Escrow namespace for hosted-trading-allowlisted venues. - if (this.pmxtApiKey && HOSTED_TRADING_VENUES.has(this.exchangeName)) { - this.escrow = new Escrow(this); - } - - // auto_start_server defaults: true for local, false for hosted. - // An explicit value in the options always wins. - const autoStartServer = options.autoStartServer !== undefined - ? options.autoStartServer - : !this.isHosted; - - // Initialize server manager (no network calls happen here — the - // constructor just stores config). - this.serverManager = new ServerManager({ baseUrl }); - - // Configure the API client with the initial base URL (will be - // updated to the actual listen port if the local sidecar gets - // bumped off the default). - this.config = new Configuration({ basePath: baseUrl }); - this.api = new DefaultApi(this.config); - - // Initialize the server connection asynchronously - this.initPromise = this.initializeServer(autoStartServer); - } - - private async initializeServer(autoStartServer: boolean): Promise { - if (autoStartServer) { - try { - await this.serverManager.ensureServerRunning(); - - if (this._useSidecarLockBaseUrl) { - // Get the actual port the server is running on - // (may differ from default if default port was busy) - const actualPort = this.serverManager.getRunningPort(); - const newBaseUrl = `http://localhost:${actualPort}`; - - // Update API client with actual base URL - this.config = new Configuration({ - basePath: newBaseUrl, - }); - this.api = new DefaultApi(this.config); - } - } catch (error) { - const msg = - `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` + - `Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n` + - `Or start the server manually: pmxt-server`; - const pmxtError = new PmxtError(msg); - if (error instanceof Error) { - (pmxtError as any).cause = error; - } - throw pmxtError; - } - } - } - - protected handleResponse(response: any): any { - if (!response.success) { - const error = response.error || {}; - if (error && typeof error === "object" && (error.code || error.message)) { - throw fromServerError(error); - } - throw new PmxtError(error.message || "Unknown error"); - } - return response.data; - } - - protected getCredentials(): ExchangeCredentials | undefined { - if (!this.apiKey && !this.apiSecret && !this.privateKey) { - return undefined; - } - return { - apiKey: this.apiKey, - apiSecret: this.apiSecret, - privateKey: this.privateKey, - funderAddress: this.proxyAddress, - signatureType: this.signatureType, - }; - } - - protected getAuthHeaders(): Record { - const headers: Record = { ...(this.config.headers as Record) }; - - // Local sidecar access token (read from the lock file). Only - // meaningful when talking to a local sidecar we spawned - // ourselves; harmless elsewhere. - const accessToken = this.serverManager.getAccessToken(); - if (accessToken) { - headers['x-pmxt-access-token'] = accessToken; - } - - // Hosted pmxt bearer token. The hosted service requires this; - // the local sidecar ignores it. Safe to attach unconditionally - // whenever a pmxtApiKey has been resolved. - if (this.pmxtApiKey) { - headers['Authorization'] = `Bearer ${this.pmxtApiKey}`; - } - - return headers; - } - - /** - * Returns true when this client should dispatch trading methods through - * the hosted PMXT trading API (`pmxtApiKey` set AND venue is on the - * hosted-trading allowlist). Used to gate every Group A method. - */ - protected isHostedTradingMode(): boolean { - return Boolean(this.pmxtApiKey) && HOSTED_TRADING_VENUES.has(this.exchangeName); - } - - /** - * Require a configured signer for a hosted write. Returns the signer or - * throws {@link MissingWalletAddress} (consistent with the error class - * surfaced for missing wallet wiring on hosted writes). - */ - protected requireHostedSigner(): Signer { - if (!this.signer) { - throw new MissingWalletAddress( - "hosted write requires a signer (pass `signer` or `privateKey`)", - ); - } - return this.signer; - } - - /** - * Resolve the current sidecar base URL. - * - * Hosted mode and explicit baseUrl clients keep the configured basePath. - * Default local clients re-read the lock-file port on every call so they - * pick up sidecar restarts that land on a different port. - */ - private resolveBaseUrl(): string { - if (this.isHosted || !this._useSidecarLockBaseUrl) return this.config.basePath; - const port = this.serverManager.getRunningPort(); - return `http://localhost:${port}`; - } - - /** - * Execute a fetch with retry on connection failures. - * - * Only retries on connection-level errors (ECONNREFUSED, ECONNRESET) — - * never on HTTP responses (4xx, 5xx). On first connection failure, - * attempts to restart the sidecar. - */ - private async fetchWithRetry( - input: RequestInfo | URL, - init?: RequestInit, - ): Promise { - const delays = [200, 500, 1000]; - let lastError: unknown; - - for (let attempt = 0; attempt <= delays.length; attempt++) { - try { - return await fetch(input, { - ...init, - signal: AbortSignal.timeout(30_000), - }); - } catch (error) { - lastError = error; - if (attempt >= delays.length) break; - - // Connection failed — try to restart the sidecar on first failure - if (attempt === 0 && !this.isHosted) { - try { - await this.serverManager.ensureServerRunning(); - } catch (err) { - logger.warn('PmxtClient: server restart failed during retry', { attempt, error: String(err) }); - } - } - await new Promise(resolve => setTimeout(resolve, delays[attempt])); - } - } - throw lastError; - } - - /** - * Return the shared WebSocket client, creating it on first use. - * - * Returns `null` if the sidecar /ws endpoint was previously found - * to be unavailable. - */ - private async getOrCreateWs(): Promise { - if (this._wsUnsupported) return null; - if (this._wsClient?.connected) return this._wsClient; - - const host = this.resolveBaseUrl(); - const accessToken = this.isHosted - ? this.pmxtApiKey - : this.serverManager.getAccessToken(); - const authParamName = this.isHosted ? "apiKey" : "token"; - - const client = new SidecarWsClient(host, accessToken || undefined, authParamName); - try { - // Trigger connection to validate the endpoint exists. - // subscribe() calls ensureConnected internally, but we want - // to detect failure eagerly so we can set _wsUnsupported. - await client.subscribe( - this.exchangeName, - "_ping", - [], - undefined, - 3000, - ).catch(() => { - // Expected -- no _ping method. The connection itself - // succeeded if we got a WS error frame back. If the - // connection itself failed, we'll catch below. - }); - // If we got here without the connect promise rejecting, - // the WS endpoint exists. - if (!client.connected) { - throw new Error("WS handshake failed"); - } - } catch (err) { - logger.warn('PmxtClient: WebSocket probe failed, falling back to HTTP', { error: String(err) }); - this._wsUnsupported = true; - client.close(); - return null; - } - - this._wsClient = client; - return this._wsClient; - } - - /** - * Attempt to use the WS transport for a watch method. - * Returns the raw data on success, or `null` if WS is unavailable. - */ - private async watchViaWs( - method: string, - args: any[], - ): Promise { - const ws = await this.getOrCreateWs(); - if (!ws) return null; - - try { - return await ws.subscribe( - this.exchangeName, - method, - args, - this.getCredentials() as Record | undefined, - ); - } catch (error) { - if (this.isWsTransportUnavailableError(error)) { - return null; - } - throw error; - } - } - - private wsTransportUnavailableError(method: string): PmxtError { - return new PmxtError(`${method}() requires WebSocket transport — connection failed`); - } - - private isWsTransportUnavailableError(error: unknown): boolean { - return error instanceof PmxtError - && /connection failed|no websocket|websocket.*not connected/i.test(error.message); - } - - private defaultWatchAllOrderBookVenues(): string[] | undefined { - if (Exchange.OBDATA_WATCH_ALL_SOURCES.has(this.exchangeName)) { - return [this.exchangeName]; - } - return undefined; - } - - private getWsInternals(ws: SidecarWsClient): SidecarWsClientInternals { - return ws as unknown as SidecarWsClientInternals; - } - - private wsSubscriptionKey(method: string, args: any[]): string { - const firstArg = args[0] ?? ""; - return Array.isArray(firstArg) - ? `${method}:${[...firstArg].sort().join(",")}` - : `${method}:${firstArg}`; - } - - private getWsSubscriptionId(ws: SidecarWsClient, method: string, args: any[]): string | undefined { - const internals = this.getWsInternals(ws); - const subKey = this.wsSubscriptionKey(method, args); - return internals.activeSubs.get(subKey); - } - - private clearWsSubscription(ws: SidecarWsClient, method: string, args: any[]): void { - const internals = this.getWsInternals(ws); - const subKey = this.wsSubscriptionKey(method, args); - const requestId = internals.activeSubs.get(subKey); - if (!requestId) return; - - const sub = internals.subscriptions.get(requestId); - if (sub?.reject) { - sub.reject(new PmxtError(`${method} subscription cancelled`)); - } - - internals.activeSubs.delete(subKey); - internals.subscriptions.delete(requestId); - internals.dataQueues.delete(requestId); - internals.dataStore.delete(requestId); - - const firstArg = args[0] ?? ""; - const symbols = Array.isArray(firstArg) - ? firstArg.map(String) - : firstArg - ? [String(firstArg)] - : []; - for (const symbol of symbols) { - internals.dataStore.delete(`${requestId}:${symbol}`); - } - } - - private async sendWsMessage( - ws: SidecarWsClient, - message: Record, - ): Promise { - const internals = this.getWsInternals(ws); - await internals.ensureConnected(); - - const socket = internals.ws; - if (!socket) { - throw new PmxtError('[ws-client] Cannot send: WebSocket not connected'); - } - socket.send(JSON.stringify(message)); - } - - // Low-Level API Access - - /** - * Call an exchange-specific REST endpoint by its operationId. - * This provides direct access to all implicit API methods defined in - * the exchange's OpenAPI spec (e.g., Polymarket CLOB, Kalshi trading API). - * - * @param operationId - The operationId (or auto-generated name) of the endpoint - * @param params - Optional parameters to pass to the endpoint - * @returns The raw response data from the exchange - * - * @example - * ```typescript - * // Call a Polymarket CLOB endpoint directly - * const result = await poly.callApi('getMarket', { condition_id: '0x...' }); - * ``` - */ - async callApi(operationId: string, params?: Record): Promise { - await this.initPromise; - try { - const url = `${this.resolveBaseUrl()}/api/${this.exchangeName}/callApi`; - - const requestBody: any = { - args: [operationId, params], - credentials: this.getCredentials() - }; - - const response = await this.fetchWithRetry(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...this.getAuthHeaders() - }, - body: JSON.stringify(requestBody) - }); - - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to call API '${operationId}': ${error}`); - } - } - - /** - * Dispatch a sidecar read method, preferring GET but transparently - * falling back to POST for full backward compatibility. - * - * GET is used when: - * - the client has no per-instance credentials (the sidecar's GET - * handler intentionally drops credentials to avoid leaking them - * through query strings and access logs), and - * - the sidecar hasn't already returned 404/405 for a previous GET - * in this client's lifetime (`_getReadsUnsupported`), and - * - the query has no nested objects (query strings can't round-trip - * arbitrary JSON). - * - * Otherwise (or if the GET attempt is rejected with 404/405) the call - * is sent as POST with the original `{args, credentials}` body so that - * SDK users talking to an older pmxt-core continue to work unchanged. - * - * @internal — shared transport used by every generated read method. - */ - protected async sidecarReadRequest( - methodName: string, - query: Record, - args: unknown[], - ): Promise { - const resolvedBase = this.resolveBaseUrl(); - const baseUrl = `${resolvedBase}/api/${this.exchangeName}/${methodName}`; - const hasCredentials = this.getCredentials() !== undefined; - - if (!hasCredentials && !this._getReadsUnsupported && !queryHasNestedObject(query)) { - const qs = buildSidecarQueryString(query); - const getUrl = qs ? `${baseUrl}?${qs}` : baseUrl; - const response = await this.fetchWithRetry(getUrl, { - method: 'GET', - headers: this.getAuthHeaders(), - }); - - // 404 / 405 => older sidecar without GET dispatch. Remember - // the downgrade so future calls skip the probe, and fall - // through to POST below. - if (response.status === 404 || response.status === 405) { - await response.text().catch(() => undefined); - this._getReadsUnsupported = true; - } else { - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - return response.json(); - } - } - - // POST fallback — identical to the original per-method template. - const response = await this.fetchWithRetry(baseUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - return response.json(); - } - - /** - * Dispatch a sidecar POST method with positional args and credentials. - * - * @internal - shared transport for hand-maintained methods that should - * never use the GET read path. - */ - protected async sidecarPostRequest(methodName: string, args: unknown[]): Promise { - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/${methodName}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - return response.json(); - } - - /** - * Pre-warm the sidecar's internal caches for a market outcome. - * - * @param outcomeId - The CLOB token ID for the outcome. - */ - async preWarmMarket(outcomeId: string): Promise { - await this.initPromise; - try { - const json = await this.sidecarPostRequest("preWarmMarket", [outcomeId]); - this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to preWarmMarket: ${error}`); - } - } - - /** - * Fetch a single Probable event by its numeric ID. - * - * @param id - The numeric event ID. - * @returns The event, or null when the venue has no matching event. - */ - async getEventById(id: string): Promise { - await this.initPromise; - try { - const json = await this.sidecarPostRequest("getEventById", [id]); - const data = this.handleResponse(json); - return data == null ? null : convertEvent(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to getEventById: ${error}`); - } - } - - /** - * Fetch a single Probable event by its URL slug. - * - * @param slug - The event slug. - * @returns The event, or null when the venue has no matching event. - */ - async getEventBySlug(slug: string): Promise { - await this.initPromise; - try { - const json = await this.sidecarPostRequest("getEventBySlug", [slug]); - const data = this.handleResponse(json); - return data == null ? null : convertEvent(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to getEventBySlug: ${error}`); - } - } - - /** - * Read a hosted catalog endpoint directly. - * - * Hosted-only Router APIs such as matched clusters are not part of the - * core sidecar method namespace. They live under /v0 and return their own - * response envelopes, so callers intentionally receive the raw JSON body. - */ - protected async catalogReadRequest(path: string, query: Record = {}): Promise { - const qs = buildSidecarQueryString(query); - const url = `${this.resolveBaseUrl()}${path}${qs ? `?${qs}` : ''}`; - const response = await this.fetchWithRetry(url, { - method: 'GET', - headers: this.getAuthHeaders(), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - return response.json(); - } - - // BEGIN GENERATED METHODS - - async loadMarkets(reload: boolean = false): Promise> { - await this.initPromise; - try { - const args: any[] = []; - args.push(reload); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/loadMarkets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - const result: Record = {}; - for (const [key, value] of Object.entries(data as any)) { - result[key] = convertMarket(value); - } - return result; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to loadMarkets: ${error}`); - } - } - - async fetchMarkets(params?: MarketFetchParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarkets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertMarket); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMarkets: ${error}`); - } - } - - async fetchMarketsPaginated(params?: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarketsPaginated`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return { - data: (data.data || []).map(convertMarket), - total: data.total, - nextCursor: data.nextCursor, - }; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMarketsPaginated: ${error}`); - } - } - - async fetchEventsPaginated(params?: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEventsPaginated`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return { - data: (data.data || []).map(convertEvent), - total: data.total, - nextCursor: data.nextCursor, - }; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchEventsPaginated: ${error}`); - } - } - - async fetchEvents(params?: EventFetchParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEvents`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertEvent); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchEvents: ${error}`); - } - } - - async fetchSeries(params?: SeriesFetchParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchSeries`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertSeries); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchSeries: ${error}`); - } - } - - async fetchMarket(params?: MarketFetchParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarket`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertMarket(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMarket: ${error}`); - } - } - - async fetchEvent(params?: EventFetchParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEvent`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertEvent(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchEvent: ${error}`); - } - } - - async fetchOrderBook(outcomeId: string | MarketOutcome, limit?: number, params?: FetchOrderBookParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(resolveOutcomeId(outcomeId)); - if (limit !== undefined) args.push(limit); - if (params !== undefined) { - if (limit === undefined) args.push(null); - args.push(params); - } - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOrderBook`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - if (Array.isArray(data)) { - return data.map(convertOrderBook); - } - return convertOrderBook(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchOrderBook: ${error}`); - } - } - - async fetchOrderBooks(outcomeIds: (string | MarketOutcome)[]): Promise> { - await this.initPromise; - try { - const args: any[] = []; - args.push(outcomeIds.map(resolveOutcomeId)); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOrderBooks`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - const result: Record = {}; - for (const [key, value] of Object.entries(data as any)) { - result[key] = convertOrderBook(value); - } - return result; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchOrderBooks: ${error}`); - } - } - - async submitOrder(built: BuiltOrder): Promise { - await this.initPromise; - if (this.isHosted) { - throw new PmxtError("submitOrder is not available in hosted mode. Use createOrder instead."); - } - try { - const args: any[] = []; - args.push(built); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/submitOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertOrder(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to submitOrder: ${error}`); - } - } - - async cancelOrder(orderId: string): Promise { - await this.initPromise; - if (this.isHosted) { - return this._hostedCancelOrder(orderId); - } - try { - const args: any[] = []; - args.push(orderId); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/cancelOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertOrder(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to cancelOrder: ${error}`); - } - } - - async fetchOrder(orderId: string): Promise { - await this.initPromise; - if (this.isHosted) { - const route = HOSTED_METHOD_ROUTES.get("fetchOrder")!; - const path = formatRoutePath(route, { order_id: orderId }); - const data = await _tradingRequest(this, { method: route.method, path }); - const payload = data && typeof data === "object" && "order" in data - ? (data as { order: unknown }).order - : data; - return orderFromV0(payload as Record); - } - try { - const args: any[] = []; - args.push(orderId); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertOrder(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchOrder: ${error}`); - } - } - - async fetchOpenOrders(marketId?: string): Promise { - await this.initPromise; - if (this.isHosted) { - const resolvedAddress = resolveWalletAddress(this, undefined); - const route = HOSTED_METHOD_ROUTES.get("fetchOpenOrders")!; - const path = formatRoutePath(route, {}); - const params: Record = { address: resolvedAddress }; - if (marketId !== undefined) params.market_id = marketId; - const data = await _tradingRequest(this, { method: route.method, path, params }); - const list = Array.isArray(data) - ? data - : (data && Array.isArray((data as any).orders) ? (data as any).orders : []); - return list.map((o: unknown) => orderFromV0(o as Record)); - } - try { - const args: any[] = []; - if (marketId !== undefined) args.push(marketId); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOpenOrders`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertOrder); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchOpenOrders: ${error}`); - } - } - - async fetchMyTrades(params?: MyTradesParams): Promise { - await this.initPromise; - if (this.isHosted) { - const resolvedAddress = resolveWalletAddress(this, undefined); - const route = HOSTED_METHOD_ROUTES.get("fetchMyTrades")!; - const path = formatRoutePath(route, { address: resolvedAddress }); - const data = await _tradingRequest(this, { method: route.method, path }); - const list = Array.isArray(data) ? data : (data && Array.isArray((data as any).trades) ? (data as any).trades : []); - return list.map((t: unknown) => userTradeFromV0(t as Record)); - } - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMyTrades`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertUserTrade); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMyTrades: ${error}`); - } - } - - async fetchClosedOrders(params?: OrderHistoryParams): Promise { - await this.initPromise; - if (this.isHosted) { - throw new NotSupported("fetchClosedOrders is not available in hosted trading mode."); - } - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchClosedOrders`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertOrder); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchClosedOrders: ${error}`); - } - } - - async fetchAllOrders(params?: OrderHistoryParams): Promise { - await this.initPromise; - if (this.isHosted) { - throw new NotSupported("fetchAllOrders is not available in hosted trading mode."); - } - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchAllOrders`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertOrder); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchAllOrders: ${error}`); - } - } - - async fetchPositions(address?: string): Promise { - await this.initPromise; - if (this.isHosted) { - const resolvedAddress = resolveWalletAddress(this, address); - const route = HOSTED_METHOD_ROUTES.get("fetchPositions")!; - const path = formatRoutePath(route, { address: resolvedAddress }); - const data = await _tradingRequest(this, { method: route.method, path }); - const list: unknown[] = Array.isArray(data) - ? data - : (data && Array.isArray((data as any).positions) ? (data as any).positions : []); - return list.map((p) => positionFromV0(p as Record)); - } - try { - const args: any[] = []; - if (address !== undefined) args.push(address); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchPositions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertPosition); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchPositions: ${error}`); - } - } - - async fetchBalance(address?: string): Promise { - await this.initPromise; - if (this.isHosted) { - const resolvedAddress = resolveWalletAddress(this, address); - const route = HOSTED_METHOD_ROUTES.get("fetchBalance")!; - const path = formatRoutePath(route, { address: resolvedAddress }); - const data = await _tradingRequest(this, { method: route.method, path }); - const list: unknown[] = Array.isArray(data) - ? data - : (data && Array.isArray((data as any).balances) ? (data as any).balances : []); - return list.map((b) => balanceFromV0(b as Record)); - } - try { - const args: any[] = []; - if (address !== undefined) args.push(address); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchBalance`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data.map(convertBalance); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchBalance: ${error}`); - } - } - - async unwatchOrderBook(outcomeId: string | MarketOutcome): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(resolveOutcomeId(outcomeId)); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/unwatchOrderBook`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to unwatchOrderBook: ${error}`); - } - } - - async unwatchAddress(address: string): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(address); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/unwatchAddress`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to unwatchAddress: ${error}`); - } - } - - async close(): Promise { - await this.initPromise; - try { - const args: any[] = []; - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/close`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to close: ${error}`); - } - } - - async fetchMarketMatches(params?: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarketMatches`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMarketMatches: ${error}`); - } - } - - async fetchMatches(params: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMatches`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMatches: ${error}`); - } - } - - async fetchEventMatches(params?: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEventMatches`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchEventMatches: ${error}`); - } - } - - async compareMarketPrices(params: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/compareMarketPrices`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to compareMarketPrices: ${error}`); - } - } - - async fetchRelatedMarkets(params: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchRelatedMarkets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchRelatedMarkets: ${error}`); - } - } - - async fetchMatchedMarkets(params?: FetchMatchedMarketClustersParams): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMatchedMarkets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMatchedMarkets: ${error}`); - } - } - - async fetchMatchedPrices(params?: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMatchedPrices`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchMatchedPrices: ${error}`); - } - } - - async fetchHedges(params: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchHedges`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchHedges: ${error}`); - } - } - - async fetchArbitrage(params?: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - if (params !== undefined) args.push(params); - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchArbitrage`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetchArbitrage: ${error}`); - } - } - - // END GENERATED METHODS - - /** - * Get historical price candles. - * - * @param outcomeId - Outcome ID (from market.outcomes[].outcomeId) - * @param params - History filter parameters - * @returns List of price candles - * - * @example - * ```typescript - * const markets = await exchange.fetchMarkets({ query: "Trump" }); - * const outcomeId = markets[0].outcomes[0].outcomeId; - * const candles = await exchange.fetchOHLCV(outcomeId, { - * resolution: "1h", - * limit: 100 - * }); - * ``` - */ - async fetchOHLCV( - outcomeId: string | MarketOutcome, - params: any - ): Promise { - await this.initPromise; - const resolvedOutcomeId = resolveOutcomeId(outcomeId); - try { - const paramsDict: any = { resolution: params.resolution }; - if (params.start) { - paramsDict.start = params.start.toISOString(); - } - if (params.end) { - paramsDict.end = params.end.toISOString(); - } - if (params.limit) { - paramsDict.limit = params.limit; - } - - const args = [resolvedOutcomeId, paramsDict]; - const query = { outcomeId: resolvedOutcomeId, ...paramsDict }; - const json = await this.sidecarReadRequest('fetchOHLCV', query, args); - const data = this.handleResponse(json); - return data.map(convertCandle); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetch OHLCV: ${error}`); - } - } - - /** - * Get trade history for an outcome. - * - * Note: Polymarket requires API key. - * - * @param outcomeId - Outcome ID - * @param params - History filter parameters - * @returns List of trades - */ - async fetchTrades( - outcomeId: string | MarketOutcome, - params: any - ): Promise { - await this.initPromise; - const resolvedOutcomeId = resolveOutcomeId(outcomeId); - try { - const paramsDict: any = {}; - if (params.resolution) { - paramsDict.resolution = params.resolution; - } - if (params.limit) { - paramsDict.limit = params.limit; - } - if (params.start) { - paramsDict.start = params.start instanceof Date ? params.start.toISOString() : params.start; - } - if (params.end) { - paramsDict.end = params.end instanceof Date ? params.end.toISOString() : params.end; - } - - const args = [resolvedOutcomeId, paramsDict]; - const query = { outcomeId: resolvedOutcomeId, ...paramsDict }; - const json = await this.sidecarReadRequest('fetchTrades', query, args); - const data = this.handleResponse(json); - return data.map(convertTrade); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to fetch trades: ${error}`); - } - } - - // WebSocket Streaming Methods - - /** - * Watch real-time order book updates via WebSocket. - * - * Returns a promise that resolves with the next order book update. - * Call repeatedly in a loop to stream updates (CCXT Pro pattern). - * - * @param outcomeId - Outcome ID to watch - * @param limit - Optional depth limit for order book - * @param params - Optional exchange-specific parameters - * @returns Next order book update - * - * @example - * ```typescript - * // Stream order book updates - * while (true) { - * const orderBook = await exchange.watchOrderBook(outcomeId); - * console.log(`Best bid: ${orderBook.bids[0].price}`); - * console.log(`Best ask: ${orderBook.asks[0].price}`); - * } - * ``` - */ - async watchOrderBook(outcomeId: string | MarketOutcome, limit?: number, params: Record = {}): Promise { - await this.initPromise; - const resolvedOutcomeId = resolveOutcomeId(outcomeId); - const args: any[] = [resolvedOutcomeId]; - if (limit !== undefined) { - args.push(limit); - } - if (Object.keys(params).length > 0) { - if (limit === undefined) { - args.push(undefined); - } - args.push(params); - } - - const wsData = await this.watchViaWs("watchOrderBook", args); - if (wsData !== null) { - return convertOrderBook(wsData); - } - - throw this.wsTransportUnavailableError("watchOrderBook"); - } - - /** - * Watch real-time order book updates for multiple outcomes at once. - * - * Returns a record mapping each outcome ID (ticker) to its latest - * order book snapshot. Call repeatedly in a loop to stream updates - * (CCXT Pro pattern). - * - * @param outcomeIds - Array of outcome IDs (or MarketOutcome objects) - * @param limit - Optional depth limit for each order book - * @param params - Optional exchange-specific parameters - * @returns Record mapping ticker to OrderBook - * - * @example - * ```typescript - * const ids = markets.slice(0, 3).map(m => m.outcomes[0].outcomeId); - * while (true) { - * const books = await exchange.watchOrderBooks(ids); - * for (const [ticker, ob] of Object.entries(books)) { - * console.log(`${ticker}: bid=${ob.bids[0]?.price}`); - * } - * } - * ``` - */ - async watchOrderBooks( - outcomeIds: (string | MarketOutcome)[], - limit?: number, - params: Record = {}, - ): Promise> { - await this.initPromise; - const resolvedIds = outcomeIds.map(resolveOutcomeId); - const args: any[] = [resolvedIds]; - if (limit !== undefined) { - args.push(limit); - } - if (Object.keys(params).length > 0) { - if (limit === undefined) { - args.push(undefined); - } - args.push(params); - } - - try { - const ws = await this.getOrCreateWs(); - if (!ws) { - throw this.wsTransportUnavailableError("watchOrderBooks"); - } - - const rawResult = await ws.subscribeBatch( - this.exchangeName, - "watchOrderBooks", - args, - this.getCredentials() as Record | undefined, - ); - if (rawResult && typeof rawResult === "object") { - const result: Record = {}; - for (const [k, v] of Object.entries(rawResult)) { - if (v && typeof v === "object") { - result[k] = convertOrderBook(v); - } - } - return result; - } - - throw new PmxtError("watchOrderBooks: unexpected response shape from server"); - } catch (error) { - if (this.isWsTransportUnavailableError(error)) { - throw this.wsTransportUnavailableError("watchOrderBooks"); - } - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to watch order books: ${error}`); - } - } - - /** - * Stream all orderbook updates across venues via the hosted WebSocket API. - * - * Returns a promise that resolves with the next book event. - * Call repeatedly in a loop to stream updates (CCXT Pro pattern). - * Requires hosted mode (`pmxtApiKey` set). - * - * @param venues - Optional venue filter. Defaults to this exchange's venue - * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues. - * @returns Next event with source, symbol, and orderbook - * - * @example - * ```typescript - * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" }); - * while (true) { - * const event = await poly.watchAllOrderBooks(); - * console.log(event.source, event.symbol, event.orderbook.bids[0]); - * } - * ``` - */ - async watchAllOrderBooks(venues?: string[]): Promise { - await this.initPromise; - - if (!this.isHosted) { - throw new PmxtError("watchAllOrderBooks() requires hosted mode (set pmxtApiKey)"); - } - - const effectiveVenues = venues ?? this.defaultWatchAllOrderBookVenues(); - const args: any[] = effectiveVenues?.length ? [effectiveVenues] : []; - const wsData = await this.watchViaWs("watchAllOrderBooks", args); - if (wsData !== null) { - return { - source: (wsData as any)._source || "", - symbol: (wsData as any)._symbol || "", - orderbook: convertOrderBook(wsData), - }; - } - - throw this.wsTransportUnavailableError("watchAllOrderBooks"); - } - - /** - * Stream all orderbook updates across venues. - * - * @deprecated Use {@link watchAllOrderBooks} instead. - * @param venues - Optional venue filter - * @returns Next event with source, symbol, and orderbook - */ - async firehose(venues?: string[]): Promise { - return this.watchAllOrderBooks(venues); - } - - /** - * Watch real-time trade updates via WebSocket. - * - * Returns a promise that resolves with the next trade(s). - * Call repeatedly in a loop to stream updates (CCXT Pro pattern). - * - * @param outcomeId - Outcome ID to watch - * @param address - Public wallet to be watched - * @param since - Optional timestamp to filter trades from - * @param limit - Optional limit for number of trades - * @returns Next trade update(s) - * - * @example - * ```typescript - * // Stream trade updates - * while (true) { - * const trades = await exchange.watchTrades(outcomeId); - * for (const trade of trades) { - * console.log(`Trade: ${trade.price} @ ${trade.amount}`); - * } - * } - * ``` - */ - async watchTrades( - outcomeId: string | MarketOutcome, - address?: string, - since?: number, - limit?: number - ): Promise { - await this.initPromise; - const resolvedOutcomeId = resolveOutcomeId(outcomeId); - const args: any[] = [resolvedOutcomeId]; - if (address !== undefined) { - args.push(address); - } - if (since !== undefined) { - args.push(since); - } - if (limit !== undefined) { - args.push(limit); - } - - const wsData = await this.watchViaWs("watchTrades", args); - if (wsData !== null) { - return wsData.map(convertTrade); - } - - throw this.wsTransportUnavailableError("watchTrades"); - } - - /** - * Watch real-time updates of a public wallet via WebSocket. - * - * Returns a promise that resolves with the next update(s). - * Call repeatedly in a loop to stream updates (CCXT Pro pattern). - * - * @param address - Public wallet to be watched - * @param types - Subscription options including 'trades', 'positions', and 'balances' - * @returns Next update(s) - * - * @example - * ```typescript - * // Stream updates of a public wallet address - * while (true) { - * const snapshots = await exchange.watchAddress(address, types); - * for (const snapshot of snapshots) { - * console.log(`Trade: ${snapshot.trades}`); - * } - * } - * ``` - */ - async watchAddress( - address: string, - types?: SubscriptionOption[], - ): Promise { - await this.initPromise; - try { - const args: any[] = [address]; - if (types !== undefined) { - args.push(types); - } - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/watchAddress`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertSubscriptionSnapshot(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to watch address: ${error}`); - } - } - - // Trading Methods (require authentication) - - /** - * Build an order payload without submitting it to the exchange. - * Returns the exchange-native signed order or transaction payload for - * inspection, forwarding through a middleware layer, or deferred - * submission via {@link submitOrder}. - * - * You can specify the market either with explicit marketId/outcomeId, - * or by passing an outcome object directly (e.g., market.yes). - * - * @param params - Order parameters (same as createOrder) - * @returns A BuiltOrder containing the exchange-native payload - * - * @example - * ```typescript - * // Build, inspect, then submit: - * const built = await exchange.buildOrder({ - * marketId: "663583", - * outcomeId: "10991849...", - * side: "buy", - * type: "limit", - * amount: 10, - * price: 0.55 - * }); - * - * console.log(built.signedOrder); // inspect before submitting - * const order = await exchange.submitOrder(built); - * - * // Using outcome shorthand: - * const yes = market.yes; - * if (!yes) throw new Error("Market has no YES outcome"); - * - * const built2 = await exchange.buildOrder({ - * outcome: yes, - * side: "buy", - * type: "market", - * amount: 10 - * }); - * ``` - */ - async buildOrder(params: CreateOrderInput): Promise { - if (this.isHostedTradingMode()) { - return this._hostedBuildOrder(params); - } - if (this.isHosted) { - throw new PmxtError( - "Trade execution is not available through the hosted API. " + - "Use the local PMXT SDK with your venue credentials instead. " + - "See https://pmxt.dev/docs/quickstart for setup instructions." - ); - } - await this.initPromise; - try { - let marketId = params.marketId; - let outcomeId = params.outcomeId; - - if (params.outcome) { - if (marketId !== undefined || outcomeId !== undefined) { - throw new PmxtError( - "Cannot specify both 'outcome' and 'marketId'/'outcomeId'. Use one or the other." - ); - } - const outcome: MarketOutcome = params.outcome; - if (!outcome.marketId) { - throw new PmxtError( - "outcome.marketId is not set. Ensure the outcome comes from a fetched market." - ); - } - marketId = outcome.marketId; - outcomeId = outcome.outcomeId; - } - - const paramsDict: any = { - marketId, - outcomeId, - side: params.side, - type: params.type, - amount: params.amount, - }; - if (params.price !== undefined) { - paramsDict.price = params.price; - } - if (params.fee !== undefined) { - paramsDict.fee = params.fee; - } - if (params.tickSize !== undefined) { - paramsDict.tickSize = params.tickSize; - } - if (params.negRisk !== undefined) { - paramsDict.negRisk = params.negRisk; - } - if (params.onBehalfOf !== undefined) { - paramsDict.onBehalfOf = params.onBehalfOf; - } - - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/buildOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args: [paramsDict], credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return data as BuiltOrder; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to build order: ${error}`); - } - } - - private async _discoverHostedAccount(): Promise { - if (this._hostedAccount) return; - if (!this._accountDiscoveryPromise) { - this._accountDiscoveryPromise = (async () => { - try { - const res = await this.fetchWithRetry( - `${this.resolveBaseUrl()}/v0/account`, - { method: 'GET', headers: { ...this.getAuthHeaders() } }, - ); - if (res.ok) { - const body = await res.json(); - this._hostedAccount = { depositWallet: body.deposit_wallet, signatureType: body.signature_type }; - } else { this._hostedAccount = {}; } - } catch (err) { - logger.warn('PmxtClient: hosted account discovery failed', { error: String(err) }); - this._hostedAccount = {}; - } - })(); - } - await this._accountDiscoveryPromise; - } - - private async _executeSorOrder(params: any): Promise { - await this._discoverHostedAccount(); - - const buildRes = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/sor/buildOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args: [params] }), - }); - if (!buildRes.ok) throw new PmxtError(`buildOrder failed: ${await buildRes.text()}`); - const buildJson = await buildRes.json(); - const { orderId, legs } = buildJson.data || buildJson; - - const fills: any[] = []; - for (const leg of legs) { - try { - const { Polymarket, Limitless } = require('pmxt-core'); - const VenueClass = leg.venue === 'polymarket' ? Polymarket : leg.venue === 'limitless' ? Limitless : null; - if (!VenueClass) throw new Error(`unsupported venue: ${leg.venue}`); - - const venueOpts: any = { privateKey: this.privateKey }; - if (leg.venue === 'polymarket' && this._hostedAccount?.depositWallet) { - venueOpts.funderAddress = this._hostedAccount.depositWallet; - venueOpts.signatureType = this._hostedAccount.signatureType || 3; - } - const venue = new VenueClass(venueOpts); - const orderParams: any = { outcomeId: leg.tokenId, side: leg.side, amount: leg.shares }; - if (leg.orderType === 'market') { orderParams.type = 'market'; orderParams.price = leg.price; } - else { orderParams.price = leg.price; } - const order = await venue.createOrder(orderParams); - fills.push({ venue: leg.venue, venueOrderId: order.id, venueMarketId: leg.venueMarketId, venueOutcomeId: leg.venueOutcomeId, shares: order.filled ?? leg.shares, price: order.price || leg.price, status: order.status }); - } catch (err: any) { - fills.push({ venue: leg.venue, venueMarketId: leg.venueMarketId, venueOutcomeId: leg.venueOutcomeId, shares: leg.shares, price: leg.price, status: 'failed', error: err.message }); - } - } - - const submitRes = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/sor/submitOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args: [{ orderId, fills }] }), - }); - if (!submitRes.ok) throw new PmxtError(`submitOrder failed: ${await submitRes.text()}`); - const submitJson = await submitRes.json(); - const submitData = submitJson.data || submitJson; - if (submitData.status === 'failed' && submitData.errors?.length) { - throw new PmxtError(submitData.errors[0]); - } - return convertOrder(submitData); - } - - /** - * Hosted-mode buildOrder: validate inputs locally, then POST to the - * trading service's `build-order` endpoint and return a BuiltOrder - * that carries the original build_request for Layer-2 economic checks - * at submit time. - */ - private async _hostedBuildOrder( - params: CreateOrderInput, - ): Promise { - const body = this._hostedBuildOrderBody(params); - const route = HOSTED_METHOD_ROUTES.get("buildOrder")!; - const data = await _tradingRequest(this, { - method: route.method, - path: route.path, - body, - }) as Record; - // Attach the originating build_request so submit can run economic - // validation without an extra catalog round-trip. - const built = { ...data, build_request: body } as unknown as BuiltOrder; - return built; - } - - /** - * Hosted-mode createOrder: build → sign → submit single-call wrapper. - */ - private async _hostedCreateOrder(params: CreateOrderInput): Promise { - const built = await this._hostedBuildOrder(params); - return this._hostedSubmitOrder(built); - } - - /** - * Hosted-mode submitOrder: validate the stored build response, sign the - * typed_data (and pull_typed_data for cross-chain venue sells), then - * POST to `/v0/trade/submit-order`. - * - * Restored after PR #1058 (Limitless hosted wire-up) accidentally - * removed it but left the call site + signing imports intact. - */ - private async _hostedSubmitOrder(built: BuiltOrder): Promise { - const signer = this.requireHostedSigner(); - if (!this.walletAddress) { - throw new MissingWalletAddress( - "hosted submitOrder requires walletAddress", - ); - } - const payload = built as unknown as Record; - const typedData = payload["typed_data"] as TypedData | undefined; - if (!typedData) { - throw new HostedInvalidSignature(0, "typed_data missing from built order"); - } - const buildRequest = (payload["build_request"] as Record | undefined) - ?? ((payload["params"] as Record | undefined)?.["build_request"] as Record | undefined); - - const side = String(buildRequest?.["side"] ?? "buy"); - const primaryRoute = this._hostedTypedDataRoute(side, false); - validateTypedData(typedData, primaryRoute, this.walletAddress); - if (buildRequest) { - validateEconomics(typedData, primaryRoute, buildRequest, payload); - } - - const signature = await signer.signTypedData(typedData); - verifySignature(typedData, signature, signer.address); - - const body: Record = { - built_order_id: payload["built_order_id"], - signature, - }; - - const pullTypedData = payload["pull_typed_data"] as TypedData | undefined; - if (pullTypedData) { - const pullRoute = this._hostedTypedDataRoute(side, true); - if (pullRoute) { - validateTypedData(pullTypedData, pullRoute, this.walletAddress); - } - const pullSig = await signer.signTypedData(pullTypedData); - verifySignature(pullTypedData, pullSig, signer.address); - body["pull_signature"] = pullSig; - } - - const route = HOSTED_METHOD_ROUTES.get("submitOrder")!; - const data = await _tradingRequest(this, { method: route.method, path: route.path, body }); - return orderFromV0(data as Record); - } - - /** - * Resolve the per-(venue, side, pull) typed-data schema route used by - * `validateTypedData` / `validateEconomics`. Returns the cross-chain - * pull-leg route name for Opinion sells and Limitless cross-chain orders. - */ - private _hostedTypedDataRoute(side: string, isPull: boolean): HostedRoute { - const venue = this.exchangeName; - const sideLower = side.toLowerCase(); - if (venue === "polymarket") { - return sideLower === "sell" ? "polymarket_sell" : "polymarket_buy"; - } - if (venue === "limitless") { - if (sideLower === "buy") return "limitless_buy"; - return isPull ? "limitless_sell_base_pull" : "limitless_sell_polygon"; - } - // opinion - if (sideLower === "buy") return "opinion_buy"; - return isPull ? "opinion_sell_bsc_pull" : "opinion_sell_polygon"; - } - - /** - * Hosted-mode cancelOrder: build → sign → cancel single-call wrapper. - * Mirrors the Python SDK's `_hosted_cancel_order`. - */ - private async _hostedCancelOrder(orderId: string): Promise { - const signer = this.requireHostedSigner(); - if (!this.walletAddress) { - throw new MissingWalletAddress("hosted cancelOrder requires walletAddress"); - } - const buildRequest = { order_id: orderId, user_address: this.walletAddress }; - const buildRoute = HOSTED_METHOD_ROUTES.get("cancelOrderBuild")!; - const buildPayload = await _tradingRequest(this, { - method: buildRoute.method, - path: buildRoute.path, - body: buildRequest, - }) as Record; - - const typedData = buildPayload["typed_data"] as TypedData | undefined; - if (!typedData) { - throw new HostedInvalidSignature(0, "typed_data missing from hosted cancel build response"); - } - const primaryRoute = this._hostedCancelTypedDataRoute(false); - validateTypedData(typedData, primaryRoute, this.walletAddress); - const signature = await signer.signTypedData(typedData); - verifySignature(typedData, signature, signer.address); - - const cancelId = buildPayload["cancel_id"]; - if (!cancelId) { - throw new HostedInvalidSignature(0, "cancel_id missing from hosted cancel build response"); - } - const body: Record = { cancel_id: cancelId, signature }; - - const pullTypedData = buildPayload["pull_typed_data"] as TypedData | undefined; - if (pullTypedData) { - const pullRoute = this._hostedCancelTypedDataRoute(true); - validateTypedData(pullTypedData, pullRoute, this.walletAddress); - const pullSig = await signer.signTypedData(pullTypedData); - verifySignature(pullTypedData, pullSig, signer.address); - body["pull_signature"] = pullSig; - } - - const cancelRoute = HOSTED_METHOD_ROUTES.get("cancelOrder")!; - const data = await _tradingRequest(this, { - method: cancelRoute.method, - path: cancelRoute.path, - body, - }); - return orderFromV0(data as Record); - } - - private _hostedCancelTypedDataRoute(isPull: boolean): HostedRoute { - const venue = this.exchangeName; - if (venue === "polymarket") return "cancel_polymarket"; - if (venue === "limitless") { - return isPull ? "cancel_limitless_base_pull" : "cancel_limitless_polygon"; - } - return isPull ? "cancel_opinion_bsc_pull" : "cancel_opinion_polygon"; - } - - /** - * Construct the hosted build-order request body and validate inputs - * locally per the v0 contract (denom/side compatibility, > 6-decimal - * precision rejected via {@link to6dec}). - */ - private _hostedBuildOrderBody( - params: CreateOrderInput, - ): Record { - let marketId: string | undefined = params.marketId; - let outcomeId: string | undefined = params.outcomeId; - - if (params.outcome) { - if (marketId !== undefined || outcomeId !== undefined) { - throw new InvalidOrder( - "cannot specify both 'outcome' and 'marketId'/'outcomeId'", - ); - } - const outcome: MarketOutcome = params.outcome; - if (!outcome.outcomeId) { - throw new InvalidOrder( - "outcome.outcomeId is not set; ensure the outcome comes from a fetched market", - ); - } - // marketId is optional in hosted mode -- backend derives it from - // outcomeId (catalog UUID). Forward it when present for backcompat. - marketId = outcome.marketId || undefined; - outcomeId = outcome.outcomeId; - } - - if (!outcomeId) { - throw new InvalidOrder( - "outcomeId is required (or pass an 'outcome' from a fetched market)", - ); - } - - const side = String(params.side); - const orderType = String(params.type ?? "market"); - const denom = (params as unknown as Record)["denom"] as - | "usdc" - | "shares" - | undefined; - - // denom/side compatibility per v0: - // market buy -> denom='usdc' - // market sell -> denom='shares' - // any limit -> denom='shares' - let resolvedDenom: "usdc" | "shares"; - if (orderType === "market") { - if (side === "buy") { - if (denom && denom !== "usdc") { - throw new InvalidOrder("market buy requires denom='usdc'"); - } - resolvedDenom = "usdc"; - } else if (side === "sell") { - if (denom && denom !== "shares") { - throw new InvalidOrder("market sell requires denom='shares'"); - } - resolvedDenom = "shares"; - } else { - throw new InvalidOrder(`unknown side: ${side}`); - } - } else { - if (denom && denom !== "shares") { - throw new InvalidOrder("limit orders require denom='shares'"); - } - resolvedDenom = "shares"; - } - - if (!(Number(params.amount) > 0)) { - throw new InvalidOrder("amount must be positive"); - } - - // to6dec throws InvalidOrder for sub-micro precision. - const amount6dec = to6dec(params.amount as number).toString(); - - // The supplied outcomeId may be a catalog UUID OR a venue-native id - // (e.g. a Polymarket tokenId or an Opinion market hash). Catalog - // UUIDs are forwarded as `outcome_id`; venue-native ids are - // forwarded as `(venue, venue_outcome_id)` so the backend resolver - // picks the right path. Either shape is accepted by the v0 trading - // API. - const body: Record = { - side, - order_type: orderType, - denom: resolvedDenom, - amount: params.amount, - amount_6dec: amount6dec, - }; - if (Exchange._isCatalogUuid(outcomeId)) { - body["outcome_id"] = outcomeId; - // market_id is optional in hosted mode: backend derives it - // from outcome_id (UUID) when omitted. Forward only when the - // caller supplied a non-empty UUID -- "absent" and "null" are - // not equivalent under some Pydantic configs on the backend. - if (marketId && Exchange._isCatalogUuid(marketId)) { - body["market_id"] = marketId; - } - } else { - // Venue-native form: backend resolves the row from - // (source_exchange, pmxt_id). marketId from a venue client is - // itself venue-native and would fail backend UUID validation - // if forwarded -- suppress it. - body["venue"] = this.exchangeName; - body["venue_outcome_id"] = outcomeId; - } - - if (params.price !== undefined) body["price"] = params.price; - const extra = params as unknown as Record; - if (extra["slippage_pct"] !== undefined) { - body["slippage_pct"] = extra["slippage_pct"]; - } - body["user_address"] = resolveWalletAddress(this); - return body; - } - - /** - * @example - * ```typescript - * const order = await exchange.createOrder({ - * marketId: "663583", - * outcomeId: "10991849...", - * side: "buy", - * type: "limit", - * amount: 10, - * price: 0.55 - * }); - * ``` - */ - async createOrder(params: CreateOrderInput): Promise { - // SOR escape path (preserved): legacy hosted SOR flow uses a venue-side - // SDK to execute the legs, only when a privateKey is present. - if (this.isHosted && this.exchangeName === 'sor' && this.privateKey) { - return this._executeSorOrder(params as any); - } - if (this.isHostedTradingMode()) { - return this._hostedCreateOrder(params); - } - if (this.isHosted) { - throw new PmxtError( - "Trade execution is not available through the hosted API. " + - "Use the local PMXT SDK with your venue credentials instead. " + - "See https://pmxt.dev/docs/quickstart for setup instructions." - ); - } - await this.initPromise; - try { - // Resolve outcome shorthand: extract marketId/outcomeId from outcome object - let marketId = params.marketId; - let outcomeId = params.outcomeId; - - if (params.outcome) { - if (marketId !== undefined || outcomeId !== undefined) { - throw new PmxtError( - "Cannot specify both 'outcome' and 'marketId'/'outcomeId'. Use one or the other." - ); - } - const outcome: MarketOutcome = params.outcome; - if (!outcome.marketId) { - throw new PmxtError( - "outcome.marketId is not set. Ensure the outcome comes from a fetched market." - ); - } - marketId = outcome.marketId; - outcomeId = outcome.outcomeId; - } - - const paramsDict: any = { - marketId, - outcomeId, - side: params.side, - type: params.type, - amount: params.amount, - }; - if (params.price !== undefined) { - paramsDict.price = params.price; - } - if (params.fee !== undefined) { - paramsDict.fee = params.fee; - } - if (params.tickSize !== undefined) { - paramsDict.tickSize = params.tickSize; - } - if (params.negRisk !== undefined) { - paramsDict.negRisk = params.negRisk; - } - if (params.onBehalfOf !== undefined) { - paramsDict.onBehalfOf = params.onBehalfOf; - } - - const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/createOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args: [paramsDict], credentials: this.getCredentials() }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertOrder(data); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to create order: ${error}`); - } - } - - /** - * Calculate the average execution price for a given amount by walking the order book. - * Uses the sidecar server for calculation to ensure consistency. - * - * @param orderBook - The current order book - * @param side - 'buy' or 'sell' - * @param amount - The amount to execute - * @returns The volume-weighted average price, or 0 if insufficient liquidity - */ - getExecutionPrice(orderBook: OrderBook, side: 'buy' | 'sell', amount: number): number { - const levels = side === 'buy' ? orderBook.asks : orderBook.bids; - let remaining = amount; - let totalCost = 0; - for (const level of levels) { - const fill = Math.min(remaining, level.size); - totalCost += fill * level.price; - remaining -= fill; - if (remaining <= 0) break; - } - if (remaining > 0) return 0; - return totalCost / amount; - } - - /** - * Calculate detailed execution price information by walking the order book locally. - * - * @param orderBook - The current order book - * @param side - 'buy' or 'sell' - * @param amount - The amount to execute - * @returns Detailed execution result - */ - getExecutionPriceDetailed( - orderBook: OrderBook, - side: 'buy' | 'sell', - amount: number - ): ExecutionPriceResult { - if (amount <= 0) { - throw new Error('Amount must be greater than 0'); - } - - const levels = (side === 'buy' ? orderBook.asks : orderBook.bids) - .filter((level) => level.size > 0) - .sort((a, b) => side === 'buy' ? a.price - b.price : b.price - a.price); - - if (levels.length === 0) { - return { price: 0, filledAmount: 0, fullyFilled: false }; - } - - let remainingAmount = amount; - let totalCost = 0; - let filledAmount = 0; - const epsilon = 0.00000001; - - for (const level of levels) { - if (remainingAmount <= epsilon) break; - - const fillSize = Math.min(remainingAmount, level.size); - totalCost += fillSize * level.price; - filledAmount += fillSize; - remainingAmount -= fillSize; - } - - return { - price: filledAmount > epsilon ? totalCost / filledAmount : 0, - filledAmount, - fullyFilled: remainingAmount <= epsilon, - }; - } - - // ---------------------------------------------------------------------------- - // Filtering Methods - // ---------------------------------------------------------------------------- - - /** - * Filter markets based on criteria or custom function. - * - * @param markets - Array of markets to filter - * @param criteria - Filter criteria object, string (simple text search), or predicate function - * @returns Filtered array of markets - * - * @example Simple text search - * api.filterMarkets(markets, 'Trump') - * - * @example Advanced filtering - * api.filterMarkets(markets, { - * text: 'Trump', - * searchIn: ['title', 'tags'], - * volume24h: { min: 10000 }, - * category: 'Politics', - * price: { outcome: 'yes', max: 0.5 } - * }) - * - * @example Custom predicate - * api.filterMarkets(markets, m => m.liquidity > 5000 && m.yes?.price < 0.3) - */ - filterMarkets( - markets: UnifiedMarket[], - criteria: string | MarketFilterCriteria | MarketFilterFunction - ): UnifiedMarket[] { - // Handle predicate function - if (typeof criteria === 'function') { - return markets.filter(criteria); - } - - // Handle simple string search - if (typeof criteria === 'string') { - const lowerQuery = criteria.toLowerCase(); - return markets.filter(m => - m.title.toLowerCase().includes(lowerQuery) - ); - } - - // Handle criteria object - return markets.filter(market => { - // Text search - if (criteria.text) { - const lowerQuery = criteria.text.toLowerCase(); - const searchIn = criteria.searchIn || ['title']; - let textMatch = false; - - for (const field of searchIn) { - if (field === 'title' && market.title?.toLowerCase().includes(lowerQuery)) { - textMatch = true; - break; - } - if (field === 'description' && market.description?.toLowerCase().includes(lowerQuery)) { - textMatch = true; - break; - } - if (field === 'category' && market.category?.toLowerCase().includes(lowerQuery)) { - textMatch = true; - break; - } - if (field === 'tags' && market.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))) { - textMatch = true; - break; - } - if (field === 'outcomes' && market.outcomes?.some(o => o.label.toLowerCase().includes(lowerQuery))) { - textMatch = true; - break; - } - } - - if (!textMatch) return false; - } - - // Category filter - if (criteria.category && market.category !== criteria.category) { - return false; - } - - // Tags filter (match ANY of the provided tags) - if (criteria.tags && criteria.tags.length > 0) { - const hasMatchingTag = criteria.tags.some(tag => - market.tags?.some(marketTag => - marketTag.toLowerCase() === tag.toLowerCase() - ) - ); - if (!hasMatchingTag) return false; - } - - // Volume24h filter - if (criteria.volume24h) { - if (criteria.volume24h.min !== undefined && market.volume24h < criteria.volume24h.min) { - return false; - } - if (criteria.volume24h.max !== undefined && market.volume24h > criteria.volume24h.max) { - return false; - } - } - - // Volume filter - if (criteria.volume) { - if (criteria.volume.min !== undefined && (market.volume || 0) < criteria.volume.min) { - return false; - } - if (criteria.volume.max !== undefined && (market.volume || 0) > criteria.volume.max) { - return false; - } - } - - // Liquidity filter - if (criteria.liquidity) { - if (criteria.liquidity.min !== undefined && market.liquidity < criteria.liquidity.min) { - return false; - } - if (criteria.liquidity.max !== undefined && market.liquidity > criteria.liquidity.max) { - return false; - } - } - - // OpenInterest filter - if (criteria.openInterest) { - if (criteria.openInterest.min !== undefined && (market.openInterest || 0) < criteria.openInterest.min) { - return false; - } - if (criteria.openInterest.max !== undefined && (market.openInterest || 0) > criteria.openInterest.max) { - return false; - } - } - - // ResolutionDate filter - if (criteria.resolutionDate && market.resolutionDate) { - const resDate = market.resolutionDate; - if (criteria.resolutionDate.before && resDate >= criteria.resolutionDate.before) { - return false; - } - if (criteria.resolutionDate.after && resDate <= criteria.resolutionDate.after) { - return false; - } - } - - // Price filter (for binary markets) - if (criteria.price) { - const outcome = market[criteria.price.outcome]; - if (!outcome) return false; - - if (criteria.price.min !== undefined && outcome.price < criteria.price.min) { - return false; - } - if (criteria.price.max !== undefined && outcome.price > criteria.price.max) { - return false; - } - } - - // Price change filter - if (criteria.priceChange24h) { - const outcome = market[criteria.priceChange24h.outcome]; - if (!outcome || outcome.priceChange24h === undefined) return false; - - if (criteria.priceChange24h.min !== undefined && outcome.priceChange24h < criteria.priceChange24h.min) { - return false; - } - if (criteria.priceChange24h.max !== undefined && outcome.priceChange24h > criteria.priceChange24h.max) { - return false; - } - } - - return true; - }); - } - - /** - * Filter events based on criteria or custom function. - * - * @param events - Array of events to filter - * @param criteria - Filter criteria object, string (simple text search), or predicate function - * @returns Filtered array of events - * - * @example Simple text search - * api.filterEvents(events, 'Trump') - * - * @example Advanced filtering - * api.filterEvents(events, { - * text: 'Election', - * searchIn: ['title', 'tags'], - * category: 'Politics', - * marketCount: { min: 5 } - * }) - * - * @example Custom predicate - * api.filterEvents(events, e => e.markets.length > 10) - */ - filterEvents( - events: UnifiedEvent[], - criteria: string | EventFilterCriteria | EventFilterFunction - ): UnifiedEvent[] { - // Handle predicate function - if (typeof criteria === 'function') { - return events.filter(criteria); - } - - // Handle simple string search - if (typeof criteria === 'string') { - const lowerQuery = criteria.toLowerCase(); - return events.filter(e => - e.title.toLowerCase().includes(lowerQuery) - ); - } - - // Handle criteria object - return events.filter(event => { - // Text search - if (criteria.text) { - const lowerQuery = criteria.text.toLowerCase(); - const searchIn = criteria.searchIn || ['title']; - let textMatch = false; - - for (const field of searchIn) { - if (field === 'title' && event.title?.toLowerCase().includes(lowerQuery)) { - textMatch = true; - break; - } - if (field === 'description' && event.description?.toLowerCase().includes(lowerQuery)) { - textMatch = true; - break; - } - if (field === 'category' && event.category?.toLowerCase().includes(lowerQuery)) { - textMatch = true; - break; - } - if (field === 'tags' && event.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))) { - textMatch = true; - break; - } - } - - if (!textMatch) return false; - } - - // Category filter - if (criteria.category && event.category !== criteria.category) { - return false; - } - - // Tags filter (match ANY of the provided tags) - if (criteria.tags && criteria.tags.length > 0) { - const hasMatchingTag = criteria.tags.some(tag => - event.tags?.some(eventTag => - eventTag.toLowerCase() === tag.toLowerCase() - ) - ); - if (!hasMatchingTag) return false; - } - - // Market count filter - if (criteria.marketCount) { - const count = event.markets.length; - if (criteria.marketCount.min !== undefined && count < criteria.marketCount.min) { - return false; - } - if (criteria.marketCount.max !== undefined && count > criteria.marketCount.max) { - return false; - } - } - - // Total volume filter - if (criteria.totalVolume) { - const totalVolume = event.markets.reduce((sum, m) => sum + m.volume24h, 0); - if (criteria.totalVolume.min !== undefined && totalVolume < criteria.totalVolume.min) { - return false; - } - if (criteria.totalVolume.max !== undefined && totalVolume > criteria.totalVolume.max) { - return false; - } - } - - return true; - }); - } -} - -/** - * Polymarket exchange client. - * - * @example - * ```typescript - * // Public data (no auth) - * const poly = new Polymarket(); - * const markets = await poly.fetchMarkets({ query: "Trump" }); - * - * // Trading (requires auth) - * const poly = new Polymarket({ privateKey: process.env.POLYMARKET_PRIVATE_KEY }); - * const balance = await poly.fetchBalance(); - * ``` - */ -/** - * Options for initializing Polymarket client. - */ -export interface PolymarketOptions { - /** Venue-specific API key (e.g. Polymarket CLOB key). Optional. */ - apiKey?: string; - - /** Private key for authentication (optional) */ - privateKey?: string; - - /** Hosted pmxt API key. Enables hosted mode when set. */ - pmxtApiKey?: string; - - /** Base URL of the PMXT sidecar server */ - baseUrl?: string; - - /** Automatically start server if not running (default: true) */ - autoStartServer?: boolean; - - /** Optional Polymarket Proxy/Smart Wallet address */ - proxyAddress?: string; - - /** Optional signature type */ - signatureType?: 'eoa' | 'poly-proxy' | 'gnosis-safe' | number; - - /** - * EVM wallet address used for hosted reads/writes. - */ - walletAddress?: string; - - /** - * External signer used for hosted writes. - */ - signer?: Signer; -} - -export class Polymarket extends Exchange { - constructor(options: PolymarketOptions = {}) { - // Default to gnosis-safe signature type - const polyOptions = { - signatureType: 'gnosis-safe', - ...options - }; - super("polymarket", polyOptions as ExchangeOptions); - } - - /** - * Initialize Polymarket L2 API credentials for implicit API signing. - * - * Call this before private Polymarket implicit-API endpoints when the - * underlying CLOB credentials have not been created yet. - */ - async initAuth(): Promise { - await this.initPromise; - try { - const json = await this.sidecarPostRequest('initAuth', []); - this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to initAuth: ${error}`); - } - } -} - -/** - * Kalshi exchange client. - * - * @example - * ```typescript - * // Public data (no auth) - * const kalshi = new Kalshi(); - * const markets = await kalshi.fetchMarkets({ query: "Fed rates" }); - * - * // Trading (requires auth) - * const kalshi = new Kalshi({ - * apiKey: process.env.KALSHI_API_KEY, - * privateKey: process.env.KALSHI_PRIVATE_KEY - * }); - * const balance = await kalshi.fetchBalance(); - * ``` - */ -export class Kalshi extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("kalshi", options); - } -} - -/** - * Limitless exchange client. - * - * @example - * ```typescript - * // Public data (no auth) - * const limitless = new Limitless(); - * const markets = await limitless.fetchMarkets({ query: "Trump" }); - * - * // Trading (requires auth) - * const limitless = new Limitless({ - * apiKey: process.env.LIMITLESS_API_KEY, - * privateKey: process.env.LIMITLESS_PRIVATE_KEY - * }); - * const balance = await limitless.fetchBalance(); - * ``` - */ -export class Limitless extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("limitless", options); - } - - /** - * Watch real-time AMM price updates via WebSocket. - * - * @param marketAddress - Limitless market contract address - * @param callback - Optional callback invoked with the returned update - * @returns Next price update - */ - async watchPrices( - marketAddress: string, - callback?: (data: Record) => void, - ): Promise> { - await this.initPromise; - try { - const json = await this.sidecarPostRequest("watchPrices", [marketAddress]); - const data = this.handleResponse(json); - callback?.(data); - return data; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to watch prices: ${error}`); - } - } - - /** - * Watch real-time Limitless user position updates. - * - * Requires API key authentication. - * - * @param callback - Optional callback invoked with the returned positions - * @returns Next position update payload - */ - async watchUserPositions( - callback?: (data: Position[]) => void, - ): Promise { - await this.initPromise; - try { - const json = await this.sidecarPostRequest("watchUserPositions", []); - const data = this.handleResponse(json).map(convertPosition); - callback?.(data); - return data; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to watch user positions: ${error}`); - } - } - - /** - * Watch real-time Limitless user transaction updates. - * - * Requires API key authentication. - * - * @param callback - Optional callback invoked with the returned update - * @returns Next transaction update - */ - async watchUserTransactions( - callback?: (data: Record) => void, - ): Promise> { - await this.initPromise; - try { - const json = await this.sidecarPostRequest("watchUserTransactions", []); - const data = this.handleResponse(json); - callback?.(data); - return data; - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to watch user transactions: ${error}`); - } - } -} - -/** - * Kalshi Demo exchange client (paper trading / sandbox environment). - * - * Uses Kalshi's demo environment — same API as Kalshi but against test accounts. - * Credentials are separate from production Kalshi credentials. - * - * @example - * ```typescript - * const kalshiDemo = new KalshiDemo({ - * apiKey: process.env.KALSHI_DEMO_API_KEY, - * privateKey: process.env.KALSHI_DEMO_PRIVATE_KEY - * }); - * const balance = await kalshiDemo.fetchBalance(); - * ``` - */ -export class KalshiDemo extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("kalshi-demo", options); - } -} - -/** - * Myriad exchange client. - * - * AMM-based prediction market exchange. Requires an API key for trading. - * The `privateKey` field is used as the wallet address. - * - * @example - * ```typescript - * // Public data (no auth) - * const myriad = new Myriad(); - * const markets = await myriad.fetchMarkets(); - * - * // Trading (requires auth) - * const myriad = new Myriad({ - * apiKey: process.env.MYRIAD_API_KEY, - * privateKey: process.env.MYRIAD_WALLET_ADDRESS - * }); - * ``` - */ -export class Myriad extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("myriad", options); - } -} - -/** - * Probable exchange client. - * - * BSC-based CLOB exchange. Requires all four credential fields for trading. - * - * @example - * ```typescript - * // Public data (no auth) - * const probable = new Probable(); - * const markets = await probable.fetchMarkets(); - * - * // Trading (requires auth) - * const probable = new Probable({ - * privateKey: process.env.PROBABLE_PRIVATE_KEY, - * apiKey: process.env.PROBABLE_API_KEY, - * apiSecret: process.env.PROBABLE_API_SECRET, - * passphrase: process.env.PROBABLE_PASSPHRASE - * }); - * ``` - */ -export class Probable extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("probable", options); - } -} - -/** - * Baozi exchange client. - * - * Solana-based on-chain pari-mutuel betting exchange. - * Requires a Solana private key for trading. - * - * @example - * ```typescript - * // Public data (no auth) - * const baozi = new Baozi(); - * const markets = await baozi.fetchMarkets(); - * - * // Trading (requires auth) - * const baozi = new Baozi({ - * privateKey: process.env.BAOZI_PRIVATE_KEY - * }); - * ``` - */ -export class Baozi extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("baozi", options); - } -} - -/** - * Opinion exchange client. - * - * Polygon-based CLOB exchange. Public catalog endpoints work without - * credentials; trading requires `apiKey` (proxy address) and `privateKey`. - * - * @example - * ```typescript - * const opinion = new Opinion(); - * const events = await opinion.fetchEvents(); - * ``` - */ -export class Opinion extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("opinion", options); - } -} - -/** - * Metaculus exchange client. - * - * Forecasting platform. Public read-only access works without credentials; - * authenticated calls accept a bearer token via `apiKey`. - * - * @example - * ```typescript - * const metaculus = new Metaculus(); - * const events = await metaculus.fetchEvents(); - * ``` - */ -export class Metaculus extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("metaculus", options); - } -} - -/** - * Smarkets exchange client. - * - * UK-based betting exchange. Public catalog endpoints work without - * credentials; trading requires Smarkets account email (`apiKey`) and - * password (`privateKey`). - * - * @example - * ```typescript - * const smarkets = new Smarkets(); - * const events = await smarkets.fetchEvents(); - * ``` - */ -export class Smarkets extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("smarkets", options); - } -} - -/** - * Polymarket US exchange client. - * - * US-regulated Polymarket venue. Public catalog endpoints work without - * credentials; trading requires `apiKey` (keyId) and `privateKey` - * (secretKey) issued by Polymarket US. - * - * @example - * ```typescript - * const polyUs = new PolymarketUS(); - * const events = await polyUs.fetchEvents(); - * ``` - */ -export class PolymarketUS extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("polymarket_us", options); - } -} - -/** - * Gemini Titan exchange client. - * - * @example - * ```typescript - * const titan = new GeminiTitan(); - * const markets = await titan.fetchMarkets(); - * ``` - */ -export class GeminiTitan extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("gemini-titan", options); - } -} - -/** - * Hyperliquid exchange client. - * - * @example - * ```typescript - * const hl = new Hyperliquid(); - * const markets = await hl.fetchMarkets(); - * ``` - */ -export class Hyperliquid extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("hyperliquid", options); - } -} - -/** - * Options for the SuiBets exchange client. - */ -export interface SuiBetsOptions extends ExchangeOptions { - /** - * Sui wallet address (0x + 64 hex chars). - * Required for fetchPositions(). Can also be set via the - * SUIBETS_WALLET_ADDRESS environment variable on the sidecar. - */ - walletAddress?: string; -} - -/** - * SuiBets exchange client. - * - * SuiBets is a decentralised P2P sports betting exchange on Sui mainnet. - * No house edge. 2% platform fee. - * Contract: 0xd51fe151bec66a15b086a67c1cfce9b05759ddac1d73fcd3e14324ad202b2e59 - * - * @example - * ```typescript - * const suibets = new SuiBets(); - * const markets = await suibets.fetchMarkets({ limit: 20 }); - * - * // With wallet for fetchPositions() - * const me = new SuiBets({ walletAddress: '0xabc...' }); - * const positions = await me.fetchPositions(); - * ``` - */ -export class SuiBets extends Exchange { - private readonly _walletAddress?: string; - - constructor(options: SuiBetsOptions = {}) { - super("suibets", options); - this._walletAddress = options.walletAddress; - } - - /** - * Includes walletAddress in the credentials sent to the sidecar so - * that fetchPositions() can reach the /api/p2p/my endpoint. - * Falls back to SUIBETS_WALLET_ADDRESS env var on the sidecar side - * when walletAddress is not set here. - */ - protected override getCredentials(): ExchangeCredentials | undefined { - const base = super.getCredentials(); - if (!this._walletAddress) return base; - return { - ...(base ?? {}), - walletAddress: this._walletAddress, - } as ExchangeCredentials & { walletAddress: string }; - } -} - -// Backwards-compatible casing alias matching the Python SDK export. -export const Suibets = SuiBets; - -/** - * Rain exchange client. - * - * Rain is a permissionless AMM-plus-orderbook prediction market on Arbitrum One. - * Reads are unauthenticated; trading requires an EVM privateKey. - * - * @example - * ```typescript - * const rain = new Rain(); - * const markets = await rain.fetchMarkets({ limit: 20 }); - * - * // With wallet for trading - * const me = new Rain({ privateKey: '0x...' }); - * await me.createOrder({ marketId, outcomeId, side: 'buy', type: 'market', amount: 10 }); - * ``` - */ -export class Rain extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("rain", options); - } -} - - -/** - * Hunch exchange client. - * - * Hunch is a crypto-native prediction market. Reads are unauthenticated; - * trading requires an EVM private key. - */ -export class Hunch extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("hunch", options); - } -} - -/** - * Mock exchange client. - * - * Offline deterministic exchange for testing and development. - * No credentials required. - * - * @example - * ```typescript - * const mock = new Mock(); - * const markets = await mock.fetchMarkets(); - * ``` - */ -export class Mock extends Exchange { - constructor(options: ExchangeOptions = {}) { - super("mock", options); - } -} +/** + * Exchange client implementations. + * + * This module provides clean, TypeScript-friendly wrappers around the auto-generated + * OpenAPI client, matching the Python API exactly. + */ + +import { + Configuration, + CreateOrderRequest, + DefaultApi, + ExchangeCredentials, + BuildOrderRequest, + SubmitOrderRequest, +} from "../generated/src/index.js"; + +import { + Balance, + BuiltOrder, + CreateOrderInput, + EventFetchParams, + EventFilterCriteria, + EventFilterFunction, + ExecutionPriceResult, + FetchOrderBookParams, + MarketFetchParams, + MarketFilterCriteria, + MarketFilterFunction, + MarketList, + MarketOutcome, + MyTradesParams, + Order, + OrderBook, + OrderHistoryParams, + OrderLevel, + PaginatedMarketsResult, + PaginatedEventsResult, + Position, + SeriesFetchParams, + PriceCandle, + SubscribedAddressSnapshot, + SubscriptionOption, + Trade, + UnifiedEvent, + UnifiedMarket, + UnifiedSeries, + UserTrade, + FirehoseEvent, + FetchMatchedMarketClustersParams, +} from "./models.js"; + +import { ServerManager } from "./server-manager.js"; +import { buildArgsWithOptionalOptions } from "./args.js"; +import { PmxtError, fromServerError, InvalidOrder, NotSupported } from "./errors.js"; +import { ENV, LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js"; +import { SidecarWsClient } from "./ws-client.js"; +import { logger } from "./logger.js"; + +// Hosted-mode trading dispatch. +// These modules are introduced as part of the hosted trading mode rollout. +// Some of them may be authored by parallel agents; until they all land, the +// import names below are the conventional ones from the plan. Cross-module +// "Cannot find module" errors during the parallel landing window resolve +// once the matching files exist. +import { + HOSTED_TRADING_VENUES, + _tradingRequest, + resolveWalletAddress, + ensureHostedTradingSupported, + formatRoutePath, + HOSTED_METHOD_ROUTES, +} from "./hosted-routing.js"; +import { + orderFromV0, + positionFromV0, + balanceFromV0, + userTradeFromV0, + to6dec, +} from "./hosted-mappers.js"; +import { + validateTypedData, + validateEconomics, + verifySignature, +} from "./hosted-typed-data.js"; +import type { HostedRoute } from "./hosted-typed-data.js"; +import type { Signer, TypedData } from "./signers.js"; +import { signerFromPrivateKey, EthersSigner } from "./signers.js"; +import { Escrow } from "./escrow.js"; +import { + MissingWalletAddress, + InvalidSignature as HostedInvalidSignature, +} from "./hosted-errors.js"; + +interface RawWebSocketLike { + send(data: string): void; +} + +interface SidecarWsClientInternals { + ensureConnected(): Promise; + ws: RawWebSocketLike | null; + activeSubs: Map; + subscriptions: Map void) | null }>; + dataQueues: Map; + dataStore: Map; +} + +/** + * Resolve a MarketOutcome shorthand to a plain outcome ID string. + * Accepts either a raw string ID or a MarketOutcome object. + */ +function resolveOutcomeId(input: string | MarketOutcome): string { + if (typeof input === 'string') return input; + return input.outcomeId; +} + +/** + * Build a URL-encoded query string from a plain record. + * + * - `undefined` / `null` values are skipped (they shouldn't appear in the URL). + * - Arrays are serialised as repeated `key=v1&key=v2` pairs. + * - Nested objects are skipped here; callers should route such queries through + * POST instead (see `queryHasNestedObject`). + */ +function buildSidecarQueryString(query: Record): string { + const parts: string[] = []; + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null) continue; + if (Array.isArray(value)) { + for (const v of value) { + if (v === undefined || v === null) continue; + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`); + } + } else if (typeof value === 'object') { + // Nested objects don't round-trip through query strings. Caller + // should have detected this and POSTed instead. + continue; + } else { + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + } + return parts.join('&'); +} + +/** + * True if any top-level value in the query is a nested object (not an array). + * Such queries can't be safely expressed in a query string, so we fall back + * to POST to preserve the original argument shape. + */ +function queryHasNestedObject(query: Record): boolean { + for (const value of Object.values(query)) { + if (value === undefined || value === null) continue; + if (typeof value === 'object' && !Array.isArray(value)) return true; + } + return false; +} + +// Converter functions +function convertMarket(raw: any): UnifiedMarket { + const market: UnifiedMarket = { + ...raw, + resolutionDate: raw.resolutionDate ? new Date(raw.resolutionDate) : undefined, + outcomes: (raw.outcomes || []).map((o: any) => ({ ...o })), + yes: raw.yes ? { ...raw.yes } : undefined, + no: raw.no ? { ...raw.no } : undefined, + up: raw.up ? { ...raw.up } : undefined, + down: raw.down ? { ...raw.down } : undefined, + }; + Object.defineProperty(market, 'question', { + get() { return this.title; }, + enumerable: false, + configurable: true, + }); + return market; +} + + +function convertCandle(raw: any): PriceCandle { + return { ...raw }; +} + +function convertOrderBook(raw: any): OrderBook { + return { + ...raw, + bids: (raw.bids || []).map((b: any) => ({ ...b })), + asks: (raw.asks || []).map((a: any) => ({ ...a })), + }; +} + +function convertTrade(raw: any): Trade { + return { ...raw, side: raw.side || "unknown" }; +} + +function convertOrder(raw: any): Order { + return { ...raw }; +} + +function convertPosition(raw: any): Position { + return { ...raw }; +} + +function convertBalance(raw: any): Balance { + return { ...raw }; +} + +function convertUserTrade(raw: any): UserTrade { + return { ...raw, side: raw.side || "unknown" }; +} + +function convertEvent(raw: any): UnifiedEvent { + const markets = MarketList.from((raw.markets || []).map(convertMarket)) as MarketList; + return { ...raw, markets }; +} + +function convertSeries(raw: any): UnifiedSeries { + const events = Array.isArray(raw.events) ? raw.events.map(convertEvent) : undefined; + return { ...raw, ...(events !== undefined ? { events } : {}) }; +} + +function convertSubscriptionSnapshot(raw: any): SubscribedAddressSnapshot { + return { + ...raw, + trades: (raw.trades ?? []).map(convertTrade), + balances: (raw.balances ?? []).map(convertBalance), + positions: (raw.positions ?? []).map(convertPosition), + }; +} + +/** + * Base exchange client options. + */ +export interface ExchangeOptions { + /** Venue-specific API key (e.g. Polymarket CLOB key). Optional. */ + apiKey?: string; + + /** Venue-specific API secret for HMAC/CLOB-authenticated exchanges. Optional. */ + apiSecret?: string; + + /** Venue-specific private key. Optional. */ + privateKey?: string; + + /** + * Hosted pmxt API key. + * + * When set (either as this kwarg or via the `PMXT_API_KEY` env + * variable), and no explicit `baseUrl` / `PMXT_BASE_URL` is set, + * the Exchange will default to the hosted pmxt endpoint + * (`https://api.pmxt.dev`) instead of the local sidecar, and send + * `Authorization: Bearer ` on every request. + * + * The local sidecar ignores this header, so it is safe to set in + * both local and hosted modes. + */ + pmxtApiKey?: string; + + /** + * Base URL of the pmxt server. + * + * Resolution precedence: + * 1. Explicit `baseUrl` kwarg. + * 2. `PMXT_BASE_URL` environment variable. + * 3. `HOSTED_URL` when `pmxtApiKey` (kwarg or env) is present. + * 4. Local sidecar (`http://localhost:3847`). + */ + baseUrl?: string; + + /** + * Automatically start the local sidecar if it is not running. + * + * Default: `true` when the resolved base URL is the local sidecar, + * `false` otherwise. Explicit `true` / `false` always wins. + */ + autoStartServer?: boolean; + + /** Optional Polymarket Proxy/Smart Wallet address */ + proxyAddress?: string; + + /** Optional signature type (0=EOA, 1=Proxy) */ + signatureType?: number; + + /** + * EVM wallet address used for hosted reads/writes. Required for hosted + * endpoints that operate on a wallet (balances, positions, trades, open + * orders). When omitted, hosted reads raise {@link MissingWalletAddress}. + */ + walletAddress?: string; + + /** + * External signer used for hosted writes. When `privateKey` is supplied + * without `signer` in hosted mode, an internal {@link EthersSigner} is + * built from it lazily. + */ + signer?: Signer; +} + +/** + * Base class for prediction market exchanges. + * + * This provides a unified interface for interacting with different + * prediction market platforms (Polymarket, Kalshi, etc.). + */ +export abstract class Exchange { + private static readonly OBDATA_WATCH_ALL_SOURCES = new Set([ + "polymarket", + "limitless", + "kalshi", + "opinion", + ]); + + // Match a canonical 8-4-4-4-12 UUID string. The hosted catalog emits + // UUIDs in this exact shape, so a regex is both faster and stricter + // than `crypto.randomUUID()`-style parsing. + private static readonly _UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + /** True iff `value` parses as a canonical catalog UUID string. */ + protected static _isCatalogUuid(value: string): boolean { + return Exchange._UUID_RE.test(value); + } + + // Public so structural interfaces like `HostedClientLike` + // (./hosted-routing) can read the venue name and hosted credentials + // without violating protected-access on this base class. + public exchangeName: string; + public pmxtApiKey?: string; + protected apiKey?: string; + protected apiSecret?: string; + protected privateKey?: string; + protected proxyAddress?: string; + protected signatureType?: number; + protected api: DefaultApi; + protected config: Configuration; + protected serverManager: ServerManager; + protected initPromise: Promise; + protected isHosted: boolean; + + /** Wallet address used for hosted endpoints that operate on a wallet. */ + public walletAddress?: string; + + /** External signer used for hosted writes. */ + public signer?: Signer; + + /** Escrow namespace — populated in hosted mode for trading-allowlisted venues. */ + public escrow?: Escrow; + private _hostedAccount?: { depositWallet?: string; signatureType?: number }; + private _accountDiscoveryPromise?: Promise; + + /** + * Sticky flag: set to `true` the first time a GET read is rejected by + * the sidecar with 404/405 (i.e. an older pmxt-core that only supports + * POST). While false, read methods try GET first; once flipped they + * POST directly and skip the GET probe for the lifetime of this client. + */ + private _getReadsUnsupported: boolean = false; + + /** Shared WebSocket client for streaming methods (lazy). */ + private _wsClient: SidecarWsClient | null = null; + /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */ + private _wsUnsupported: boolean = false; + private readonly _useSidecarLockBaseUrl: boolean; + + constructor(exchangeName: string, options: ExchangeOptions = {}) { + this.exchangeName = exchangeName.toLowerCase(); + this.apiKey = options.apiKey; + this.apiSecret = options.apiSecret; + this.privateKey = options.privateKey; + this.proxyAddress = options.proxyAddress; + this.signatureType = options.signatureType; + this.walletAddress = options.walletAddress; + this.signer = options.signer; + + // Resolve base URL + hosted API key via the shared precedence + // rules. See constants.ts for the full resolution table. + const resolved = resolvePmxtBaseUrl({ + baseUrl: options.baseUrl, + pmxtApiKey: options.pmxtApiKey, + }); + const baseUrl = resolved.baseUrl; + this.pmxtApiKey = resolved.pmxtApiKey; + this.isHosted = resolved.isHosted; + const hasBaseUrlOverride = Boolean( + options.baseUrl || + (typeof process !== "undefined" && process.env[ENV.BASE_URL]), + ); + this._useSidecarLockBaseUrl = !this.isHosted && !hasBaseUrlOverride; + + // Hosted trading bridge: if the caller passed a privateKey but no + // explicit signer, lazily wrap it in an EthersSigner so that + // `pmxt.Polymarket(pmxtApiKey, privateKey)` just works without the + // user touching `signer`. EthersSigner's constructor synchronously + // builds an ethers.Wallet, so this remains a synchronous bridge — + // the constructor stays sync. + if (this.pmxtApiKey && this.privateKey && !this.signer) { + try { + this.signer = new EthersSigner(this.privateKey); + } catch { + // ethers not installed — defer the error to the first + // hosted write that actually needs the signer. Read-only + // hosted callers don't need ethers. + } + } + + // Instantiate Escrow namespace for hosted-trading-allowlisted venues. + if (this.pmxtApiKey && HOSTED_TRADING_VENUES.has(this.exchangeName)) { + this.escrow = new Escrow(this); + } + + // auto_start_server defaults: true for local, false for hosted. + // An explicit value in the options always wins. + const autoStartServer = options.autoStartServer !== undefined + ? options.autoStartServer + : !this.isHosted; + + // Initialize server manager (no network calls happen here — the + // constructor just stores config). + this.serverManager = new ServerManager({ baseUrl }); + + // Configure the API client with the initial base URL (will be + // updated to the actual listen port if the local sidecar gets + // bumped off the default). + this.config = new Configuration({ basePath: baseUrl }); + this.api = new DefaultApi(this.config); + + // Initialize the server connection asynchronously + this.initPromise = this.initializeServer(autoStartServer); + } + + private async initializeServer(autoStartServer: boolean): Promise { + if (autoStartServer) { + try { + await this.serverManager.ensureServerRunning(); + + if (this._useSidecarLockBaseUrl) { + // Get the actual port the server is running on + // (may differ from default if default port was busy) + const actualPort = this.serverManager.getRunningPort(); + const newBaseUrl = `http://localhost:${actualPort}`; + + // Update API client with actual base URL + this.config = new Configuration({ + basePath: newBaseUrl, + }); + this.api = new DefaultApi(this.config); + } + } catch (error) { + const msg = + `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` + + `Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n` + + `Or start the server manually: pmxt-server`; + const pmxtError = new PmxtError(msg); + if (error instanceof Error) { + (pmxtError as any).cause = error; + } + throw pmxtError; + } + } + } + + protected handleResponse(response: any): any { + if (!response.success) { + const error = response.error || {}; + if (error && typeof error === "object" && (error.code || error.message)) { + throw fromServerError(error); + } + throw new PmxtError(error.message || "Unknown error"); + } + return response.data; + } + + protected getCredentials(): ExchangeCredentials | undefined { + if (!this.apiKey && !this.apiSecret && !this.privateKey) { + return undefined; + } + return { + apiKey: this.apiKey, + apiSecret: this.apiSecret, + privateKey: this.privateKey, + funderAddress: this.proxyAddress, + signatureType: this.signatureType, + }; + } + + protected getAuthHeaders(): Record { + const headers: Record = { ...(this.config.headers as Record) }; + + // Local sidecar access token (read from the lock file). Only + // meaningful when talking to a local sidecar we spawned + // ourselves; harmless elsewhere. + const accessToken = this.serverManager.getAccessToken(); + if (accessToken) { + headers['x-pmxt-access-token'] = accessToken; + } + + // Hosted pmxt bearer token. The hosted service requires this; + // the local sidecar ignores it. Safe to attach unconditionally + // whenever a pmxtApiKey has been resolved. + if (this.pmxtApiKey) { + headers['Authorization'] = `Bearer ${this.pmxtApiKey}`; + } + + return headers; + } + + /** + * Returns true when this client should dispatch trading methods through + * the hosted PMXT trading API (`pmxtApiKey` set AND venue is on the + * hosted-trading allowlist). Used to gate every Group A method. + */ + protected isHostedTradingMode(): boolean { + return Boolean(this.pmxtApiKey) && HOSTED_TRADING_VENUES.has(this.exchangeName); + } + + /** + * Require a configured signer for a hosted write. Returns the signer or + * throws {@link MissingWalletAddress} (consistent with the error class + * surfaced for missing wallet wiring on hosted writes). + */ + protected requireHostedSigner(): Signer { + if (!this.signer) { + throw new MissingWalletAddress( + "hosted write requires a signer (pass `signer` or `privateKey`)", + ); + } + return this.signer; + } + + /** + * Resolve the current sidecar base URL. + * + * Hosted mode and explicit baseUrl clients keep the configured basePath. + * Default local clients re-read the lock-file port on every call so they + * pick up sidecar restarts that land on a different port. + */ + private resolveBaseUrl(): string { + if (this.isHosted || !this._useSidecarLockBaseUrl) return this.config.basePath; + const port = this.serverManager.getRunningPort(); + return `http://localhost:${port}`; + } + + /** + * Execute a fetch with retry on connection failures. + * + * Only retries on connection-level errors (ECONNREFUSED, ECONNRESET) — + * never on HTTP responses (4xx, 5xx). On first connection failure, + * attempts to restart the sidecar. + */ + private async fetchWithRetry( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise { + const delays = [200, 500, 1000]; + let lastError: unknown; + + for (let attempt = 0; attempt <= delays.length; attempt++) { + try { + return await fetch(input, { + ...init, + signal: AbortSignal.timeout(30_000), + }); + } catch (error) { + lastError = error; + if (attempt >= delays.length) break; + + // Connection failed — try to restart the sidecar on first failure + if (attempt === 0 && !this.isHosted) { + try { + await this.serverManager.ensureServerRunning(); + } catch (err) { + logger.warn('PmxtClient: server restart failed during retry', { attempt, error: String(err) }); + } + } + await new Promise(resolve => setTimeout(resolve, delays[attempt])); + } + } + throw lastError; + } + + /** + * Return the shared WebSocket client, creating it on first use. + * + * Returns `null` if the sidecar /ws endpoint was previously found + * to be unavailable. + */ + private async getOrCreateWs(): Promise { + if (this._wsUnsupported) return null; + if (this._wsClient?.connected) return this._wsClient; + + const host = this.resolveBaseUrl(); + const accessToken = this.isHosted + ? this.pmxtApiKey + : this.serverManager.getAccessToken(); + const authParamName = this.isHosted ? "apiKey" : "token"; + + const client = new SidecarWsClient(host, accessToken || undefined, authParamName); + try { + // Trigger connection to validate the endpoint exists. + // subscribe() calls ensureConnected internally, but we want + // to detect failure eagerly so we can set _wsUnsupported. + await client.subscribe( + this.exchangeName, + "_ping", + [], + undefined, + 3000, + ).catch(() => { + // Expected -- no _ping method. The connection itself + // succeeded if we got a WS error frame back. If the + // connection itself failed, we'll catch below. + }); + // If we got here without the connect promise rejecting, + // the WS endpoint exists. + if (!client.connected) { + throw new Error("WS handshake failed"); + } + } catch (err) { + logger.warn('PmxtClient: WebSocket probe failed, falling back to HTTP', { error: String(err) }); + this._wsUnsupported = true; + client.close(); + return null; + } + + this._wsClient = client; + return this._wsClient; + } + + /** + * Attempt to use the WS transport for a watch method. + * Returns the raw data on success, or `null` if WS is unavailable. + */ + private async watchViaWs( + method: string, + args: any[], + ): Promise { + const ws = await this.getOrCreateWs(); + if (!ws) return null; + + try { + return await ws.subscribe( + this.exchangeName, + method, + args, + this.getCredentials() as Record | undefined, + ); + } catch (error) { + if (this.isWsTransportUnavailableError(error)) { + return null; + } + throw error; + } + } + + private wsTransportUnavailableError(method: string): PmxtError { + return new PmxtError(`${method}() requires WebSocket transport — connection failed`); + } + + private isWsTransportUnavailableError(error: unknown): boolean { + return error instanceof PmxtError + && /connection failed|no websocket|websocket.*not connected/i.test(error.message); + } + + private defaultWatchAllOrderBookVenues(): string[] | undefined { + if (Exchange.OBDATA_WATCH_ALL_SOURCES.has(this.exchangeName)) { + return [this.exchangeName]; + } + return undefined; + } + + private getWsInternals(ws: SidecarWsClient): SidecarWsClientInternals { + return ws as unknown as SidecarWsClientInternals; + } + + private wsSubscriptionKey(method: string, args: any[]): string { + const firstArg = args[0] ?? ""; + return Array.isArray(firstArg) + ? `${method}:${[...firstArg].sort().join(",")}` + : `${method}:${firstArg}`; + } + + private getWsSubscriptionId(ws: SidecarWsClient, method: string, args: any[]): string | undefined { + const internals = this.getWsInternals(ws); + const subKey = this.wsSubscriptionKey(method, args); + return internals.activeSubs.get(subKey); + } + + private clearWsSubscription(ws: SidecarWsClient, method: string, args: any[]): void { + const internals = this.getWsInternals(ws); + const subKey = this.wsSubscriptionKey(method, args); + const requestId = internals.activeSubs.get(subKey); + if (!requestId) return; + + const sub = internals.subscriptions.get(requestId); + if (sub?.reject) { + sub.reject(new PmxtError(`${method} subscription cancelled`)); + } + + internals.activeSubs.delete(subKey); + internals.subscriptions.delete(requestId); + internals.dataQueues.delete(requestId); + internals.dataStore.delete(requestId); + + const firstArg = args[0] ?? ""; + const symbols = Array.isArray(firstArg) + ? firstArg.map(String) + : firstArg + ? [String(firstArg)] + : []; + for (const symbol of symbols) { + internals.dataStore.delete(`${requestId}:${symbol}`); + } + } + + private async sendWsMessage( + ws: SidecarWsClient, + message: Record, + ): Promise { + const internals = this.getWsInternals(ws); + await internals.ensureConnected(); + + const socket = internals.ws; + if (!socket) { + throw new PmxtError('[ws-client] Cannot send: WebSocket not connected'); + } + socket.send(JSON.stringify(message)); + } + + // Low-Level API Access + + /** + * Call an exchange-specific REST endpoint by its operationId. + * This provides direct access to all implicit API methods defined in + * the exchange's OpenAPI spec (e.g., Polymarket CLOB, Kalshi trading API). + * + * @param operationId - The operationId (or auto-generated name) of the endpoint + * @param params - Optional parameters to pass to the endpoint + * @returns The raw response data from the exchange + * + * @example + * ```typescript + * // Call a Polymarket CLOB endpoint directly + * const result = await poly.callApi('getMarket', { condition_id: '0x...' }); + * ``` + */ + async callApi(operationId: string, params?: Record): Promise { + await this.initPromise; + try { + const url = `${this.resolveBaseUrl()}/api/${this.exchangeName}/callApi`; + + const requestBody: any = { + args: [operationId, params], + credentials: this.getCredentials() + }; + + const response = await this.fetchWithRetry(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.getAuthHeaders() + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to call API '${operationId}': ${error}`); + } + } + + /** + * Dispatch a sidecar read method, preferring GET but transparently + * falling back to POST for full backward compatibility. + * + * GET is used when: + * - the client has no per-instance credentials (the sidecar's GET + * handler intentionally drops credentials to avoid leaking them + * through query strings and access logs), and + * - the sidecar hasn't already returned 404/405 for a previous GET + * in this client's lifetime (`_getReadsUnsupported`), and + * - the query has no nested objects (query strings can't round-trip + * arbitrary JSON). + * + * Otherwise (or if the GET attempt is rejected with 404/405) the call + * is sent as POST with the original `{args, credentials}` body so that + * SDK users talking to an older pmxt-core continue to work unchanged. + * + * @internal — shared transport used by every generated read method. + */ + protected async sidecarReadRequest( + methodName: string, + query: Record, + args: unknown[], + ): Promise { + const resolvedBase = this.resolveBaseUrl(); + const baseUrl = `${resolvedBase}/api/${this.exchangeName}/${methodName}`; + const hasCredentials = this.getCredentials() !== undefined; + + if (!hasCredentials && !this._getReadsUnsupported && !queryHasNestedObject(query)) { + const qs = buildSidecarQueryString(query); + const getUrl = qs ? `${baseUrl}?${qs}` : baseUrl; + const response = await this.fetchWithRetry(getUrl, { + method: 'GET', + headers: this.getAuthHeaders(), + }); + + // 404 / 405 => older sidecar without GET dispatch. Remember + // the downgrade so future calls skip the probe, and fall + // through to POST below. + if (response.status === 404 || response.status === 405) { + await response.text().catch(() => undefined); + this._getReadsUnsupported = true; + } else { + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + return response.json(); + } + } + + // POST fallback — identical to the original per-method template. + const response = await this.fetchWithRetry(baseUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + return response.json(); + } + + /** + * Dispatch a sidecar POST method with positional args and credentials. + * + * @internal - shared transport for hand-maintained methods that should + * never use the GET read path. + */ + protected async sidecarPostRequest(methodName: string, args: unknown[]): Promise { + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/${methodName}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + return response.json(); + } + + /** + * Pre-warm the sidecar's internal caches for a market outcome. + * + * @param outcomeId - The CLOB token ID for the outcome. + */ + async preWarmMarket(outcomeId: string): Promise { + await this.initPromise; + try { + const json = await this.sidecarPostRequest("preWarmMarket", [outcomeId]); + this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to preWarmMarket: ${error}`); + } + } + + /** + * Fetch a single Probable event by its numeric ID. + * + * @param id - The numeric event ID. + * @returns The event, or null when the venue has no matching event. + */ + async getEventById(id: string): Promise { + await this.initPromise; + try { + const json = await this.sidecarPostRequest("getEventById", [id]); + const data = this.handleResponse(json); + return data == null ? null : convertEvent(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to getEventById: ${error}`); + } + } + + /** + * Fetch a single Probable event by its URL slug. + * + * @param slug - The event slug. + * @returns The event, or null when the venue has no matching event. + */ + async getEventBySlug(slug: string): Promise { + await this.initPromise; + try { + const json = await this.sidecarPostRequest("getEventBySlug", [slug]); + const data = this.handleResponse(json); + return data == null ? null : convertEvent(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to getEventBySlug: ${error}`); + } + } + + /** + * Read a hosted catalog endpoint directly. + * + * Hosted-only Router APIs such as matched clusters are not part of the + * core sidecar method namespace. They live under /v0 and return their own + * response envelopes, so callers intentionally receive the raw JSON body. + */ + protected async catalogReadRequest(path: string, query: Record = {}): Promise { + const qs = buildSidecarQueryString(query); + const url = `${this.resolveBaseUrl()}${path}${qs ? `?${qs}` : ''}`; + const response = await this.fetchWithRetry(url, { + method: 'GET', + headers: this.getAuthHeaders(), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + return response.json(); + } + + // BEGIN GENERATED METHODS + + async loadMarkets(reload: boolean = false): Promise> { + await this.initPromise; + try { + const args: any[] = []; + args.push(reload); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/loadMarkets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + const result: Record = {}; + for (const [key, value] of Object.entries(data as any)) { + result[key] = convertMarket(value); + } + return result; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to loadMarkets: ${error}`); + } + } + + async fetchMarkets(params?: MarketFetchParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarkets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertMarket); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMarkets: ${error}`); + } + } + + async fetchMarketsPaginated(params?: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarketsPaginated`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return { + data: (data.data || []).map(convertMarket), + total: data.total, + nextCursor: data.nextCursor, + }; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMarketsPaginated: ${error}`); + } + } + + async fetchEventsPaginated(params?: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEventsPaginated`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return { + data: (data.data || []).map(convertEvent), + total: data.total, + nextCursor: data.nextCursor, + }; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchEventsPaginated: ${error}`); + } + } + + async fetchEvents(params?: EventFetchParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEvents`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertEvent); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchEvents: ${error}`); + } + } + + async fetchSeries(params?: SeriesFetchParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchSeries`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertSeries); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchSeries: ${error}`); + } + } + + async fetchMarket(params?: MarketFetchParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarket`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertMarket(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMarket: ${error}`); + } + } + + async fetchEvent(params?: EventFetchParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEvent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertEvent(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchEvent: ${error}`); + } + } + + async fetchOrderBook(outcomeId: string | MarketOutcome, limit?: number, params?: FetchOrderBookParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(resolveOutcomeId(outcomeId)); + if (limit !== undefined) args.push(limit); + if (params !== undefined) { + if (limit === undefined) args.push(null); + args.push(params); + } + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOrderBook`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + if (Array.isArray(data)) { + return data.map(convertOrderBook); + } + return convertOrderBook(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchOrderBook: ${error}`); + } + } + + async fetchOrderBooks(outcomeIds: (string | MarketOutcome)[]): Promise> { + await this.initPromise; + try { + const args: any[] = []; + args.push(outcomeIds.map(resolveOutcomeId)); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOrderBooks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + const result: Record = {}; + for (const [key, value] of Object.entries(data as any)) { + result[key] = convertOrderBook(value); + } + return result; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchOrderBooks: ${error}`); + } + } + + async submitOrder(built: BuiltOrder): Promise { + await this.initPromise; + if (this.isHosted) { + throw new PmxtError("submitOrder is not available in hosted mode. Use createOrder instead."); + } + try { + const args: any[] = []; + args.push(built); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/submitOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertOrder(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to submitOrder: ${error}`); + } + } + + async cancelOrder(orderId: string): Promise { + await this.initPromise; + if (this.isHosted) { + return this._hostedCancelOrder(orderId); + } + try { + const args: any[] = []; + args.push(orderId); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/cancelOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertOrder(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to cancelOrder: ${error}`); + } + } + + async fetchOrder(orderId: string): Promise { + await this.initPromise; + if (this.isHosted) { + const route = HOSTED_METHOD_ROUTES.get("fetchOrder")!; + const path = formatRoutePath(route, { order_id: orderId }); + const data = await _tradingRequest(this, { method: route.method, path }); + const payload = data && typeof data === "object" && "order" in data + ? (data as { order: unknown }).order + : data; + return orderFromV0(payload as Record); + } + try { + const args: any[] = []; + args.push(orderId); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertOrder(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchOrder: ${error}`); + } + } + + async fetchOpenOrders(marketId?: string): Promise { + await this.initPromise; + if (this.isHosted) { + const resolvedAddress = resolveWalletAddress(this, undefined); + const route = HOSTED_METHOD_ROUTES.get("fetchOpenOrders")!; + const path = formatRoutePath(route, {}); + const params: Record = { address: resolvedAddress }; + if (marketId !== undefined) params.market_id = marketId; + const data = await _tradingRequest(this, { method: route.method, path, params }); + const list = Array.isArray(data) + ? data + : (data && Array.isArray((data as any).orders) ? (data as any).orders : []); + return list.map((o: unknown) => orderFromV0(o as Record)); + } + try { + const args: any[] = []; + if (marketId !== undefined) args.push(marketId); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchOpenOrders`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertOrder); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchOpenOrders: ${error}`); + } + } + + async fetchMyTrades(params?: MyTradesParams): Promise { + await this.initPromise; + if (this.isHosted) { + const resolvedAddress = resolveWalletAddress(this, undefined); + const route = HOSTED_METHOD_ROUTES.get("fetchMyTrades")!; + const path = formatRoutePath(route, { address: resolvedAddress }); + const data = await _tradingRequest(this, { method: route.method, path }); + const list = Array.isArray(data) ? data : (data && Array.isArray((data as any).trades) ? (data as any).trades : []); + return list.map((t: unknown) => userTradeFromV0(t as Record)); + } + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMyTrades`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertUserTrade); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMyTrades: ${error}`); + } + } + + async fetchClosedOrders(params?: OrderHistoryParams): Promise { + await this.initPromise; + if (this.isHosted) { + throw new NotSupported("fetchClosedOrders is not available in hosted trading mode."); + } + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchClosedOrders`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertOrder); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchClosedOrders: ${error}`); + } + } + + async fetchAllOrders(params?: OrderHistoryParams): Promise { + await this.initPromise; + if (this.isHosted) { + throw new NotSupported("fetchAllOrders is not available in hosted trading mode."); + } + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchAllOrders`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertOrder); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchAllOrders: ${error}`); + } + } + + async fetchPositions(address?: string): Promise { + await this.initPromise; + if (this.isHosted) { + const resolvedAddress = resolveWalletAddress(this, address); + const route = HOSTED_METHOD_ROUTES.get("fetchPositions")!; + const path = formatRoutePath(route, { address: resolvedAddress }); + const data = await _tradingRequest(this, { method: route.method, path }); + const list: unknown[] = Array.isArray(data) + ? data + : (data && Array.isArray((data as any).positions) ? (data as any).positions : []); + return list.map((p) => positionFromV0(p as Record)); + } + try { + const args: any[] = []; + if (address !== undefined) args.push(address); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchPositions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertPosition); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchPositions: ${error}`); + } + } + + async fetchBalance(address?: string): Promise { + await this.initPromise; + if (this.isHosted) { + const resolvedAddress = resolveWalletAddress(this, address); + const route = HOSTED_METHOD_ROUTES.get("fetchBalance")!; + const path = formatRoutePath(route, { address: resolvedAddress }); + const data = await _tradingRequest(this, { method: route.method, path }); + const list: unknown[] = Array.isArray(data) + ? data + : (data && Array.isArray((data as any).balances) ? (data as any).balances : []); + return list.map((b) => balanceFromV0(b as Record)); + } + try { + const args: any[] = []; + if (address !== undefined) args.push(address); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchBalance`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data.map(convertBalance); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchBalance: ${error}`); + } + } + + async unwatchOrderBook(outcomeId: string | MarketOutcome): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(resolveOutcomeId(outcomeId)); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/unwatchOrderBook`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to unwatchOrderBook: ${error}`); + } + } + + async unwatchAddress(address: string): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(address); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/unwatchAddress`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to unwatchAddress: ${error}`); + } + } + + async close(): Promise { + await this.initPromise; + try { + const args: any[] = []; + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/close`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to close: ${error}`); + } + } + + async fetchMarketMatches(params?: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMarketMatches`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMarketMatches: ${error}`); + } + } + + async fetchMatches(params: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMatches`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMatches: ${error}`); + } + } + + async fetchEventMatches(params?: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchEventMatches`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchEventMatches: ${error}`); + } + } + + async compareMarketPrices(params: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/compareMarketPrices`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to compareMarketPrices: ${error}`); + } + } + + async fetchRelatedMarkets(params: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchRelatedMarkets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchRelatedMarkets: ${error}`); + } + } + + async fetchMatchedMarkets(params?: FetchMatchedMarketClustersParams): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMatchedMarkets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMatchedMarkets: ${error}`); + } + } + + async fetchMatchedPrices(params?: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchMatchedPrices`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchMatchedPrices: ${error}`); + } + } + + async fetchHedges(params: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchHedges`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchHedges: ${error}`); + } + } + + async fetchArbitrage(params?: any): Promise { + await this.initPromise; + try { + const args: any[] = []; + if (params !== undefined) args.push(params); + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/fetchArbitrage`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetchArbitrage: ${error}`); + } + } + + // END GENERATED METHODS + + /** + * Get historical price candles. + * + * @param outcomeId - Outcome ID (from market.outcomes[].outcomeId) + * @param params - History filter parameters + * @returns List of price candles + * + * @example + * ```typescript + * const markets = await exchange.fetchMarkets({ query: "Trump" }); + * const outcomeId = markets[0].outcomes[0].outcomeId; + * const candles = await exchange.fetchOHLCV(outcomeId, { + * resolution: "1h", + * limit: 100 + * }); + * ``` + */ + async fetchOHLCV( + outcomeId: string | MarketOutcome, + params: any + ): Promise { + await this.initPromise; + const resolvedOutcomeId = resolveOutcomeId(outcomeId); + try { + const paramsDict: any = { resolution: params.resolution }; + if (params.start) { + paramsDict.start = params.start.toISOString(); + } + if (params.end) { + paramsDict.end = params.end.toISOString(); + } + if (params.limit) { + paramsDict.limit = params.limit; + } + + const args = [resolvedOutcomeId, paramsDict]; + const query = { outcomeId: resolvedOutcomeId, ...paramsDict }; + const json = await this.sidecarReadRequest('fetchOHLCV', query, args); + const data = this.handleResponse(json); + return data.map(convertCandle); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetch OHLCV: ${error}`); + } + } + + /** + * Get trade history for an outcome. + * + * Note: Polymarket requires API key. + * + * @param outcomeId - Outcome ID + * @param params - History filter parameters + * @returns List of trades + */ + async fetchTrades( + outcomeId: string | MarketOutcome, + params: any + ): Promise { + await this.initPromise; + const resolvedOutcomeId = resolveOutcomeId(outcomeId); + try { + const paramsDict: any = {}; + if (params.resolution) { + paramsDict.resolution = params.resolution; + } + if (params.limit) { + paramsDict.limit = params.limit; + } + if (params.start) { + paramsDict.start = params.start instanceof Date ? params.start.toISOString() : params.start; + } + if (params.end) { + paramsDict.end = params.end instanceof Date ? params.end.toISOString() : params.end; + } + + const args = [resolvedOutcomeId, paramsDict]; + const query = { outcomeId: resolvedOutcomeId, ...paramsDict }; + const json = await this.sidecarReadRequest('fetchTrades', query, args); + const data = this.handleResponse(json); + return data.map(convertTrade); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to fetch trades: ${error}`); + } + } + + // WebSocket Streaming Methods + + /** + * Watch real-time order book updates via WebSocket. + * + * Returns a promise that resolves with the next order book update. + * Call repeatedly in a loop to stream updates (CCXT Pro pattern). + * + * @param outcomeId - Outcome ID to watch + * @param limit - Optional depth limit for order book + * @param params - Optional exchange-specific parameters + * @returns Next order book update + * + * @example + * ```typescript + * // Stream order book updates + * while (true) { + * const orderBook = await exchange.watchOrderBook(outcomeId); + * console.log(`Best bid: ${orderBook.bids[0].price}`); + * console.log(`Best ask: ${orderBook.asks[0].price}`); + * } + * ``` + */ + async watchOrderBook(outcomeId: string | MarketOutcome, limit?: number, params: Record = {}): Promise { + await this.initPromise; + const resolvedOutcomeId = resolveOutcomeId(outcomeId); + const args: any[] = [resolvedOutcomeId]; + if (limit !== undefined) { + args.push(limit); + } + if (Object.keys(params).length > 0) { + if (limit === undefined) { + args.push(undefined); + } + args.push(params); + } + + const wsData = await this.watchViaWs("watchOrderBook", args); + if (wsData !== null) { + return convertOrderBook(wsData); + } + + throw this.wsTransportUnavailableError("watchOrderBook"); + } + + /** + * Watch real-time order book updates for multiple outcomes at once. + * + * Returns a record mapping each outcome ID (ticker) to its latest + * order book snapshot. Call repeatedly in a loop to stream updates + * (CCXT Pro pattern). + * + * @param outcomeIds - Array of outcome IDs (or MarketOutcome objects) + * @param limit - Optional depth limit for each order book + * @param params - Optional exchange-specific parameters + * @returns Record mapping ticker to OrderBook + * + * @example + * ```typescript + * const ids = markets.slice(0, 3).map(m => m.outcomes[0].outcomeId); + * while (true) { + * const books = await exchange.watchOrderBooks(ids); + * for (const [ticker, ob] of Object.entries(books)) { + * console.log(`${ticker}: bid=${ob.bids[0]?.price}`); + * } + * } + * ``` + */ + async watchOrderBooks( + outcomeIds: (string | MarketOutcome)[], + limit?: number, + params: Record = {}, + ): Promise> { + await this.initPromise; + const resolvedIds = outcomeIds.map(resolveOutcomeId); + const args: any[] = [resolvedIds]; + if (limit !== undefined) { + args.push(limit); + } + if (Object.keys(params).length > 0) { + if (limit === undefined) { + args.push(undefined); + } + args.push(params); + } + + try { + const ws = await this.getOrCreateWs(); + if (!ws) { + throw this.wsTransportUnavailableError("watchOrderBooks"); + } + + const rawResult = await ws.subscribeBatch( + this.exchangeName, + "watchOrderBooks", + args, + this.getCredentials() as Record | undefined, + ); + if (rawResult && typeof rawResult === "object") { + const result: Record = {}; + for (const [k, v] of Object.entries(rawResult)) { + if (v && typeof v === "object") { + result[k] = convertOrderBook(v); + } + } + return result; + } + + throw new PmxtError("watchOrderBooks: unexpected response shape from server"); + } catch (error) { + if (this.isWsTransportUnavailableError(error)) { + throw this.wsTransportUnavailableError("watchOrderBooks"); + } + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to watch order books: ${error}`); + } + } + + /** + * Stream all orderbook updates across venues via the hosted WebSocket API. + * + * Returns a promise that resolves with the next book event. + * Call repeatedly in a loop to stream updates (CCXT Pro pattern). + * Requires hosted mode (`pmxtApiKey` set). + * + * @param venues - Optional venue filter. Defaults to this exchange's venue + * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues. + * @returns Next event with source, symbol, and orderbook + * + * @example + * ```typescript + * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" }); + * while (true) { + * const event = await poly.watchAllOrderBooks(); + * console.log(event.source, event.symbol, event.orderbook.bids[0]); + * } + * ``` + */ + async watchAllOrderBooks(venues?: string[]): Promise { + await this.initPromise; + + if (!this.isHosted) { + throw new PmxtError("watchAllOrderBooks() requires hosted mode (set pmxtApiKey)"); + } + + const effectiveVenues = venues ?? this.defaultWatchAllOrderBookVenues(); + const args: any[] = effectiveVenues?.length ? [effectiveVenues] : []; + const wsData = await this.watchViaWs("watchAllOrderBooks", args); + if (wsData !== null) { + return { + source: (wsData as any)._source || "", + symbol: (wsData as any)._symbol || "", + orderbook: convertOrderBook(wsData), + }; + } + + throw this.wsTransportUnavailableError("watchAllOrderBooks"); + } + + /** + * Stream all orderbook updates across venues. + * + * @deprecated Use {@link watchAllOrderBooks} instead. + * @param venues - Optional venue filter + * @returns Next event with source, symbol, and orderbook + */ + async firehose(venues?: string[]): Promise { + return this.watchAllOrderBooks(venues); + } + + /** + * Watch real-time trade updates via WebSocket. + * + * Returns a promise that resolves with the next trade(s). + * Call repeatedly in a loop to stream updates (CCXT Pro pattern). + * + * @param outcomeId - Outcome ID to watch + * @param address - Public wallet to be watched + * @param since - Optional timestamp to filter trades from + * @param limit - Optional limit for number of trades + * @returns Next trade update(s) + * + * @example + * ```typescript + * // Stream trade updates + * while (true) { + * const trades = await exchange.watchTrades(outcomeId); + * for (const trade of trades) { + * console.log(`Trade: ${trade.price} @ ${trade.amount}`); + * } + * } + * ``` + */ + async watchTrades( + outcomeId: string | MarketOutcome, + address?: string, + since?: number, + limit?: number + ): Promise { + await this.initPromise; + const resolvedOutcomeId = resolveOutcomeId(outcomeId); + const args: any[] = [resolvedOutcomeId]; + if (address !== undefined) { + args.push(address); + } + if (since !== undefined) { + args.push(since); + } + if (limit !== undefined) { + args.push(limit); + } + + const wsData = await this.watchViaWs("watchTrades", args); + if (wsData !== null) { + return wsData.map(convertTrade); + } + + throw this.wsTransportUnavailableError("watchTrades"); + } + + /** + * Watch real-time updates of a public wallet via WebSocket. + * + * Returns a promise that resolves with the next update(s). + * Call repeatedly in a loop to stream updates (CCXT Pro pattern). + * + * @param address - Public wallet to be watched + * @param types - Subscription options including 'trades', 'positions', and 'balances' + * @returns Next update(s) + * + * @example + * ```typescript + * // Stream updates of a public wallet address + * while (true) { + * const snapshots = await exchange.watchAddress(address, types); + * for (const snapshot of snapshots) { + * console.log(`Trade: ${snapshot.trades}`); + * } + * } + * ``` + */ + async watchAddress( + address: string, + types?: SubscriptionOption[], + ): Promise { + await this.initPromise; + try { + const args: any[] = [address]; + if (types !== undefined) { + args.push(types); + } + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/watchAddress`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args, credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertSubscriptionSnapshot(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to watch address: ${error}`); + } + } + + // Trading Methods (require authentication) + + /** + * Build an order payload without submitting it to the exchange. + * Returns the exchange-native signed order or transaction payload for + * inspection, forwarding through a middleware layer, or deferred + * submission via {@link submitOrder}. + * + * You can specify the market either with explicit marketId/outcomeId, + * or by passing an outcome object directly (e.g., market.yes). + * + * @param params - Order parameters (same as createOrder) + * @returns A BuiltOrder containing the exchange-native payload + * + * @example + * ```typescript + * // Build, inspect, then submit: + * const built = await exchange.buildOrder({ + * marketId: "663583", + * outcomeId: "10991849...", + * side: "buy", + * type: "limit", + * amount: 10, + * price: 0.55 + * }); + * + * console.log(built.signedOrder); // inspect before submitting + * const order = await exchange.submitOrder(built); + * + * // Using outcome shorthand: + * const yes = market.yes; + * if (!yes) throw new Error("Market has no YES outcome"); + * + * const built2 = await exchange.buildOrder({ + * outcome: yes, + * side: "buy", + * type: "market", + * amount: 10 + * }); + * ``` + */ + async buildOrder(params: CreateOrderInput): Promise { + if (this.isHostedTradingMode()) { + return this._hostedBuildOrder(params); + } + if (this.isHosted) { + throw new PmxtError( + "Trade execution is not available through the hosted API. " + + "Use the local PMXT SDK with your venue credentials instead. " + + "See https://pmxt.dev/docs/quickstart for setup instructions." + ); + } + await this.initPromise; + try { + let marketId = params.marketId; + let outcomeId = params.outcomeId; + + if (params.outcome) { + if (marketId !== undefined || outcomeId !== undefined) { + throw new PmxtError( + "Cannot specify both 'outcome' and 'marketId'/'outcomeId'. Use one or the other." + ); + } + const outcome: MarketOutcome = params.outcome; + if (!outcome.marketId) { + throw new PmxtError( + "outcome.marketId is not set. Ensure the outcome comes from a fetched market." + ); + } + marketId = outcome.marketId; + outcomeId = outcome.outcomeId; + } + + const paramsDict: any = { + marketId, + outcomeId, + side: params.side, + type: params.type, + amount: params.amount, + }; + if (params.price !== undefined) { + paramsDict.price = params.price; + } + if (params.fee !== undefined) { + paramsDict.fee = params.fee; + } + if (params.tickSize !== undefined) { + paramsDict.tickSize = params.tickSize; + } + if (params.negRisk !== undefined) { + paramsDict.negRisk = params.negRisk; + } + if (params.onBehalfOf !== undefined) { + paramsDict.onBehalfOf = params.onBehalfOf; + } + + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/buildOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args: [paramsDict], credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return data as BuiltOrder; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to build order: ${error}`); + } + } + + private async _discoverHostedAccount(): Promise { + if (this._hostedAccount) return; + if (!this._accountDiscoveryPromise) { + this._accountDiscoveryPromise = (async () => { + try { + const res = await this.fetchWithRetry( + `${this.resolveBaseUrl()}/v0/account`, + { method: 'GET', headers: { ...this.getAuthHeaders() } }, + ); + if (res.ok) { + const body = await res.json(); + this._hostedAccount = { depositWallet: body.deposit_wallet, signatureType: body.signature_type }; + } else { this._hostedAccount = {}; } + } catch (err) { + logger.warn('PmxtClient: hosted account discovery failed', { error: String(err) }); + this._hostedAccount = {}; + } + })(); + } + await this._accountDiscoveryPromise; + } + + private async _executeSorOrder(params: any): Promise { + await this._discoverHostedAccount(); + + const buildRes = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/sor/buildOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args: [params] }), + }); + if (!buildRes.ok) throw new PmxtError(`buildOrder failed: ${await buildRes.text()}`); + const buildJson = await buildRes.json(); + const { orderId, legs } = buildJson.data || buildJson; + + const fills: any[] = []; + for (const leg of legs) { + try { + const { Polymarket, Limitless } = require('pmxt-core'); + const VenueClass = leg.venue === 'polymarket' ? Polymarket : leg.venue === 'limitless' ? Limitless : null; + if (!VenueClass) throw new Error(`unsupported venue: ${leg.venue}`); + + const venueOpts: any = { privateKey: this.privateKey }; + if (leg.venue === 'polymarket' && this._hostedAccount?.depositWallet) { + venueOpts.funderAddress = this._hostedAccount.depositWallet; + venueOpts.signatureType = this._hostedAccount.signatureType || 3; + } + const venue = new VenueClass(venueOpts); + const orderParams: any = { outcomeId: leg.tokenId, side: leg.side, amount: leg.shares }; + if (leg.orderType === 'market') { orderParams.type = 'market'; orderParams.price = leg.price; } + else { orderParams.price = leg.price; } + const order = await venue.createOrder(orderParams); + fills.push({ venue: leg.venue, venueOrderId: order.id, venueMarketId: leg.venueMarketId, venueOutcomeId: leg.venueOutcomeId, shares: order.filled ?? leg.shares, price: order.price || leg.price, status: order.status }); + } catch (err: any) { + fills.push({ venue: leg.venue, venueMarketId: leg.venueMarketId, venueOutcomeId: leg.venueOutcomeId, shares: leg.shares, price: leg.price, status: 'failed', error: err.message }); + } + } + + const submitRes = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/sor/submitOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args: [{ orderId, fills }] }), + }); + if (!submitRes.ok) throw new PmxtError(`submitOrder failed: ${await submitRes.text()}`); + const submitJson = await submitRes.json(); + const submitData = submitJson.data || submitJson; + if (submitData.status === 'failed' && submitData.errors?.length) { + throw new PmxtError(submitData.errors[0]); + } + return convertOrder(submitData); + } + + /** + * Hosted-mode buildOrder: validate inputs locally, then POST to the + * trading service's `build-order` endpoint and return a BuiltOrder + * that carries the original build_request for Layer-2 economic checks + * at submit time. + */ + private async _hostedBuildOrder( + params: CreateOrderInput, + ): Promise { + const body = this._hostedBuildOrderBody(params); + const route = HOSTED_METHOD_ROUTES.get("buildOrder")!; + const data = await _tradingRequest(this, { + method: route.method, + path: route.path, + body, + }) as Record; + // Attach the originating build_request so submit can run economic + // validation without an extra catalog round-trip. + const built = { ...data, build_request: body } as unknown as BuiltOrder; + return built; + } + + /** + * Hosted-mode createOrder: build → sign → submit single-call wrapper. + */ + private async _hostedCreateOrder(params: CreateOrderInput): Promise { + const built = await this._hostedBuildOrder(params); + return this._hostedSubmitOrder(built); + } + + /** + * Hosted-mode submitOrder: validate the stored build response, sign the + * typed_data (and pull_typed_data for cross-chain venue sells), then + * POST to `/v0/trade/submit-order`. + * + * Restored after PR #1058 (Limitless hosted wire-up) accidentally + * removed it but left the call site + signing imports intact. + */ + private async _hostedSubmitOrder(built: BuiltOrder): Promise { + const signer = this.requireHostedSigner(); + if (!this.walletAddress) { + throw new MissingWalletAddress( + "hosted submitOrder requires walletAddress", + ); + } + const payload = built as unknown as Record; + const typedData = payload["typed_data"] as TypedData | undefined; + if (!typedData) { + throw new HostedInvalidSignature(0, "typed_data missing from built order"); + } + const buildRequest = (payload["build_request"] as Record | undefined) + ?? ((payload["params"] as Record | undefined)?.["build_request"] as Record | undefined); + + const side = String(buildRequest?.["side"] ?? "buy"); + const primaryRoute = this._hostedTypedDataRoute(side, false); + validateTypedData(typedData, primaryRoute, this.walletAddress); + if (buildRequest) { + validateEconomics(typedData, primaryRoute, buildRequest, payload); + } + + const signature = await signer.signTypedData(typedData); + verifySignature(typedData, signature, signer.address); + + const body: Record = { + built_order_id: payload["built_order_id"], + signature, + }; + + const pullTypedData = payload["pull_typed_data"] as TypedData | undefined; + if (pullTypedData) { + const pullRoute = this._hostedTypedDataRoute(side, true); + if (pullRoute) { + validateTypedData(pullTypedData, pullRoute, this.walletAddress); + } + const pullSig = await signer.signTypedData(pullTypedData); + verifySignature(pullTypedData, pullSig, signer.address); + body["pull_signature"] = pullSig; + } + + const route = HOSTED_METHOD_ROUTES.get("submitOrder")!; + const data = await _tradingRequest(this, { method: route.method, path: route.path, body }); + return orderFromV0(data as Record); + } + + /** + * Resolve the per-(venue, side, pull) typed-data schema route used by + * `validateTypedData` / `validateEconomics`. Returns the cross-chain + * pull-leg route name for Opinion sells and Limitless cross-chain orders. + */ + private _hostedTypedDataRoute(side: string, isPull: boolean): HostedRoute { + const venue = this.exchangeName; + const sideLower = side.toLowerCase(); + if (venue === "polymarket") { + return sideLower === "sell" ? "polymarket_sell" : "polymarket_buy"; + } + if (venue === "limitless") { + if (sideLower === "buy") return "limitless_buy"; + return isPull ? "limitless_sell_base_pull" : "limitless_sell_polygon"; + } + // opinion + if (sideLower === "buy") return "opinion_buy"; + return isPull ? "opinion_sell_bsc_pull" : "opinion_sell_polygon"; + } + + /** + * Hosted-mode cancelOrder: build → sign → cancel single-call wrapper. + * Mirrors the Python SDK's `_hosted_cancel_order`. + */ + private async _hostedCancelOrder(orderId: string): Promise { + const signer = this.requireHostedSigner(); + if (!this.walletAddress) { + throw new MissingWalletAddress("hosted cancelOrder requires walletAddress"); + } + const buildRequest = { order_id: orderId, user_address: this.walletAddress }; + const buildRoute = HOSTED_METHOD_ROUTES.get("cancelOrderBuild")!; + const buildPayload = await _tradingRequest(this, { + method: buildRoute.method, + path: buildRoute.path, + body: buildRequest, + }) as Record; + + const typedData = buildPayload["typed_data"] as TypedData | undefined; + if (!typedData) { + throw new HostedInvalidSignature(0, "typed_data missing from hosted cancel build response"); + } + const primaryRoute = this._hostedCancelTypedDataRoute(false); + validateTypedData(typedData, primaryRoute, this.walletAddress); + const signature = await signer.signTypedData(typedData); + verifySignature(typedData, signature, signer.address); + + const cancelId = buildPayload["cancel_id"]; + if (!cancelId) { + throw new HostedInvalidSignature(0, "cancel_id missing from hosted cancel build response"); + } + const body: Record = { cancel_id: cancelId, signature }; + + const pullTypedData = buildPayload["pull_typed_data"] as TypedData | undefined; + if (pullTypedData) { + const pullRoute = this._hostedCancelTypedDataRoute(true); + validateTypedData(pullTypedData, pullRoute, this.walletAddress); + const pullSig = await signer.signTypedData(pullTypedData); + verifySignature(pullTypedData, pullSig, signer.address); + body["pull_signature"] = pullSig; + } + + const cancelRoute = HOSTED_METHOD_ROUTES.get("cancelOrder")!; + const data = await _tradingRequest(this, { + method: cancelRoute.method, + path: cancelRoute.path, + body, + }); + return orderFromV0(data as Record); + } + + private _hostedCancelTypedDataRoute(isPull: boolean): HostedRoute { + const venue = this.exchangeName; + if (venue === "polymarket") return "cancel_polymarket"; + if (venue === "limitless") { + return isPull ? "cancel_limitless_base_pull" : "cancel_limitless_polygon"; + } + return isPull ? "cancel_opinion_bsc_pull" : "cancel_opinion_polygon"; + } + + /** + * Construct the hosted build-order request body and validate inputs + * locally per the v0 contract (denom/side compatibility, > 6-decimal + * precision rejected via {@link to6dec}). + */ + private _hostedBuildOrderBody( + params: CreateOrderInput, + ): Record { + let marketId: string | undefined = params.marketId; + let outcomeId: string | undefined = params.outcomeId; + + if (params.outcome) { + if (marketId !== undefined || outcomeId !== undefined) { + throw new InvalidOrder( + "cannot specify both 'outcome' and 'marketId'/'outcomeId'", + ); + } + const outcome: MarketOutcome = params.outcome; + if (!outcome.outcomeId) { + throw new InvalidOrder( + "outcome.outcomeId is not set; ensure the outcome comes from a fetched market", + ); + } + // marketId is optional in hosted mode -- backend derives it from + // outcomeId (catalog UUID). Forward it when present for backcompat. + marketId = outcome.marketId || undefined; + outcomeId = outcome.outcomeId; + } + + if (!outcomeId) { + throw new InvalidOrder( + "outcomeId is required (or pass an 'outcome' from a fetched market)", + ); + } + + const side = String(params.side); + const orderType = String(params.type ?? "market"); + const denom = (params as unknown as Record)["denom"] as + | "usdc" + | "shares" + | undefined; + + // denom/side compatibility per v0: + // market buy -> denom='usdc' + // market sell -> denom='shares' + // any limit -> denom='shares' + let resolvedDenom: "usdc" | "shares"; + if (orderType === "market") { + if (side === "buy") { + if (denom && denom !== "usdc") { + throw new InvalidOrder("market buy requires denom='usdc'"); + } + resolvedDenom = "usdc"; + } else if (side === "sell") { + if (denom && denom !== "shares") { + throw new InvalidOrder("market sell requires denom='shares'"); + } + resolvedDenom = "shares"; + } else { + throw new InvalidOrder(`unknown side: ${side}`); + } + } else { + if (denom && denom !== "shares") { + throw new InvalidOrder("limit orders require denom='shares'"); + } + resolvedDenom = "shares"; + } + + if (!(Number(params.amount) > 0)) { + throw new InvalidOrder("amount must be positive"); + } + + // to6dec throws InvalidOrder for sub-micro precision. + const amount6dec = to6dec(params.amount as number).toString(); + + // The supplied outcomeId may be a catalog UUID OR a venue-native id + // (e.g. a Polymarket tokenId or an Opinion market hash). Catalog + // UUIDs are forwarded as `outcome_id`; venue-native ids are + // forwarded as `(venue, venue_outcome_id)` so the backend resolver + // picks the right path. Either shape is accepted by the v0 trading + // API. + const body: Record = { + side, + order_type: orderType, + denom: resolvedDenom, + amount: params.amount, + amount_6dec: amount6dec, + }; + if (Exchange._isCatalogUuid(outcomeId)) { + body["outcome_id"] = outcomeId; + // market_id is optional in hosted mode: backend derives it + // from outcome_id (UUID) when omitted. Forward only when the + // caller supplied a non-empty UUID -- "absent" and "null" are + // not equivalent under some Pydantic configs on the backend. + if (marketId && Exchange._isCatalogUuid(marketId)) { + body["market_id"] = marketId; + } + } else { + // Venue-native form: backend resolves the row from + // (source_exchange, pmxt_id). marketId from a venue client is + // itself venue-native and would fail backend UUID validation + // if forwarded -- suppress it. + body["venue"] = this.exchangeName; + body["venue_outcome_id"] = outcomeId; + } + + if (params.price !== undefined) body["price"] = params.price; + const extra = params as unknown as Record; + if (extra["slippage_pct"] !== undefined) { + body["slippage_pct"] = extra["slippage_pct"]; + } + body["user_address"] = resolveWalletAddress(this); + return body; + } + + /** + * @example + * ```typescript + * const order = await exchange.createOrder({ + * marketId: "663583", + * outcomeId: "10991849...", + * side: "buy", + * type: "limit", + * amount: 10, + * price: 0.55 + * }); + * ``` + */ + async createOrder(params: CreateOrderInput): Promise { + // SOR escape path (preserved): legacy hosted SOR flow uses a venue-side + // SDK to execute the legs, only when a privateKey is present. + if (this.isHosted && this.exchangeName === 'sor' && this.privateKey) { + return this._executeSorOrder(params as any); + } + if (this.isHostedTradingMode()) { + return this._hostedCreateOrder(params); + } + if (this.isHosted) { + throw new PmxtError( + "Trade execution is not available through the hosted API. " + + "Use the local PMXT SDK with your venue credentials instead. " + + "See https://pmxt.dev/docs/quickstart for setup instructions." + ); + } + await this.initPromise; + try { + // Resolve outcome shorthand: extract marketId/outcomeId from outcome object + let marketId = params.marketId; + let outcomeId = params.outcomeId; + + if (params.outcome) { + if (marketId !== undefined || outcomeId !== undefined) { + throw new PmxtError( + "Cannot specify both 'outcome' and 'marketId'/'outcomeId'. Use one or the other." + ); + } + const outcome: MarketOutcome = params.outcome; + if (!outcome.marketId) { + throw new PmxtError( + "outcome.marketId is not set. Ensure the outcome comes from a fetched market." + ); + } + marketId = outcome.marketId; + outcomeId = outcome.outcomeId; + } + + const paramsDict: any = { + marketId, + outcomeId, + side: params.side, + type: params.type, + amount: params.amount, + }; + if (params.price !== undefined) { + paramsDict.price = params.price; + } + if (params.fee !== undefined) { + paramsDict.fee = params.fee; + } + if (params.tickSize !== undefined) { + paramsDict.tickSize = params.tickSize; + } + if (params.negRisk !== undefined) { + paramsDict.negRisk = params.negRisk; + } + if (params.onBehalfOf !== undefined) { + paramsDict.onBehalfOf = params.onBehalfOf; + } + + const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/createOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args: [paramsDict], credentials: this.getCredentials() }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body.error && typeof body.error === "object") { + throw fromServerError(body.error); + } + throw new PmxtError(body.error?.message || response.statusText); + } + const json = await response.json(); + const data = this.handleResponse(json); + return convertOrder(data); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to create order: ${error}`); + } + } + + /** + * Calculate the average execution price for a given amount by walking the order book. + * Uses the sidecar server for calculation to ensure consistency. + * + * @param orderBook - The current order book + * @param side - 'buy' or 'sell' + * @param amount - The amount to execute + * @returns The volume-weighted average price, or 0 if insufficient liquidity + */ + getExecutionPrice(orderBook: OrderBook, side: 'buy' | 'sell', amount: number): number { + const levels = side === 'buy' ? orderBook.asks : orderBook.bids; + let remaining = amount; + let totalCost = 0; + for (const level of levels) { + const fill = Math.min(remaining, level.size); + totalCost += fill * level.price; + remaining -= fill; + if (remaining <= 0) break; + } + if (remaining > 0) return 0; + return totalCost / amount; + } + + /** + * Calculate detailed execution price information by walking the order book locally. + * + * @param orderBook - The current order book + * @param side - 'buy' or 'sell' + * @param amount - The amount to execute + * @returns Detailed execution result + */ + getExecutionPriceDetailed( + orderBook: OrderBook, + side: 'buy' | 'sell', + amount: number + ): ExecutionPriceResult { + if (amount <= 0) { + throw new Error('Amount must be greater than 0'); + } + + const levels = (side === 'buy' ? orderBook.asks : orderBook.bids) + .filter((level) => level.size > 0) + .sort((a, b) => side === 'buy' ? a.price - b.price : b.price - a.price); + + if (levels.length === 0) { + return { price: 0, filledAmount: 0, fullyFilled: false }; + } + + let remainingAmount = amount; + let totalCost = 0; + let filledAmount = 0; + const epsilon = 0.00000001; + + for (const level of levels) { + if (remainingAmount <= epsilon) break; + + const fillSize = Math.min(remainingAmount, level.size); + totalCost += fillSize * level.price; + filledAmount += fillSize; + remainingAmount -= fillSize; + } + + return { + price: filledAmount > epsilon ? totalCost / filledAmount : 0, + filledAmount, + fullyFilled: remainingAmount <= epsilon, + }; + } + + // ---------------------------------------------------------------------------- + // Filtering Methods + // ---------------------------------------------------------------------------- + + /** + * Filter markets based on criteria or custom function. + * + * @param markets - Array of markets to filter + * @param criteria - Filter criteria object, string (simple text search), or predicate function + * @returns Filtered array of markets + * + * @example Simple text search + * api.filterMarkets(markets, 'Trump') + * + * @example Advanced filtering + * api.filterMarkets(markets, { + * text: 'Trump', + * searchIn: ['title', 'tags'], + * volume24h: { min: 10000 }, + * category: 'Politics', + * price: { outcome: 'yes', max: 0.5 } + * }) + * + * @example Custom predicate + * api.filterMarkets(markets, m => m.liquidity > 5000 && m.yes?.price < 0.3) + */ + filterMarkets( + markets: UnifiedMarket[], + criteria: string | MarketFilterCriteria | MarketFilterFunction + ): UnifiedMarket[] { + // Handle predicate function + if (typeof criteria === 'function') { + return markets.filter(criteria); + } + + // Handle simple string search + if (typeof criteria === 'string') { + const lowerQuery = criteria.toLowerCase(); + return markets.filter(m => + m.title.toLowerCase().includes(lowerQuery) + ); + } + + // Handle criteria object + return markets.filter(market => { + // Text search + if (criteria.text) { + const lowerQuery = criteria.text.toLowerCase(); + const searchIn = criteria.searchIn || ['title']; + let textMatch = false; + + for (const field of searchIn) { + if (field === 'title' && market.title?.toLowerCase().includes(lowerQuery)) { + textMatch = true; + break; + } + if (field === 'description' && market.description?.toLowerCase().includes(lowerQuery)) { + textMatch = true; + break; + } + if (field === 'category' && market.category?.toLowerCase().includes(lowerQuery)) { + textMatch = true; + break; + } + if (field === 'tags' && market.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))) { + textMatch = true; + break; + } + if (field === 'outcomes' && market.outcomes?.some(o => o.label.toLowerCase().includes(lowerQuery))) { + textMatch = true; + break; + } + } + + if (!textMatch) return false; + } + + // Category filter + if (criteria.category && market.category !== criteria.category) { + return false; + } + + // Tags filter (match ANY of the provided tags) + if (criteria.tags && criteria.tags.length > 0) { + const hasMatchingTag = criteria.tags.some(tag => + market.tags?.some(marketTag => + marketTag.toLowerCase() === tag.toLowerCase() + ) + ); + if (!hasMatchingTag) return false; + } + + // Volume24h filter + if (criteria.volume24h) { + if (criteria.volume24h.min !== undefined && market.volume24h < criteria.volume24h.min) { + return false; + } + if (criteria.volume24h.max !== undefined && market.volume24h > criteria.volume24h.max) { + return false; + } + } + + // Volume filter + if (criteria.volume) { + if (criteria.volume.min !== undefined && (market.volume || 0) < criteria.volume.min) { + return false; + } + if (criteria.volume.max !== undefined && (market.volume || 0) > criteria.volume.max) { + return false; + } + } + + // Liquidity filter + if (criteria.liquidity) { + if (criteria.liquidity.min !== undefined && market.liquidity < criteria.liquidity.min) { + return false; + } + if (criteria.liquidity.max !== undefined && market.liquidity > criteria.liquidity.max) { + return false; + } + } + + // OpenInterest filter + if (criteria.openInterest) { + if (criteria.openInterest.min !== undefined && (market.openInterest || 0) < criteria.openInterest.min) { + return false; + } + if (criteria.openInterest.max !== undefined && (market.openInterest || 0) > criteria.openInterest.max) { + return false; + } + } + + // ResolutionDate filter + if (criteria.resolutionDate && market.resolutionDate) { + const resDate = market.resolutionDate; + if (criteria.resolutionDate.before && resDate >= criteria.resolutionDate.before) { + return false; + } + if (criteria.resolutionDate.after && resDate <= criteria.resolutionDate.after) { + return false; + } + } + + // Price filter (for binary markets) + if (criteria.price) { + const outcome = market[criteria.price.outcome]; + if (!outcome) return false; + + if (criteria.price.min !== undefined && outcome.price < criteria.price.min) { + return false; + } + if (criteria.price.max !== undefined && outcome.price > criteria.price.max) { + return false; + } + } + + // Price change filter + if (criteria.priceChange24h) { + const outcome = market[criteria.priceChange24h.outcome]; + if (!outcome || outcome.priceChange24h === undefined) return false; + + if (criteria.priceChange24h.min !== undefined && outcome.priceChange24h < criteria.priceChange24h.min) { + return false; + } + if (criteria.priceChange24h.max !== undefined && outcome.priceChange24h > criteria.priceChange24h.max) { + return false; + } + } + + return true; + }); + } + + /** + * Filter events based on criteria or custom function. + * + * @param events - Array of events to filter + * @param criteria - Filter criteria object, string (simple text search), or predicate function + * @returns Filtered array of events + * + * @example Simple text search + * api.filterEvents(events, 'Trump') + * + * @example Advanced filtering + * api.filterEvents(events, { + * text: 'Election', + * searchIn: ['title', 'tags'], + * category: 'Politics', + * marketCount: { min: 5 } + * }) + * + * @example Custom predicate + * api.filterEvents(events, e => e.markets.length > 10) + */ + filterEvents( + events: UnifiedEvent[], + criteria: string | EventFilterCriteria | EventFilterFunction + ): UnifiedEvent[] { + // Handle predicate function + if (typeof criteria === 'function') { + return events.filter(criteria); + } + + // Handle simple string search + if (typeof criteria === 'string') { + const lowerQuery = criteria.toLowerCase(); + return events.filter(e => + e.title.toLowerCase().includes(lowerQuery) + ); + } + + // Handle criteria object + return events.filter(event => { + // Text search + if (criteria.text) { + const lowerQuery = criteria.text.toLowerCase(); + const searchIn = criteria.searchIn || ['title']; + let textMatch = false; + + for (const field of searchIn) { + if (field === 'title' && event.title?.toLowerCase().includes(lowerQuery)) { + textMatch = true; + break; + } + if (field === 'description' && event.description?.toLowerCase().includes(lowerQuery)) { + textMatch = true; + break; + } + if (field === 'category' && event.category?.toLowerCase().includes(lowerQuery)) { + textMatch = true; + break; + } + if (field === 'tags' && event.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))) { + textMatch = true; + break; + } + } + + if (!textMatch) return false; + } + + // Category filter + if (criteria.category && event.category !== criteria.category) { + return false; + } + + // Tags filter (match ANY of the provided tags) + if (criteria.tags && criteria.tags.length > 0) { + const hasMatchingTag = criteria.tags.some(tag => + event.tags?.some(eventTag => + eventTag.toLowerCase() === tag.toLowerCase() + ) + ); + if (!hasMatchingTag) return false; + } + + // Market count filter + if (criteria.marketCount) { + const count = event.markets.length; + if (criteria.marketCount.min !== undefined && count < criteria.marketCount.min) { + return false; + } + if (criteria.marketCount.max !== undefined && count > criteria.marketCount.max) { + return false; + } + } + + // Total volume filter + if (criteria.totalVolume) { + const totalVolume = event.markets.reduce((sum, m) => sum + m.volume24h, 0); + if (criteria.totalVolume.min !== undefined && totalVolume < criteria.totalVolume.min) { + return false; + } + if (criteria.totalVolume.max !== undefined && totalVolume > criteria.totalVolume.max) { + return false; + } + } + + return true; + }); + } +} + +/** + * Polymarket exchange client. + * + * @example + * ```typescript + * // Public data (no auth) + * const poly = new Polymarket(); + * const markets = await poly.fetchMarkets({ query: "Trump" }); + * + * // Trading (requires auth) + * const poly = new Polymarket({ privateKey: process.env.POLYMARKET_PRIVATE_KEY }); + * const balance = await poly.fetchBalance(); + * ``` + */ +/** + * Options for initializing Polymarket client. + */ +export interface PolymarketOptions { + /** Venue-specific API key (e.g. Polymarket CLOB key). Optional. */ + apiKey?: string; + + /** Private key for authentication (optional) */ + privateKey?: string; + + /** Hosted pmxt API key. Enables hosted mode when set. */ + pmxtApiKey?: string; + + /** Base URL of the PMXT sidecar server */ + baseUrl?: string; + + /** Automatically start server if not running (default: true) */ + autoStartServer?: boolean; + + /** Optional Polymarket Proxy/Smart Wallet address */ + proxyAddress?: string; + + /** Optional signature type */ + signatureType?: 'eoa' | 'poly-proxy' | 'gnosis-safe' | number; + + /** + * EVM wallet address used for hosted reads/writes. + */ + walletAddress?: string; + + /** + * External signer used for hosted writes. + */ + signer?: Signer; +} + +export class Polymarket extends Exchange { + constructor(options: PolymarketOptions = {}) { + // Default to gnosis-safe signature type + const polyOptions = { + signatureType: 'gnosis-safe', + ...options + }; + super("polymarket", polyOptions as ExchangeOptions); + } + + /** + * Initialize Polymarket L2 API credentials for implicit API signing. + * + * Call this before private Polymarket implicit-API endpoints when the + * underlying CLOB credentials have not been created yet. + */ + async initAuth(): Promise { + await this.initPromise; + try { + const json = await this.sidecarPostRequest('initAuth', []); + this.handleResponse(json); + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to initAuth: ${error}`); + } + } +} + +/** + * Kalshi exchange client. + * + * @example + * ```typescript + * // Public data (no auth) + * const kalshi = new Kalshi(); + * const markets = await kalshi.fetchMarkets({ query: "Fed rates" }); + * + * // Trading (requires auth) + * const kalshi = new Kalshi({ + * apiKey: process.env.KALSHI_API_KEY, + * privateKey: process.env.KALSHI_PRIVATE_KEY + * }); + * const balance = await kalshi.fetchBalance(); + * ``` + */ +export class Kalshi extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("kalshi", options); + } +} + +/** + * Limitless exchange client. + * + * @example + * ```typescript + * // Public data (no auth) + * const limitless = new Limitless(); + * const markets = await limitless.fetchMarkets({ query: "Trump" }); + * + * // Trading (requires auth) + * const limitless = new Limitless({ + * apiKey: process.env.LIMITLESS_API_KEY, + * privateKey: process.env.LIMITLESS_PRIVATE_KEY + * }); + * const balance = await limitless.fetchBalance(); + * ``` + */ +export class Limitless extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("limitless", options); + } + + /** + * Watch real-time AMM price updates via WebSocket. + * + * @param marketAddress - Limitless market contract address + * @param callback - Optional callback invoked with the returned update + * @returns Next price update + */ + async watchPrices( + marketAddress: string, + callback?: (data: Record) => void, + ): Promise> { + await this.initPromise; + try { + const json = await this.sidecarPostRequest("watchPrices", [marketAddress]); + const data = this.handleResponse(json); + callback?.(data); + return data; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to watch prices: ${error}`); + } + } + + /** + * Watch real-time Limitless user position updates. + * + * Requires API key authentication. + * + * @param callback - Optional callback invoked with the returned positions + * @returns Next position update payload + */ + async watchUserPositions( + callback?: (data: Position[]) => void, + ): Promise { + await this.initPromise; + try { + const json = await this.sidecarPostRequest("watchUserPositions", []); + const data = this.handleResponse(json).map(convertPosition); + callback?.(data); + return data; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to watch user positions: ${error}`); + } + } + + /** + * Watch real-time Limitless user transaction updates. + * + * Requires API key authentication. + * + * @param callback - Optional callback invoked with the returned update + * @returns Next transaction update + */ + async watchUserTransactions( + callback?: (data: Record) => void, + ): Promise> { + await this.initPromise; + try { + const json = await this.sidecarPostRequest("watchUserTransactions", []); + const data = this.handleResponse(json); + callback?.(data); + return data; + } catch (error) { + if (error instanceof PmxtError) throw error; + throw new PmxtError(`Failed to watch user transactions: ${error}`); + } + } +} + +/** + * Kalshi Demo exchange client (paper trading / sandbox environment). + * + * Uses Kalshi's demo environment — same API as Kalshi but against test accounts. + * Credentials are separate from production Kalshi credentials. + * + * @example + * ```typescript + * const kalshiDemo = new KalshiDemo({ + * apiKey: process.env.KALSHI_DEMO_API_KEY, + * privateKey: process.env.KALSHI_DEMO_PRIVATE_KEY + * }); + * const balance = await kalshiDemo.fetchBalance(); + * ``` + */ +export class KalshiDemo extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("kalshi-demo", options); + } +} + +/** + * Myriad exchange client. + * + * AMM-based prediction market exchange. Requires an API key for trading. + * The `privateKey` field is used as the wallet address. + * + * @example + * ```typescript + * // Public data (no auth) + * const myriad = new Myriad(); + * const markets = await myriad.fetchMarkets(); + * + * // Trading (requires auth) + * const myriad = new Myriad({ + * apiKey: process.env.MYRIAD_API_KEY, + * privateKey: process.env.MYRIAD_WALLET_ADDRESS + * }); + * ``` + */ +export class Myriad extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("myriad", options); + } +} + +/** + * Probable exchange client. + * + * BSC-based CLOB exchange. Requires all four credential fields for trading. + * + * @example + * ```typescript + * // Public data (no auth) + * const probable = new Probable(); + * const markets = await probable.fetchMarkets(); + * + * // Trading (requires auth) + * const probable = new Probable({ + * privateKey: process.env.PROBABLE_PRIVATE_KEY, + * apiKey: process.env.PROBABLE_API_KEY, + * apiSecret: process.env.PROBABLE_API_SECRET, + * passphrase: process.env.PROBABLE_PASSPHRASE + * }); + * ``` + */ +export class Probable extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("probable", options); + } +} + +/** + * Baozi exchange client. + * + * Solana-based on-chain pari-mutuel betting exchange. + * Requires a Solana private key for trading. + * + * @example + * ```typescript + * // Public data (no auth) + * const baozi = new Baozi(); + * const markets = await baozi.fetchMarkets(); + * + * // Trading (requires auth) + * const baozi = new Baozi({ + * privateKey: process.env.BAOZI_PRIVATE_KEY + * }); + * ``` + */ +export class Baozi extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("baozi", options); + } +} + +/** + * Opinion exchange client. + * + * Polygon-based CLOB exchange. Public catalog endpoints work without + * credentials; trading requires `apiKey` (proxy address) and `privateKey`. + * + * @example + * ```typescript + * const opinion = new Opinion(); + * const events = await opinion.fetchEvents(); + * ``` + */ +export class Opinion extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("opinion", options); + } +} + +/** + * Metaculus exchange client. + * + * Forecasting platform. Public read-only access works without credentials; + * authenticated calls accept a bearer token via `apiKey`. + * + * @example + * ```typescript + * const metaculus = new Metaculus(); + * const events = await metaculus.fetchEvents(); + * ``` + */ +export class Metaculus extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("metaculus", options); + } +} + +/** + * Smarkets exchange client. + * + * UK-based betting exchange. Public catalog endpoints work without + * credentials; trading requires Smarkets account email (`apiKey`) and + * password (`privateKey`). + * + * @example + * ```typescript + * const smarkets = new Smarkets(); + * const events = await smarkets.fetchEvents(); + * ``` + */ +export class Smarkets extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("smarkets", options); + } +} + +/** + * Polymarket US exchange client. + * + * US-regulated Polymarket venue. Public catalog endpoints work without + * credentials; trading requires `apiKey` (keyId) and `privateKey` + * (secretKey) issued by Polymarket US. + * + * @example + * ```typescript + * const polyUs = new PolymarketUS(); + * const events = await polyUs.fetchEvents(); + * ``` + */ +export class PolymarketUS extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("polymarket_us", options); + } +} + +/** + * Gemini Titan exchange client. + * + * @example + * ```typescript + * const titan = new GeminiTitan(); + * const markets = await titan.fetchMarkets(); + * ``` + */ +export class GeminiTitan extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("gemini-titan", options); + } +} + +/** + * Hyperliquid exchange client. + * + * @example + * ```typescript + * const hl = new Hyperliquid(); + * const markets = await hl.fetchMarkets(); + * ``` + */ +export class Hyperliquid extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("hyperliquid", options); + } +} + +/** + * Options for the SuiBets exchange client. + */ +export interface SuiBetsOptions extends ExchangeOptions { + /** + * Sui wallet address (0x + 64 hex chars). + * Required for fetchPositions(). Can also be set via the + * SUIBETS_WALLET_ADDRESS environment variable on the sidecar. + */ + walletAddress?: string; +} + +/** + * SuiBets exchange client. + * + * SuiBets is a decentralised P2P sports betting exchange on Sui mainnet. + * No house edge. 2% platform fee. + * Contract: 0xd51fe151bec66a15b086a67c1cfce9b05759ddac1d73fcd3e14324ad202b2e59 + * + * @example + * ```typescript + * const suibets = new SuiBets(); + * const markets = await suibets.fetchMarkets({ limit: 20 }); + * + * // With wallet for fetchPositions() + * const me = new SuiBets({ walletAddress: '0xabc...' }); + * const positions = await me.fetchPositions(); + * ``` + */ +export class SuiBets extends Exchange { + private readonly _walletAddress?: string; + + constructor(options: SuiBetsOptions = {}) { + super("suibets", options); + this._walletAddress = options.walletAddress; + } + + /** + * Includes walletAddress in the credentials sent to the sidecar so + * that fetchPositions() can reach the /api/p2p/my endpoint. + * Falls back to SUIBETS_WALLET_ADDRESS env var on the sidecar side + * when walletAddress is not set here. + */ + protected override getCredentials(): ExchangeCredentials | undefined { + const base = super.getCredentials(); + if (!this._walletAddress) return base; + return { + ...(base ?? {}), + walletAddress: this._walletAddress, + } as ExchangeCredentials & { walletAddress: string }; + } +} + +// Backwards-compatible casing alias matching the Python SDK export. +export const Suibets = SuiBets; + +/** + * Rain exchange client. + * + * Rain is a permissionless AMM-plus-orderbook prediction market on Arbitrum One. + * Reads are unauthenticated; trading requires an EVM privateKey. + * + * @example + * ```typescript + * const rain = new Rain(); + * const markets = await rain.fetchMarkets({ limit: 20 }); + * + * // With wallet for trading + * const me = new Rain({ privateKey: '0x...' }); + * await me.createOrder({ marketId, outcomeId, side: 'buy', type: 'market', amount: 10 }); + * ``` + */ +export class Rain extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("rain", options); + } +} + + +/** + * Hunch exchange client. + * + * Hunch is a crypto-native prediction market. Reads are unauthenticated; + * trading requires an EVM private key. + */ +export class Hunch extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("hunch", options); + } +} + +/** + * Mock exchange client. + * + * Offline deterministic exchange for testing and development. + * No credentials required. + * + * @example + * ```typescript + * const mock = new Mock(); + * const markets = await mock.fetchMarkets(); + * ``` + */ +export class Mock extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("mock", options); + } +} +export { SqlResult, SqlMeta, SqlColumn } from './router.js'; +export { SqlResult, SqlMeta, SqlColumn } from './router.js'; + \ No newline at end of file diff --git a/sdks/typescript/pmxt/router.ts b/sdks/typescript/pmxt/router.ts index ce215daef90557dd561b31b539716d64de6f0a8c..be59b41d103dcb9ec66aa10a154126c8492ed5b0 100644 GIT binary patch literal 54138 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 Kn^nW;UiJUJvv^wo literal 24208 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