UNPKG

polynance_sdk

Version:

TypeScript SDK for prediction market aggregation supporting Polymarket, Limitless, and Truemarket

965 lines (964 loc) 45.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PolynanceSDK = void 0; exports.generatePriceChart = generatePriceChart; // src/core/client.ts const axios_1 = __importDefault(require("axios")); const wallet_1 = require("@ethersproject/wallet"); const providers_1 = require("@ethersproject/providers"); const ethers_1 = require("ethers"); const clob_client_1 = require("@polymarket/clob-client"); const panic_1 = require("./panic"); // Import from new error file const minimunAbi = { "usdc": [ "function approve(address, uint256) returns (bool)", "function allowance(address, address) view returns (uint256)", "function balanceOf(address) view returns (uint256)" ], "ctf": [ "function setApprovalForAll(address, bool) returns (bool)", "function isApprovedForAll(address, address) view returns (bool)", "function balanceOf(address, uint256) view returns (uint256)" ] }; // --- Polynance Client Class --- /** * The main client class for interacting with the Polynance API. * Provides methods to fetch prediction market data and subscribe to real-time events. */ class PolynanceSDK { /** * Creates an instance of the PolynanceClient. * @param options - Optional configuration for the client, such as API URLs and timeout. */ constructor(options) { this.pendingOrderIds = []; const apiBaseUrl = options?.apiBaseUrl || 'https://api.polynance.ag'; this.sseBaseUrl = options?.sseBaseUrl || 'https://api.polynance.ag'; // Default SSE URL const timeout = options?.timeout || 100000; // Default timeout 100s this.apiClient = axios_1.default.create({ baseURL: apiBaseUrl, timeout: timeout, headers: { 'Content-Type': 'application/json', }, }); this.wallet = options?.wallet; if (this.wallet && this.wallet instanceof providers_1.JsonRpcSigner) { if (options?.walletAddress) { this.walletAddress = options.walletAddress; } else { throw new Error("walletAddress is required when wallet is JsonRpcSigner"); } } this.polymarketClob = new clob_client_1.ClobClient("https://clob.polymarket.com/", clob_client_1.Chain.POLYGON); // Optional: Interceptors can also use handleError // this.apiClient.interceptors.response.use(response => response, error => { // return Promise.reject(this.handleError(error, 'AxiosInterceptor', { url: error.config?.url })); // }); } async initCreds(wallet) { try { const clobClient = new clob_client_1.ClobClient("https://clob.polymarket.com/", clob_client_1.Chain.POLYGON, wallet); let creds = await clobClient.deriveApiKey(); if (!creds.key) { console.log("[initCredsinitCreds] deriveApiKey failed, creating new api key"); creds = await clobClient.createApiKey(); } console.log("[initCredsinitCreds] initCreds", creds); this.polymarketClob = new clob_client_1.ClobClient("https://clob.polymarket.com/", clob_client_1.Chain.POLYGON, wallet, creds); } catch (e) { throw this.handleError(e, 'initCreds', {}); } } async buildOrder(params, wallet) { if (params.provider !== "polymarket") { throw new Error("Now Only Polymarket is supported"); } if (!this.polymarketClob.creds) { const w = wallet || this.wallet; if (!w) { throw new Error("Wallet is required to execute order"); } await this.initCreds(w); } const exchange = await (async () => { try { const isSlug = params.marketIdOrSlug.includes("-"); if (isSlug) { const exchange = await this.getExchangeBySlug(params.marketIdOrSlug); return exchange[0]; } else { return await this.getExchange("polymarket", params.marketIdOrSlug); } } catch (e) { console.log(e); return null; } })(); if (!exchange) { throw this.handleError(new Error("Exchange not found"), 'buildOrder', { params }); } const uo = await (async () => { try { const positionToken = exchange.position_tokens.find((pt) => pt.name.toLowerCase() == params.positionIdOrName.toLowerCase()); if (!positionToken) { throw new Error("Position token not found"); } const price = params.price ? params.price : Number(positionToken.price); const size = params.size ? params.size : params.usdcFlowAbs / price; console.log("report of ctf tokenQty", params.buyOrSell == "BUY" ? size : -size); console.log("usdcFlow ", params.usdcFlowAbs); console.log(` $${price};${price * size}==${params.usdcFlowAbs}`); const userOrder = { ...params, tokenID: positionToken.token_id, side: params.buyOrSell == "BUY" ? clob_client_1.Side.BUY : clob_client_1.Side.SELL, price: price, size: size, }; return userOrder; } catch (e) { return null; } })(); if (!uo) { throw this.handleError(new Error("UserOrder not found"), 'buildOrder', { params }); } try { const signedOrder = await this.polymarketClob.createOrder(uo); return signedOrder; } catch (e) { throw this.handleError(e, 'buildOrder', { userOrder: uo }); } } async executeOrder(order, orderType = clob_client_1.OrderType.GTC, rpcProvider, wallet) { try { if (!wallet && !this.wallet) { throw new Error("Wallet is required to approve allowance"); } if (!this.wallet?.provider && !rpcProvider) { throw new Error("Wallet is required to execute order"); } const provider = this.wallet?.provider ? this.wallet : rpcProvider; if (!provider) throw new Error("Provider is required to execute order"); await this.approveAllowanceBalance(provider); const res = await this.polymarketClob.postOrder(order, orderType); if (res?.orderID) { const op = await this.polymarketClob.getOrder(res.orderID); if (op.status.toLowerCase() !== "matched") { this.pendingOrderIds.push(res.orderID); } return op; } this.proposePrice(order); return res; } catch (e) { this.handleError(e, 'executeOrder', { order }); return null; } } getPendingOrdersIds() { return [...this.pendingOrderIds]; } async waitOrderMatched(orderId) { try { const op = await this.polymarketClob.getOrder(orderId); return op.status.toLowerCase() === "matched"; } catch (e) { return false; } } async approveAllowanceBalance(provider) { try { const contractConfig = (0, clob_client_1.getContractConfig)(clob_client_1.Chain.POLYGON); //TODO const walletAddress = provider instanceof wallet_1.Wallet ? await provider.getAddress() : this.walletAddress; const usdc = new ethers_1.ethers.Contract(contractConfig.collateral, minimunAbi["usdc"], provider); const ctf = new ethers_1.ethers.Contract(contractConfig.conditionalTokens, minimunAbi["ctf"], provider); const usdcAllowanceNegRiskAdapterPromise = usdc.allowance(walletAddress, contractConfig.negRiskAdapter); const usdcAllowanceNegRiskExchangePromise = usdc.allowance(walletAddress, contractConfig.negRiskExchange); const conditionalTokensAllowanceNegRiskExchangePromise = ctf.isApprovedForAll(walletAddress, contractConfig.negRiskExchange); const conditionalTokensAllowanceNegRiskAdapterPromise = ctf.isApprovedForAll(walletAddress, contractConfig.negRiskAdapter); const usdcBalancePromise = usdc.balanceOf(walletAddress); const [usdcAllowanceNegRiskAdapter, usdcAllowanceNegRiskExchange, conditionalTokensAllowanceNegRiskExchange, conditionalTokensAllowanceNegRiskAdapter, usdcBalance,] = await Promise.all([ usdcAllowanceNegRiskAdapterPromise, usdcAllowanceNegRiskExchangePromise, conditionalTokensAllowanceNegRiskExchangePromise, conditionalTokensAllowanceNegRiskAdapterPromise, usdcBalancePromise, ]); let txn; if (!usdcAllowanceNegRiskAdapter.gt(ethers_1.constants.Zero)) { txn = await usdc.approve(contractConfig.negRiskAdapter, ethers_1.constants.MaxUint256, { gasPrice: 100000000000, gasLimit: 200000, }); console.log(`[USDC->NegRiskAdapter]: ${txn.hash}`); } if (!usdcAllowanceNegRiskExchange.gt(ethers_1.constants.Zero)) { txn = await usdc.approve(contractConfig.negRiskExchange, ethers_1.constants.MaxUint256, { gasPrice: 100000000000, gasLimit: 200000, }); console.log(`[USDC->NegRiskExchange]: ${txn.hash}`); } if (!conditionalTokensAllowanceNegRiskExchange) { txn = await ctf.setApprovalForAll(contractConfig.negRiskExchange, true, { gasPrice: 100000000000, gasLimit: 200000, }); console.log(`[CTF->NegRiskExchange]: ${txn.hash}`); } if (!conditionalTokensAllowanceNegRiskAdapter) { txn = await ctf.setApprovalForAll(contractConfig.negRiskAdapter, true, { gasPrice: 100000000000, gasLimit: 200000, }); console.log(`[CTF->NegRiskAdapter]: ${txn.hash}`); } console.log(txn ? txn.hash : "allowance already set"); return Number(usdcBalance.toString()); } catch (e) { throw this.handleError(e, 'approveAllowance', {}); } } async getConditionalTokensBalance(tokenId, walletAddress) { const contractConfig = (0, clob_client_1.getContractConfig)(clob_client_1.Chain.POLYGON); if (!this.wallet) { throw new Error("Wallet is required to get balance"); } const adder = walletAddress || this.walletAddress || this.wallet.getAddress(); const ctf = new ethers_1.ethers.Contract(contractConfig.conditionalTokens, minimunAbi["ctf"], this.wallet); const balance = await ctf.balanceOf(adder, tokenId); return Number(balance.toString()); } async getUSDCBalance(walletAddress) { const contractConfig = (0, clob_client_1.getContractConfig)(clob_client_1.Chain.POLYGON); if (!this.wallet) { throw new Error("Wallet is required to get balance"); } const adder = walletAddress || this.walletAddress || this.wallet.getAddress(); const usdc = new ethers_1.ethers.Contract(contractConfig.collateral, minimunAbi["usdc"], this.wallet); const balance = await usdc.balanceOf(adder); return Number(balance.toString()); } async proposePrice(order) { try { const polyOrder = this.toPolyOrder(order); const res = await this.apiClient.post("/v1/proposePrice", { order: polyOrder }); return res; } catch (e) { return null; } } async verifyPrice() { try { await this.apiClient.post("/v1/verifyPrice"); } catch (e) { return null; } } async scanPendingPriceData() { try { const res = await this.apiClient.get("/v1/scanPendingPriceData"); return res.data.result; } catch (e) { this.handleError(e, 'scanPendingPriceData'); return false; } } toPolyOrder(o) { return { salt: o.salt, maker: o.maker, signer: o.signer, taker: o.taker, tokenId: o.tokenId, makerAmount: o.makerAmount, takerAmount: o.takerAmount, expiration: o.expiration, nonce: o.nonce, feeRateBps: o.feeRateBps.toString(), side: o.side.toString(), signatureType: o.signatureType.toString(), signature: o.signature, }; } /** * Handles errors, logs them, and wraps them in a PolynanceApiError. * @param error - The error object caught. * @param methodName - The name of the method where the error originated. * @param context - Additional context about the operation (e.g., parameters). * @returns A PolynanceApiError instance. * @private */ handleError(error, methodName, context) { if (error instanceof panic_1.PolynanceApiError) { // If it's already our custom error, just log and return it. console.error(`Polynance SDK Error (already wrapped): ${error.summary}`, error); // Log summary return error; } let code; let message; let statusCode; let responseData; let originalError = error instanceof Error ? error : undefined; if (axios_1.default.isAxiosError(error)) { statusCode = error.response?.status; responseData = error.response?.data; originalError = error; // Ensure originalError is set // Add request URL to context if available const errorContext = { ...context, url: error.config?.url, requestMethod: error.config?.method?.toUpperCase() }; if (error.code === 'ECONNABORTED' || error.message.toLowerCase().includes('timeout')) { code = panic_1.PolynanceErrorCode.TIMEOUT_ERROR; message = `API request timed out.`; } else if (error.response) { // Error with a response status code message = `API request failed with status ${statusCode}.`; switch (statusCode) { case 400: code = panic_1.PolynanceErrorCode.INVALID_PARAMETER; break; // Or more specific based on responseData case 401: code = panic_1.PolynanceErrorCode.UNAUTHORIZED; break; case 403: code = panic_1.PolynanceErrorCode.FORBIDDEN; break; case 404: code = panic_1.PolynanceErrorCode.NOT_FOUND; break; case 429: code = panic_1.PolynanceErrorCode.RATE_LIMIT_EXCEEDED; break; case 500: case 501: case 502: case 503: case 504: code = panic_1.PolynanceErrorCode.SERVER_ERROR; break; default: code = panic_1.PolynanceErrorCode.API_REQUEST_FAILED; break; } // Include server message if available if (responseData?.message) { message += ` Server message: ${responseData.message}`; } else if (responseData?.error) { message += ` Server error: ${responseData.error}`; } } else if (error.request) { // Request was made but no response received code = panic_1.PolynanceErrorCode.NETWORK_ERROR; message = `Network error: No response received from the API server.`; } else { // Error setting up the request code = panic_1.PolynanceErrorCode.API_REQUEST_FAILED; message = `Failed to setup the API request: ${error.message}`; } const apiError = new panic_1.PolynanceApiError(message, code, { cause: originalError, methodName, statusCode, responseData, context: errorContext }); console.error(`Polynance SDK Error: ${apiError.summary}`, apiError); // Log summary and full error object return apiError; } else { // Unexpected non-Axios error code = panic_1.PolynanceErrorCode.INTERNAL_SDK_ERROR; message = `An unexpected internal SDK error occurred.`; originalError = error instanceof Error ? error : new Error(String(error)); message += ` Details: ${originalError.message}`; const apiError = new panic_1.PolynanceApiError(message, code, { cause: originalError, methodName, context }); console.error(`Polynance SDK Error: ${apiError.summary}`, apiError); // Log summary and full error object return apiError; } } asContext(data, prompt) { const indentSize = 2; const prefix = prompt ? `\n${prompt}\n------\n` : ""; const pad = (lvl) => " ".repeat(lvl * indentSize); const defaultFormatter = (path, value, level) => `${pad(level)}${path} : ${String(value)}`; const fmt = defaultFormatter; const skipUndefined = true; const walk = (value, path, level, out) => { if (value === null || typeof value !== "object") { const line = fmt(path.join("."), value, level); if (line !== null) out.push(line); return; } if (Array.isArray(value)) { value.forEach((v, i) => walk(v, [...path, `[${i}]`], level, out)); return; } const keys = Object.keys(value); keys.sort(); for (const k of keys) { const v = value[k]; if (v === undefined && skipUndefined) continue; walk(v, [...path, k], level + 1, out); } }; const lines = []; walk(data, [], 0, lines); return prefix + lines.join("\n"); } /** * Retrieves detailed information for a specific market by its ID and prediction provider. * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param marketId - The unique identifier of the market. * @returns A Promise resolving to the `Market` object. * @throws {PolynanceApiError} If parameters are invalid or the API request fails. */ async getMarket(protocol, marketId) { const methodName = 'getMarket'; const context = { protocol, marketId: marketId ? '***' : marketId }; // Mask potentially long ID if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!marketId) { throw new panic_1.PolynanceApiError("Missing required parameter 'marketId'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/events/${marketId}`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves detailed information for a specific exchange by its ID and prediction provider. * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param exchangeId - The unique identifier of the exchange. * @returns A Promise resolving to the `Exchange` object. * @throws {PolynanceApiError} If parameters are invalid or the API request fails. */ async getExchange(protocol, exchangeId) { const methodName = 'getExchange'; const context = { protocol, exchangeId: exchangeId ? '***' : exchangeId }; // Mask potentially long ID if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!exchangeId) { throw new panic_1.PolynanceApiError("Missing required parameter 'exchangeId'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/markets/${exchangeId}`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves a list of currently active markets for a specific prediction provider. * Supports pagination. * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param page - The page number to retrieve (1-based). Defaults to 1. * @param limit - The maximum number of markets per page. Defaults to 50. * @returns A Promise resolving to an array of `Market` objects. * @throws {PolynanceApiError} If parameters are invalid or the API request fails. */ async getActiveMarkets(protocol, page = 1, limit = 50) { const methodName = 'getActiveMarkets'; const context = { protocol, page, limit }; if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (page < 1) { throw new panic_1.PolynanceApiError("Parameter 'page' must be 1 or greater.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (limit < 1) { throw new panic_1.PolynanceApiError("Parameter 'limit' must be 1 or greater.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get('/v1/ongoing-events', { params: { protocol, page, limit }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves a list of discussions associated with a specific market. * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param marketId - The unique identifier of the market. * @returns A Promise resolving to an array of `MarketDiscussion` objects. * @throws {PolynanceApiError} If parameters are invalid or the API request fails. */ async getMarketDiscussions(protocol, marketId) { const methodName = 'getMarketDiscussions'; const context = { protocol, marketId: marketId ? '***' : marketId }; if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!marketId) { throw new panic_1.PolynanceApiError("Missing required parameter 'marketId'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/events/${marketId}/comments`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves the current order book summary for a specific exchange. * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param exchangeId - The unique identifier of the exchange. * @returns A Promise resolving to a Record mapping asset IDs to `OrderBookSummary` objects. * @throws {PolynanceApiError} If parameters are invalid or the API request fails. */ async getOrderbook(protocol, exchangeId) { const methodName = 'getOrderbook'; const context = { protocol, exchangeId: exchangeId ? '***' : exchangeId }; if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!exchangeId) { throw new panic_1.PolynanceApiError("Missing required parameter 'exchangeId'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/markets/${exchangeId}/orderbook`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves the historical price history for all position tokens in a specific exchange. * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param exchangeId - The unique identifier of the exchange. * @returns A Promise resolving to a 2D array of `TradeRecord`, organized by position token index. * @throws {PolynanceApiError} If parameters are invalid or the API request fails. */ async getPriceHistory(protocol, exchangeId) { const methodName = 'getPriceHistory'; const context = { protocol, exchangeId: exchangeId ? '***' : exchangeId }; if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!exchangeId) { throw new panic_1.PolynanceApiError("Missing required parameter 'exchangeId'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/markets/${exchangeId}/orderbook/filledevents`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } async getTrader(protocol, traderAddress) { const methodName = 'getTrader'; const context = { protocol, traderAddress: traderAddress ? '***' : traderAddress }; // Mask potentially long address if (!traderAddress) { throw new panic_1.PolynanceApiError("Missing required parameter 'traderAddress'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/trader/${traderAddress}`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } async traderPositions(protocol, traderAddress) { const methodName = 'traderPositions'; const context = { protocol, traderAddress: traderAddress ? '***' : traderAddress }; // Mask potentially long address if (!traderAddress) { throw new panic_1.PolynanceApiError("Missing required parameter 'traderAddress'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get(`/v1/trader/${traderAddress}/positions`, { params: { protocol }, }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves a list of all available market slugs across all prediction providers. * Supports pagination. Slugs are URL-friendly identifiers for markets. * @param page - The page number to retrieve (1-based). Defaults to 1. * @param limit - The maximum number of slugs per page. Defaults to 100. * @returns A Promise resolving to an array of market slug strings. * @throws {PolynanceApiError} If the API request fails. */ async getSlugs(page = 1, limit = 100) { const methodName = 'getSlugs'; const context = { page, limit }; if (page < 1) { throw new panic_1.PolynanceApiError("Parameter 'page' must be 1 or greater.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (limit < 1) { throw new panic_1.PolynanceApiError("Parameter 'limit' must be 1 or greater.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get('/v1/agg/sluglist', { params: { page, limit } }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } } /** * Retrieves market information using its unique slug. * A single slug might resolve to multiple markets if the same market exists on different prediction providers. * @param slug - The URL-friendly identifier of the market. * @returns A Promise resolving to an array of `Market` objects matching the slug. * @throws {PolynanceApiError} If the slug is missing or the API request fails. */ async getMarketBySlug(slug) { const methodName = 'getMarketBySlug'; const context = { slug: slug ? '***' : slug }; // Mask potentially long slug if (!slug) { throw new panic_1.PolynanceApiError("Missing required parameter 'slug'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get('/v1/agg', { params: { slug } }); // Optionally, check for 404 specifically if desired if (response.status === 404 || response.data.length === 0) { throw new panic_1.PolynanceApiError(`Market with slug '${slug}' not found.`, panic_1.PolynanceErrorCode.NOT_FOUND, { methodName, context, statusCode: 404 }); } return response.data; } catch (error) { // If it was an Axios 404, handleError will set NOT_FOUND code throw this.handleError(error, methodName, context); } } async getExchangeBySlug(slug) { const methodName = 'getExchangeBySlug'; const context = { slug: slug ? '***' : slug }; // Mask potentially long slug if (!slug) { throw new panic_1.PolynanceApiError("Missing required parameter 'slug'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const response = await this.apiClient.get('/v1/agg/market', { params: { slug } }); // Optionally, check for 404 specifically if desired if (response.status === 404 || response.data.length === 0) { throw new panic_1.PolynanceApiError(`Exchange with slug '${slug}' not found.`, panic_1.PolynanceErrorCode.NOT_FOUND, { methodName, context, statusCode: 404 }); } return response.data; } catch (error) { // If it was an Axios 404, handleError will set NOT_FOUND code throw this.handleError(error, methodName, context); } } /** * Searches for prediction markets using a natural language query. * Allows filtering by prediction provider, comment inclusion, result count, and similarity threshold. * @param query - The search query string (e.g., "Who will win the next US election?"). * @param filter - Optional filtering parameters (`SearchFilter`). * @returns A Promise resolving to an array of `MarketMatchResult` objects, sorted by relevance. * @throws {PolynanceApiError} If the query is missing or the API request fails. */ async search(query, filter) { const methodName = 'search'; const context = { query: query ? `"${query.substring(0, 50)}${query.length > 50 ? '...' : ''}"` : query, filter }; // Log truncated query if (!query) { throw new panic_1.PolynanceApiError("Missing required parameter 'query'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } try { const params = { query }; if (filter) { if (filter.topK !== undefined) params.topK = filter.topK; if (filter.protocols !== undefined && filter.protocols.length > 0) params.protocols = filter.protocols.join(','); if (filter.isIncludeComment !== undefined) params.isIncludeComment = filter.isIncludeComment; if (filter.threshold !== undefined) params.threshold = filter.threshold; } const response = await this.apiClient.get('/v1/agg/retrieve', { params }); return response.data; } catch (error) { throw this.handleError(error, methodName, context); } function asContext(data) { const indentSize = 2; const pad = (lvl) => " ".repeat(lvl * indentSize); const defaultFormatter = (path, value, level) => `${pad(level)}${path} : ${String(value)}`; const fmt = defaultFormatter; const skipUndefined = true; const walk = (value, path, level, out) => { if (value === null || typeof value !== "object") { const line = fmt(path.join("."), value, level); if (line !== null) out.push(line); return; } if (Array.isArray(value)) { value.forEach((v, i) => walk(v, [...path, `[${i}]`], level, out)); return; } const keys = Object.keys(value); keys.sort(); for (const k of keys) { const v = value[k]; if (v === undefined && skipUndefined) continue; walk(v, [...path, k], level + 1, out); } }; const lines = []; walk(data, [], 0, lines); return lines.join("\n"); } } /** * Subscribes to real-time trade updates for a specific exchange or identifier via Server-Sent Events (SSE). * * **Note:** This requires a browser environment or a Node.js environment with an `EventSource` polyfill. * * @param protocol - The prediction provider identifier (e.g., 'polymarket'). * @param id - The identifier for the event stream, typically the exchange ID. * @param handlers - Optional callback functions for handling SSE lifecycle events (`onOpen`, `onMessage`, `onError`). * @returns A `TradeSubscription` object containing the `EventSource` instance and methods to control the subscription. * @throws {PolynanceApiError} If `EventSource` is unavailable or parameters are invalid. */ subscribeToTrades(protocol, id, handlers) { const methodName = 'subscribeToTrades'; const context = { protocol, id: id ? '***' : id }; // Check for EventSource availability if (typeof EventSource === 'undefined') { throw new panic_1.PolynanceApiError("EventSource is not available in this environment. Ensure you are in a browser or have a suitable polyfill.", panic_1.PolynanceErrorCode.ENVIRONMENT_ERROR, { methodName, context }); } // Validate parameters if (!protocol) { throw new panic_1.PolynanceApiError("Missing required parameter 'protocol'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } if (!id) { throw new panic_1.PolynanceApiError("Missing required parameter 'id'.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context }); } const url = `${this.sseBaseUrl}/sse/fillevent?protocol=${protocol}&id=${id}`; let eventSource; try { eventSource = new EventSource(url); } catch (error) { // Catch potential synchronous errors during EventSource creation const initError = this.handleError(error, methodName, { ...context, url }); // Ensure it has a relevant code if generic if (initError.code === panic_1.PolynanceErrorCode.INTERNAL_SDK_ERROR) { console.error("EventSource failed:", initError); } throw initError; } let latestData = null; // --- SSE Event Listeners --- eventSource.onopen = (ev) => { console.log(`SSE connection opened: ${protocol}/${id}`); if (handlers?.onOpen) { try { handlers.onOpen(ev); } catch (handlerError) { console.error("Error in SSE 'onOpen' handler:", this.handleError(handlerError, `${methodName}.onOpen`, context)); } } }; eventSource.onmessage = (event) => { try { if (typeof event.data !== 'string') { throw new Error('Received non-string SSE message data.'); // Convert to error for unified handling } const data = JSON.parse(event.data); if (typeof data.price !== 'number' || typeof data.volumeBase !== 'number' || typeof data.timestamp !== 'number') { throw new Error('Received SSE message with unexpected data structure.'); // Validation error } latestData = data; if (handlers?.onMessage) { try { handlers.onMessage(data); } catch (handlerError) { console.error("Error in SSE 'onMessage' handler:", this.handleError(handlerError, `${methodName}.onMessage`, context)); // Optionally, trigger onError as well if a handler error is critical // if (handlers.onError) { ... } } } } catch (error) { const parseError = new panic_1.PolynanceApiError(`Failed to process SSE message: ${error instanceof Error ? error.message : String(error)}`, panic_1.PolynanceErrorCode.SSE_MESSAGE_ERROR, { methodName: `${methodName}.onMessage`, cause: error instanceof Error ? error : undefined, context: { ...context, rawData: event.data?.substring(0, 100) } // Include snippet of raw data }); console.error(parseError.summary, parseError); // Log the parsing error if (handlers?.onError) { try { handlers.onError(parseError); } catch (handlerError) { console.error("Error calling SSE 'onError' handler after message error:", this.handleError(handlerError, `${methodName}.onError`, context)); } } } }; eventSource.onerror = (ev) => { // Create a PolynanceApiError to pass to the handler const isClosed = eventSource.readyState === EventSource.CLOSED; const errorCode = isClosed ? panic_1.PolynanceErrorCode.SSE_CLOSED : panic_1.PolynanceErrorCode.SSE_CONNECTION_FAILED; const errorMessage = isClosed ? `SSE connection closed unexpectedly for ${protocol}/${id}.` : `SSE connection error occurred for ${protocol}/${id}.`; const sseError = new panic_1.PolynanceApiError(errorMessage, errorCode, { methodName: `${methodName}.onError`, cause: new Error(`SSE Error Event: ${JSON.stringify(ev)}`), // Wrap original event info context }); console.error(sseError.summary, sseError); // Log the error if (handlers?.onError) { try { handlers.onError(sseError); } catch (handlerError) { console.error("Error calling SSE 'onError' handler:", this.handleError(handlerError, `${methodName}.onError`, context)); } } if (isClosed) { // Optionally implement automatic reconnection logic here if desired console.warn(`SSE connection for ${protocol}/${id} is closed. Automatic reconnection not implemented.`); } }; // --- Subscription Control Methods --- const close = () => { if (eventSource && eventSource.readyState !== EventSource.CLOSED) { console.log(`Closing SSE connection: ${protocol}/${id}...`); eventSource.close(); } }; return { eventSource, close, getLatestData: () => latestData, }; } } // End of PolynanceClient class exports.PolynanceSDK = PolynanceSDK; // --- Utility Functions --- /** * Generates price chart data (OHLCV) from a list of trade records. * * @param tradeRecords - An array of `TradeRecord` objects representing trades. Assumes timestamps are in **seconds**. * @param intervalMillis - The desired candlestick interval duration in **milliseconds**. * @param fromTimeMillis - The start timestamp (Unix milliseconds) for the desired data range (inclusive). * @param toTimeMillis - The end timestamp (Unix milliseconds) for the desired data range (exclusive). * @returns An array of `Candle` objects, sorted by time. Returns an empty array if no valid events fall within the range. * @throws {PolynanceApiError} if intervalMillis is not positive. */ function generatePriceChart(tradeRecords, intervalMillis, fromTimeMillis, toTimeMillis) { const methodName = 'generatePriceChart'; if (intervalMillis <= 0) { throw new panic_1.PolynanceApiError("Candlestick intervalMillis must be positive.", panic_1.PolynanceErrorCode.INVALID_PARAMETER, { methodName, context: { intervalMillis } }); } if (!tradeRecords || tradeRecords.length === 0) { return []; } // Filter and sort events (ensure timestamps are handled correctly) const filteredRecords = tradeRecords .filter(record => typeof record.timestamp === 'number' && typeof record.price === 'number' && typeof record.volumeBase === 'number' && record.timestamp * 1000 >= fromTimeMillis && record.timestamp * 1000 < toTimeMillis) .map(record => ({ timestampMillis: record.timestamp * 1000, price: record.price, volumeBase: record.volumeBase })) .sort((a, b) => a.timestampMillis - b.timestampMillis); if (filteredRecords.length === 0) { return []; } const candleMap = new Map(); for (const record of filteredRecords) { const bucketStartTimeMillis = Math.floor(record.timestampMillis / intervalMillis) * intervalMillis; const bucketStartTimeSeconds = Math.floor(bucketStartTimeMillis / 1000); const existingCandle = candleMap.get(bucketStartTimeSeconds); if (!existingCandle) { candleMap.set(bucketStartTimeSeconds, { time: bucketStartTimeSeconds, open: record.price, high: record.price, low: record.price, close: record.price, volume: record.volumeBase, }); } else { existingCandle.high = Math.max(existingCandle.high, record.price); existingCandle.low = Math.min(existingCandle.low, record.price); existingCandle.close = record.price; // Last price updates close existingCandle.volume += record.volumeBase; } } // Convert map values to array and sort const candles = Array.from(candleMap.values()).sort((a, b) => a.time - b.time); return candles; }