diff --git a/core/src/exchanges/baozi/errors.ts b/core/src/exchanges/baozi/errors.ts index 8cda8770..15b1bc09 100644 --- a/core/src/exchanges/baozi/errors.ts +++ b/core/src/exchanges/baozi/errors.ts @@ -27,9 +27,9 @@ export class BaoziErrorMapper extends ErrorMapper { super('Baozi'); } - mapError(error: any): BaseError { + mapError(error: unknown): BaseError { // Handle Solana transaction errors - if (error?.message) { + if (error instanceof Error) { const msg = error.message; // Solana insufficient funds diff --git a/core/src/exchanges/gemini-titan/errors.ts b/core/src/exchanges/gemini-titan/errors.ts index 1967e609..7c2708a1 100644 --- a/core/src/exchanges/gemini-titan/errors.ts +++ b/core/src/exchanges/gemini-titan/errors.ts @@ -24,7 +24,7 @@ export class GeminiErrorMapper extends ErrorMapper { super('GeminiTitan'); } - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; if (typeof data === 'string') { @@ -40,9 +40,9 @@ export class GeminiErrorMapper extends ErrorMapper { return super.extractErrorMessage(error); } - protected mapBadRequestError(message: string, data: any): BadRequest { - const reason = typeof data === 'object' && data?.reason - ? String(data.reason) + protected mapBadRequestError(message: string, data: unknown): BadRequest { + const reason = typeof data === 'object' && data !== null && 'reason' in data + ? String((data as Record).reason) : ''; const lowerReason = reason.toLowerCase(); const lowerMessage = message.toLowerCase(); @@ -74,7 +74,7 @@ export class GeminiErrorMapper extends ErrorMapper { return super.mapBadRequestError(message, data); } - mapError(error: any): ReturnType { + mapError(error: unknown): ReturnType { if (axios.isAxiosError(error) && error.response?.status === 429) { const retryAfter = error.response.headers?.['retry-after']; const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined; diff --git a/core/src/exchanges/hyperliquid/errors.ts b/core/src/exchanges/hyperliquid/errors.ts index 1d670792..872e585c 100644 --- a/core/src/exchanges/hyperliquid/errors.ts +++ b/core/src/exchanges/hyperliquid/errors.ts @@ -22,7 +22,7 @@ export class HyperliquidErrorMapper extends ErrorMapper { super('Hyperliquid'); } - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; if (typeof data === 'string') { @@ -35,10 +35,10 @@ export class HyperliquidErrorMapper extends ErrorMapper { return super.extractErrorMessage(error); } - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = message.toLowerCase(); - const responseStr = typeof data === 'object' && data?.response - ? String(data.response).toLowerCase() + const responseStr = typeof data === 'object' && data !== null && 'response' in data + ? String((data as Record).response).toLowerCase() : lowerMessage; if (responseStr.includes('insufficient margin') || responseStr.includes('not enough')) { @@ -56,7 +56,7 @@ export class HyperliquidErrorMapper extends ErrorMapper { return super.mapBadRequestError(message, data); } - mapError(error: any): ReturnType { + mapError(error: unknown): ReturnType { if (axios.isAxiosError(error) && error.response?.status === 429) { const retryAfter = error.response.headers?.['retry-after']; const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined; diff --git a/core/src/exchanges/kalshi/errors.ts b/core/src/exchanges/kalshi/errors.ts index 37b6d311..65f78b78 100644 --- a/core/src/exchanges/kalshi/errors.ts +++ b/core/src/exchanges/kalshi/errors.ts @@ -18,7 +18,7 @@ export class KalshiErrorMapper extends ErrorMapper { /** * Override to handle Kalshi-specific error patterns */ - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { // Handle Kalshi API errors if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; @@ -44,7 +44,7 @@ export class KalshiErrorMapper extends ErrorMapper { /** * Override to detect Kalshi-specific error patterns */ - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = message.toLowerCase(); // Kalshi-specific insufficient funds detection diff --git a/core/src/exchanges/limitless/errors.ts b/core/src/exchanges/limitless/errors.ts index c3289a50..4e0b7a96 100644 --- a/core/src/exchanges/limitless/errors.ts +++ b/core/src/exchanges/limitless/errors.ts @@ -19,7 +19,7 @@ export class LimitlessErrorMapper extends ErrorMapper { /** * Override to handle Limitless-specific error patterns */ - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { // Handle Limitless CLOB errors (similar to Polymarket) if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; @@ -45,7 +45,7 @@ export class LimitlessErrorMapper extends ErrorMapper { /** * Override to detect Limitless-specific error patterns */ - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = (message || '').toString().toLowerCase(); // Limitless-specific authentication errors (400 status) diff --git a/core/src/exchanges/metaculus/errors.ts b/core/src/exchanges/metaculus/errors.ts index 1165beb6..922aefeb 100644 --- a/core/src/exchanges/metaculus/errors.ts +++ b/core/src/exchanges/metaculus/errors.ts @@ -23,7 +23,7 @@ export class MetaculusErrorMapper extends ErrorMapper { super('Metaculus'); } - protected override mapNotFoundError(message: string, _data: any): NotFound { + protected override mapNotFoundError(message: string, _data: unknown): NotFound { const lower = message.toLowerCase(); if (lower.includes('question') || lower.includes('market')) { const match = message.match(/[\d]+/); @@ -33,7 +33,7 @@ export class MetaculusErrorMapper extends ErrorMapper { return new NotFound(message, this.exchangeName); } - protected override mapBadRequestError(message: string, data: any): BadRequest { + protected override mapBadRequestError(message: string, data: unknown): BadRequest { const lower = message.toLowerCase(); // Probability validation errors from the forecast API @@ -57,8 +57,8 @@ export class MetaculusErrorMapper extends ErrorMapper { protected override mapByStatusCode( status: number, message: string, - data: any, - response?: any, + data: unknown, + response?: unknown, ): BaseError { if (status === 401) { return new AuthenticationError( diff --git a/core/src/exchanges/myriad/errors.ts b/core/src/exchanges/myriad/errors.ts index d1ba3158..dc7311bd 100644 --- a/core/src/exchanges/myriad/errors.ts +++ b/core/src/exchanges/myriad/errors.ts @@ -7,7 +7,7 @@ export class MyriadErrorMapper extends ErrorMapper { super('Myriad'); } - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; if (data.message) return data.message; @@ -16,7 +16,7 @@ export class MyriadErrorMapper extends ErrorMapper { return super.extractErrorMessage(error); } - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes('insufficient') || lowerMessage.includes('liquidity')) { return new BadRequest(message, this.exchangeName); diff --git a/core/src/exchanges/opinion/errors.ts b/core/src/exchanges/opinion/errors.ts index 986c9e1b..e97cb9d5 100644 --- a/core/src/exchanges/opinion/errors.ts +++ b/core/src/exchanges/opinion/errors.ts @@ -7,7 +7,7 @@ export class OpinionErrorMapper extends ErrorMapper { super('Opinion'); } - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; // OpenAPI format uses "msg", SDK format uses "errmsg" @@ -19,12 +19,13 @@ export class OpinionErrorMapper extends ErrorMapper { return super.extractErrorMessage(error); } - protected mapBadRequestError(message: string, data: any): BadRequest { - if (data && typeof data === 'object') { + protected mapBadRequestError(message: string, data: unknown): BadRequest { + if (typeof data === 'object' && data !== null) { + const obj = data as Record; // OpenAPI format: { code: number, msg: string } // SDK format: { errno: number, errmsg: string } - const errorCode = data.code ?? data.errno; - const errorMsg = data.msg || data.errmsg || message; + const errorCode = obj.code ?? obj.errno; + const errorMsg = obj.msg || obj.errmsg || message; if (errorCode !== undefined && errorCode !== 0) { return new BadRequest( `Opinion API error (code ${errorCode}): ${errorMsg}`, diff --git a/core/src/exchanges/polymarket/errors.ts b/core/src/exchanges/polymarket/errors.ts index 0433fb83..5fd7e7db 100644 --- a/core/src/exchanges/polymarket/errors.ts +++ b/core/src/exchanges/polymarket/errors.ts @@ -33,7 +33,7 @@ export class PolymarketErrorMapper extends ErrorMapper { * `errorMsg` path for any residual V1 responses (order submission still * returns `errorMsg` in some batch flows). */ - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; @@ -54,7 +54,7 @@ export class PolymarketErrorMapper extends ErrorMapper { /** * Override to handle V2 status code 425 (Too Early -- matching engine restarting) */ - protected mapByStatusCode(status: number, message: string, data: any, response?: any): BaseError { + protected mapByStatusCode(status: number, message: string, data: unknown, response?: unknown): BaseError { if (status === 425) { return new ExchangeNotAvailable( `Matching engine restarting: ${message}`, @@ -68,7 +68,7 @@ export class PolymarketErrorMapper extends ErrorMapper { /** * Override to detect Polymarket-specific error patterns in 400 responses */ - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = message.toLowerCase(); // Signature type / maker address mismatch — the most common auth diff --git a/core/src/exchanges/probable/errors.ts b/core/src/exchanges/probable/errors.ts index 2014e726..0d85e2c4 100644 --- a/core/src/exchanges/probable/errors.ts +++ b/core/src/exchanges/probable/errors.ts @@ -7,7 +7,7 @@ export class ProbableErrorMapper extends ErrorMapper { super('Probable'); } - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const data = error.response.data; @@ -25,14 +25,14 @@ export class ProbableErrorMapper extends ErrorMapper { } // Handle @prob/clob SDK error objects - if (error && typeof error === 'object' && error.msg) { - return String(error.msg); + if (typeof error === 'object' && error !== null && 'msg' in error) { + return String((error as Record).msg); } return super.extractErrorMessage(error); } - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = message.toLowerCase(); // SDK auth failures diff --git a/core/src/exchanges/smarkets/errors.ts b/core/src/exchanges/smarkets/errors.ts index ffaf66aa..c30be056 100644 --- a/core/src/exchanges/smarkets/errors.ts +++ b/core/src/exchanges/smarkets/errors.ts @@ -98,7 +98,7 @@ export class SmarketsErrorMapper extends ErrorMapper { /** * Override to handle the Smarkets { error_type, data } format */ - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { if (axios.isAxiosError(error) && error.response?.data) { const body = error.response.data; const errorType = body.error_type; @@ -124,7 +124,7 @@ export class SmarketsErrorMapper extends ErrorMapper { * Override to map Smarkets error_type values before falling back * to the default status-code-based mapping */ - mapError(error: any): ReturnType { + mapError(error: unknown): ReturnType { if (axios.isAxiosError(error) && error.response?.data) { const errorType: string | undefined = error.response.data.error_type; @@ -143,8 +143,11 @@ export class SmarketsErrorMapper extends ErrorMapper { /** * Override to detect order-specific errors within 400 responses */ - protected mapBadRequestError(message: string, data: any): BadRequest { - const errorType: string | undefined = data?.error_type; + protected mapBadRequestError(message: string, data: unknown): BadRequest { + const errorType: string | undefined = + typeof data === 'object' && data !== null && 'error_type' in data + ? String((data as Record).error_type) + : undefined; if (errorType) { if (INSUFFICIENT_FUNDS_ERRORS.has(errorType)) { @@ -172,14 +175,19 @@ export class SmarketsErrorMapper extends ErrorMapper { private mapByErrorType( errorType: string, message: string, - response: any + response: unknown ): ReturnType | undefined { if (AUTHENTICATION_ERRORS.has(errorType)) { return new AuthenticationError(message, this.exchangeName); } if (RATE_LIMIT_ERRORS.has(errorType)) { - const retryAfter = response?.headers?.['retry-after']; + const headers = ( + typeof response === 'object' && response !== null && 'headers' in response + ? (response as { headers?: Record }).headers + : undefined + ); + const retryAfter = headers?.['retry-after']; const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined; diff --git a/core/src/utils/error-mapper.ts b/core/src/utils/error-mapper.ts index 462fe721..222760d0 100644 --- a/core/src/utils/error-mapper.ts +++ b/core/src/utils/error-mapper.ts @@ -15,6 +15,56 @@ import { ExchangeNotAvailable, } from '../errors'; +/** + * A plain error-like object with an HTTP status and optional data payload. + * Common in third-party SDKs (e.g. Polymarket clob-client) that don't throw + * proper Error instances. + */ +interface PlainErrorObject { + readonly status: number; + readonly data?: unknown; + readonly statusText?: string; + readonly message?: string; +} + +/** Type guard for plain error objects with numeric status codes. */ +function isPlainErrorObject(value: unknown): value is PlainErrorObject { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof Error) && + 'status' in value && + typeof (value as PlainErrorObject).status === 'number' + ); +} + +/** + * An Error subclass with optional HTTP metadata attached by third-party SDKs. + */ +interface ErrorWithHttpMetadata extends Error { + readonly status?: number; + readonly statusCode?: number; + readonly data?: unknown; + readonly response?: { + readonly status?: number; + readonly statusCode?: number; + readonly data?: unknown; + readonly body?: unknown; + readonly headers?: Record; + }; +} + +/** A Node.js-style error with an error code (e.g. ECONNREFUSED). */ +interface NodeError extends Error { + readonly code?: string; +} + +/** Type guard for Node.js-style errors with a `code` property. */ +function isNodeError(value: unknown): value is NodeError { + return value instanceof Error && 'code' in value; +} + /** * Maps raw errors to PMXT unified error classes * @@ -31,11 +81,11 @@ export class ErrorMapper { /** * Main entry point for error mapping */ - mapError(error: any): BaseError { + mapError(error: unknown): BaseError { // Already a BaseError, just add exchange context if missing if (error instanceof BaseError) { if (!error.exchange && this.exchangeName) { - return new (error.constructor as any)( + return new (error.constructor as new (...args: unknown[]) => BaseError)( error.message, this.exchangeName ); @@ -49,24 +99,24 @@ export class ErrorMapper { } // Handle plain objects with status/data (e.g., Polymarket clob-client) - if (error && typeof error === 'object' && !Array.isArray(error) && !(error instanceof Error)) { - if (error.status && typeof error.status === 'number') { - const message = this.extractErrorMessage(error); - return this.mapByStatusCode(error.status, message, error.data, error); - } + if (isPlainErrorObject(error)) { + const message = this.extractErrorMessage(error); + return this.mapByStatusCode(error.status, message, error.data, error); } // Handle network errors - if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') { - return new NetworkError( - `Network error: ${error.message}`, - this.exchangeName - ); + if (isNodeError(error)) { + if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') { + return new NetworkError( + `Network error: ${error.message}`, + this.exchangeName + ); + } } // Handle Error instances with attached HTTP metadata (common in third-party SDKs) if (error instanceof Error) { - const err: any = error; + const err = error as ErrorWithHttpMetadata; const status = err.status ?? err.statusCode ?? err.response?.status ?? err.response?.statusCode; if (typeof status === 'number') { const message = this.extractErrorMessage(error); @@ -108,7 +158,7 @@ export class ErrorMapper { /** * Maps an HTTP status code to the appropriate error class */ - protected mapByStatusCode(status: number, message: string, data: any, response?: any): BaseError { + protected mapByStatusCode(status: number, message: string, data: unknown, response?: unknown): BaseError { switch (status) { case 400: return this.mapBadRequestError(message, data); @@ -139,7 +189,7 @@ export class ErrorMapper { /** * Maps 400 errors to specific bad request subtypes */ - protected mapBadRequestError(message: string, data: any): BadRequest { + protected mapBadRequestError(message: string, data: unknown): BadRequest { const lowerMessage = message.toLowerCase(); // Detect insufficient funds @@ -175,7 +225,7 @@ export class ErrorMapper { /** * Maps 404 errors to specific not found subtypes */ - protected mapNotFoundError(message: string, data: any): NotFound { + protected mapNotFoundError(message: string, data: unknown): NotFound { const lowerMessage = message.toLowerCase(); // Detect order not found (but not "order book" — that's a different resource) @@ -203,9 +253,14 @@ export class ErrorMapper { /** * Maps rate limit errors */ - protected mapRateLimitError(message: string, response: any): RateLimitExceeded { + protected mapRateLimitError(message: string, response: unknown): RateLimitExceeded { // Try to extract retry-after from headers - const retryAfter = response?.headers?.['retry-after']; + const headers = ( + typeof response === 'object' && response !== null && 'headers' in response + ? (response as { headers?: Record }).headers + : undefined + ); + const retryAfter = headers?.['retry-after']; const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined; return new RateLimitExceeded(message, retryAfterSeconds, this.exchangeName); @@ -214,31 +269,35 @@ export class ErrorMapper { /** * Extracts error message from various error formats */ - protected extractErrorMessage(error: any): string { + protected extractErrorMessage(error: unknown): string { // Axios error with response data if (axios.isAxiosError(error) && error.response?.data) { - const data = error.response.data; + const data: unknown = error.response.data; // Try various common error message paths if (typeof data === 'string') { return data; } - if (data.error) { - if (typeof data.error === 'string') { - return data.error; - } - if (data.error.message) { - return data.error.message; + if (typeof data === 'object' && data !== null) { + const obj = data as Record; + + if (obj.error) { + if (typeof obj.error === 'string') { + return obj.error; + } + if (typeof obj.error === 'object' && obj.error !== null && 'message' in obj.error) { + return String((obj.error as Record).message); + } } - } - if (data.message) { - return data.message; - } + if (typeof obj.message === 'string') { + return obj.message; + } - if (data.errorMsg) { - return data.errorMsg; + if (typeof obj.errorMsg === 'string') { + return obj.errorMsg; + } } // Fallback to stringified data @@ -247,29 +306,13 @@ export class ErrorMapper { // Plain object with status and data (e.g., Polymarket clob-client errors) // These aren't AxiosError instances but have similar structure - if (error && typeof error === 'object' && !Array.isArray(error) && !(error instanceof Error)) { - const data = error.data; + if (isPlainErrorObject(error)) { + const data: unknown = error.data; if (data) { - if (typeof data === 'string') { - return data; - } - - if (data.error) { - if (typeof data.error === 'string') { - return data.error; - } - if (data.error.message) { - return data.error.message; - } - } - - if (data.message) { - return data.message; - } - - if (data.errorMsg) { - return data.errorMsg; + const extracted = this.extractFromData(data); + if (extracted) { + return extracted; } } @@ -285,8 +328,8 @@ export class ErrorMapper { // Standard Error object - check for attached response data from third-party SDKs if (error instanceof Error) { - const err: any = error; - const data = err.response?.data ?? err.data ?? err.body; + const err = error as ErrorWithHttpMetadata; + const data: unknown = err.response?.data ?? err.data ?? (err as unknown as Record).body; if (data) { const extracted = this.extractFromData(data); if (extracted) { @@ -305,7 +348,7 @@ export class ErrorMapper { if (typeof error === 'object' && error !== null) { try { return JSON.stringify(error, Object.getOwnPropertyNames(error)); - } catch (e) { + } catch { return String(error); } } @@ -315,27 +358,29 @@ export class ErrorMapper { /** * Extracts a message string from a response data payload */ - protected extractFromData(data: any): string | undefined { + protected extractFromData(data: unknown): string | undefined { if (typeof data === 'string') { return data; } - if (data && typeof data === 'object') { - if (data.error) { - if (typeof data.error === 'string') { - return data.error; + if (typeof data === 'object' && data !== null) { + const obj = data as Record; + + if (obj.error) { + if (typeof obj.error === 'string') { + return obj.error; } - if (data.error.message) { - return data.error.message; + if (typeof obj.error === 'object' && obj.error !== null && 'message' in obj.error) { + return String((obj.error as Record).message); } } - if (data.message) { - return data.message; + if (typeof obj.message === 'string') { + return obj.message; } - if (data.errorMsg) { - return data.errorMsg; + if (typeof obj.errorMsg === 'string') { + return obj.errorMsg; } try {