polynance_sdk
Version:
TypeScript SDK for prediction market aggregation supporting Polymarket, Limitless, and Truemarket
1,084 lines (972 loc) • 46.9 kB
text/typescript
// src/core/client.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
import { Wallet } from "@ethersproject/wallet";
import { JsonRpcProvider, JsonRpcSigner } from "@ethersproject/providers";
import { constants, ethers } from "ethers";
import { Chain, ClobClient, getContractConfig, OpenOrder, OrderType, Side,UserOrder } from '@polymarket/clob-client';
import {SignedOrder} from "@polymarket/order-utils";
import { PolynanceApiError, PolynanceErrorCode } from './panic'; // Import from new error file
import {
PredictionProvider,
Market,
MarketDiscussion,
OrderBookSummary,
TradeRecord,
Exchange,
SearchFilter,
Trader,
TraderPosition,
ExecuteOrderParams,
TradeUpdateHandlers,
TradeSubscription,
MarketMatchResult,
PolynanceClientOptions,
Candle,
PolyOrder
} from './types';
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.
*/
export class PolynanceSDK {
private apiClient: AxiosInstance;
private sseBaseUrl: string;
public polymarketClob: ClobClient;
private wallet?: Wallet | JsonRpcSigner;
private walletAddress?: string;
private pendingOrderIds: string[] = [];
/**
* Creates an instance of the PolynanceClient.
* @param options - Optional configuration for the client, such as API URLs and timeout.
*/
constructor(options?: PolynanceClientOptions) {
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.create({
baseURL: apiBaseUrl,
timeout: timeout,
headers: {
'Content-Type': 'application/json',
},
});
this.wallet = options?.wallet;
if(this.wallet && this.wallet instanceof JsonRpcSigner) {
if(options?.walletAddress) {
this.walletAddress = options.walletAddress;
}else {
throw new Error("walletAddress is required when wallet is JsonRpcSigner");
}
}
this.polymarketClob = new ClobClient("https://clob.polymarket.com/", 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 }));
// });
}
public async initCreds(wallet: JsonRpcSigner|Wallet) {
try {
const clobClient = new ClobClient(
"https://clob.polymarket.com/",
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 ClobClient(
"https://clob.polymarket.com/",
Chain.POLYGON,
wallet,
creds
)
}catch(e) {
throw this.handleError(e, 'initCreds', {});
}
}
public async buildOrder(params: ExecuteOrderParams, wallet?: JsonRpcSigner|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: UserOrder = {
...params,
tokenID: positionToken.token_id,
side: params.buyOrSell=="BUY" ? Side.BUY : 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 });
}
}
public async executeOrder(order: SignedOrder,orderType: OrderType=OrderType.GTC,rpcProvider?: JsonRpcProvider,wallet?: JsonRpcSigner|Wallet): Promise<OpenOrder|any> {
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;
}
}
public getPendingOrdersIds(): string[] {
return [...this.pendingOrderIds];
}
public async waitOrderMatched(orderId: string): Promise<boolean> {
try {
const op = await this.polymarketClob.getOrder(orderId);
return op.status.toLowerCase() === "matched";
}catch(e) {
return false;
}
}
private async approveAllowanceBalance(
provider: JsonRpcProvider|Wallet|JsonRpcSigner,
) {
try{
const contractConfig = getContractConfig(Chain.POLYGON);
//TODO
const walletAddress = provider instanceof Wallet ? await provider.getAddress() : this.walletAddress;
const usdc = new ethers.Contract(contractConfig.collateral, minimunAbi["usdc"], provider);
const ctf = new 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(constants.Zero)) {
txn = await usdc.approve(contractConfig.negRiskAdapter, constants.MaxUint256, {
gasPrice: 100_000_000_000,
gasLimit: 200_000,
});
console.log(`[USDC->NegRiskAdapter]: ${txn.hash}`);
}
if (!usdcAllowanceNegRiskExchange.gt(constants.Zero)) {
txn = await usdc.approve(contractConfig.negRiskExchange, constants.MaxUint256, {
gasPrice: 100_000_000_000,
gasLimit: 200_000,
});
console.log(`[USDC->NegRiskExchange]: ${txn.hash}`);
}
if (!conditionalTokensAllowanceNegRiskExchange) {
txn = await ctf.setApprovalForAll(contractConfig.negRiskExchange, true, {
gasPrice: 100_000_000_000,
gasLimit: 200_000,
});
console.log(`[CTF->NegRiskExchange]: ${txn.hash}`);
}
if (!conditionalTokensAllowanceNegRiskAdapter) {
txn = await ctf.setApprovalForAll(contractConfig.negRiskAdapter, true, {
gasPrice: 100_000_000_000,
gasLimit: 200_000,
});
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', {});
}
}
public async getConditionalTokensBalance(tokenId: string,walletAddress?: string) {
const contractConfig = getContractConfig(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.Contract(contractConfig.conditionalTokens, minimunAbi["ctf"], this.wallet);
const balance = await ctf.balanceOf(adder, tokenId);
return Number(balance.toString());
}
public async getUSDCBalance(walletAddress?: string) {
const contractConfig = getContractConfig(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.Contract(contractConfig.collateral, minimunAbi["usdc"], this.wallet);
const balance = await usdc.balanceOf(adder);
return Number(balance.toString());
}
public async proposePrice(order: SignedOrder) {
try {
const polyOrder = this.toPolyOrder(order);
const res = await this.apiClient.post("/v1/proposePrice", {order: polyOrder});
return res;
}catch(e) {
return null;
}
}
public async verifyPrice() {
try {
await this.apiClient.post("/v1/verifyPrice");
}catch(e) {
return null;
}
}
public async scanPendingPriceData() {
try {
const res = await this.apiClient.get<{result: boolean}>("/v1/scanPendingPriceData");
return res.data.result;
}catch(e) {
this.handleError(e, 'scanPendingPriceData');
return false;
}
}
private toPolyOrder(o: SignedOrder): PolyOrder {
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
*/
private handleError(error: any, methodName: string, context?: Record<string, any>): PolynanceApiError {
if (error instanceof 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: PolynanceErrorCode;
let message: string;
let statusCode: number | undefined;
let responseData: any;
let originalError: Error | AxiosError | undefined = error instanceof Error ? error : undefined;
if (axios.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 = 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 = PolynanceErrorCode.INVALID_PARAMETER; break; // Or more specific based on responseData
case 401: code = PolynanceErrorCode.UNAUTHORIZED; break;
case 403: code = PolynanceErrorCode.FORBIDDEN; break;
case 404: code = PolynanceErrorCode.NOT_FOUND; break;
case 429: code = PolynanceErrorCode.RATE_LIMIT_EXCEEDED; break;
case 500: case 501: case 502: case 503: case 504:
code = PolynanceErrorCode.SERVER_ERROR; break;
default: code = 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 = PolynanceErrorCode.NETWORK_ERROR;
message = `Network error: No response received from the API server.`;
} else {
// Error setting up the request
code = PolynanceErrorCode.API_REQUEST_FAILED;
message = `Failed to setup the API request: ${error.message}`;
}
const apiError = new 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 = 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 PolynanceApiError(message, code, {
cause: originalError,
methodName,
context
});
console.error(`Polynance SDK Error: ${apiError.summary}`, apiError); // Log summary and full error object
return apiError;
}
}
public asContext<T>(
data: T,
prompt?: string
): string {
const indentSize = 2;
const prefix = prompt ? `\n${prompt}\n------\n` : "";
const pad = (lvl: number) => " ".repeat(lvl * indentSize);
const defaultFormatter = (path: string, value: unknown, level: number) =>
`${pad(level)}${path} : ${String(value)}`;
const fmt = defaultFormatter;
const skipUndefined = true
const walk = (value: unknown, path: string[], level: number, out: string[]) => {
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 as Record<string, unknown>);
keys.sort();
for (const k of keys) {
const v = (value as Record<string, unknown>)[k];
if (v === undefined && skipUndefined) continue;
walk(v, [...path, k], level + 1, out);
}
};
const lines: string[] = [];
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: PredictionProvider, marketId: string): Promise<Market> {
const methodName = 'getMarket';
const context = { protocol, marketId: marketId ? '***' : marketId }; // Mask potentially long ID
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!marketId) {
throw new PolynanceApiError("Missing required parameter 'marketId'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Market>(`/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: PredictionProvider, exchangeId: string): Promise<Exchange> {
const methodName = 'getExchange';
const context = { protocol, exchangeId: exchangeId ? '***' : exchangeId }; // Mask potentially long ID
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!exchangeId) {
throw new PolynanceApiError("Missing required parameter 'exchangeId'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Exchange>(`/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: PredictionProvider, page: number = 1, limit: number = 50): Promise<Market[]> {
const methodName = 'getActiveMarkets';
const context = { protocol, page, limit };
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (page < 1) {
throw new PolynanceApiError("Parameter 'page' must be 1 or greater.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (limit < 1) {
throw new PolynanceApiError("Parameter 'limit' must be 1 or greater.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Market[]>('/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: PredictionProvider, marketId: string): Promise<MarketDiscussion[]> {
const methodName = 'getMarketDiscussions';
const context = { protocol, marketId: marketId ? '***' : marketId };
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!marketId) {
throw new PolynanceApiError("Missing required parameter 'marketId'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<MarketDiscussion[]>(`/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: PredictionProvider, exchangeId: string): Promise<Record<string, OrderBookSummary>> {
const methodName = 'getOrderbook';
const context = { protocol, exchangeId: exchangeId ? '***' : exchangeId };
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!exchangeId) {
throw new PolynanceApiError("Missing required parameter 'exchangeId'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Record<string, OrderBookSummary>>(`/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: PredictionProvider, exchangeId: string): Promise<TradeRecord[][]> {
const methodName = 'getPriceHistory';
const context = { protocol, exchangeId: exchangeId ? '***' : exchangeId };
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!exchangeId) {
throw new PolynanceApiError("Missing required parameter 'exchangeId'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<TradeRecord[][]>(`/v1/markets/${exchangeId}/orderbook/filledevents`, {
params: { protocol },
});
return response.data;
} catch (error) {
throw this.handleError(error, methodName, context);
}
}
public async getTrader(protocol: PredictionProvider,traderAddress: string): Promise<Trader> {
const methodName = 'getTrader';
const context = { protocol, traderAddress: traderAddress ? '***' : traderAddress }; // Mask potentially long address
if (!traderAddress) {
throw new PolynanceApiError("Missing required parameter 'traderAddress'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Trader>(`/v1/trader/${traderAddress}`,{
params: { protocol },
});
return response.data;
} catch (error) {
throw this.handleError(error, methodName, context);
}
}
public async traderPositions(protocol: PredictionProvider,traderAddress: string): Promise<TraderPosition[]> {
const methodName = 'traderPositions';
const context = { protocol, traderAddress: traderAddress ? '***' : traderAddress }; // Mask potentially long address
if (!traderAddress) {
throw new PolynanceApiError("Missing required parameter 'traderAddress'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<TraderPosition[]>(`/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: number = 1, limit: number = 100): Promise<string[]> {
const methodName = 'getSlugs';
const context = { page, limit };
if (page < 1) {
throw new PolynanceApiError("Parameter 'page' must be 1 or greater.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (limit < 1) {
throw new PolynanceApiError("Parameter 'limit' must be 1 or greater.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<string[]>('/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: string): Promise<Market[]> {
const methodName = 'getMarketBySlug';
const context = { slug: slug ? '***' : slug }; // Mask potentially long slug
if (!slug) {
throw new PolynanceApiError("Missing required parameter 'slug'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Market[]>('/v1/agg', {
params: { slug }
});
// Optionally, check for 404 specifically if desired
if (response.status === 404 || response.data.length === 0) {
throw new PolynanceApiError(`Market with slug '${slug}' not found.`, 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: string): Promise<Exchange[]> {
const methodName = 'getExchangeBySlug';
const context = { slug: slug ? '***' : slug }; // Mask potentially long slug
if (!slug) {
throw new PolynanceApiError("Missing required parameter 'slug'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const response = await this.apiClient.get<Exchange[]>('/v1/agg/market', {
params: { slug }
});
// Optionally, check for 404 specifically if desired
if (response.status === 404 || response.data.length === 0) {
throw new PolynanceApiError(`Exchange with slug '${slug}' not found.`, 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: string, filter?: Partial<SearchFilter>): Promise<MarketMatchResult[]> {
const methodName = 'search';
const context = { query: query ? `"${query.substring(0, 50)}${query.length > 50 ? '...' : ''}"` : query, filter }; // Log truncated query
if (!query) {
throw new PolynanceApiError("Missing required parameter 'query'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
try {
const params: any = { 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<MarketMatchResult[]>('/v1/agg/retrieve', { params });
return response.data;
} catch (error) {
throw this.handleError(error, methodName, context);
}
function asContext<T>(
data: T
): string {
const indentSize = 2;
const pad = (lvl: number) => " ".repeat(lvl * indentSize);
const defaultFormatter = (path: string, value: unknown, level: number) =>
`${pad(level)}${path} : ${String(value)}`;
const fmt = defaultFormatter;
const skipUndefined = true
const walk = (value: unknown, path: string[], level: number, out: string[]) => {
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 as Record<string, unknown>);
keys.sort();
for (const k of keys) {
const v = (value as Record<string, unknown>)[k];
if (v === undefined && skipUndefined) continue;
walk(v, [...path, k], level + 1, out);
}
};
const lines: string[] = [];
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: PredictionProvider,
id: string,
handlers?: TradeUpdateHandlers
): TradeSubscription {
const methodName = 'subscribeToTrades';
const context = { protocol, id: id ? '***' : id };
// Check for EventSource availability
if (typeof EventSource === 'undefined') {
throw new PolynanceApiError(
"EventSource is not available in this environment. Ensure you are in a browser or have a suitable polyfill.",
PolynanceErrorCode.ENVIRONMENT_ERROR,
{ methodName, context }
);
}
// Validate parameters
if (!protocol) {
throw new PolynanceApiError("Missing required parameter 'protocol'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
if (!id) {
throw new PolynanceApiError("Missing required parameter 'id'.", PolynanceErrorCode.INVALID_PARAMETER, { methodName, context });
}
const url = `${this.sseBaseUrl}/sse/fillevent?protocol=${protocol}&id=${id}`;
let eventSource: 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 === PolynanceErrorCode.INTERNAL_SDK_ERROR) {
console.error("EventSource failed:", initError);
}
throw initError;
}
let latestData: TradeRecord | null = 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) as TradeRecord;
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 PolynanceApiError(
`Failed to process SSE message: ${error instanceof Error ? error.message : String(error)}`,
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 ? PolynanceErrorCode.SSE_CLOSED : PolynanceErrorCode.SSE_CONNECTION_FAILED;
const errorMessage = isClosed ? `SSE connection closed unexpectedly for ${protocol}/${id}.` : `SSE connection error occurred for ${protocol}/${id}.`;
const sseError = new 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
// --- 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.
*/
export function generatePriceChart(
tradeRecords: TradeRecord[],
intervalMillis: number,
fromTimeMillis: number,
toTimeMillis: number
): Candle[] {
const methodName = 'generatePriceChart';
if (intervalMillis <= 0) {
throw new PolynanceApiError("Candlestick intervalMillis must be positive.", 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<number, Candle>();
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;
}