polynance_sdk
Version:
TypeScript SDK for prediction market aggregation supporting Polymarket, Limitless, and Truemarket
965 lines (964 loc) • 45.9 kB
JavaScript
;
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;
}