polymarket_sdk/core/
error.rs

1//! Error types for the Polymarket SDK.
2//!
3//! Provides structured error handling with retry and recovery support.
4
5use std::time::Duration;
6use thiserror::Error;
7
8/// Main error type for the Polymarket SDK.
9#[derive(Error, Debug)]
10pub enum PolymarketError {
11    /// Network-related errors (typically retryable)
12    #[error("Network error: {message}")]
13    Network {
14        message: String,
15        #[source]
16        source: Option<Box<dyn std::error::Error + Send + Sync>>,
17    },
18
19    /// API errors from Polymarket
20    #[error("API error ({status}): {message}")]
21    Api {
22        status: u16,
23        message: String,
24        error_code: Option<String>,
25    },
26
27    /// Authentication/authorization errors
28    #[error("Auth error: {message}")]
29    Auth {
30        message: String,
31        kind: AuthErrorKind,
32    },
33
34    /// Order-related errors
35    #[error("Order error: {message}")]
36    Order {
37        message: String,
38        kind: OrderErrorKind,
39    },
40
41    /// Market data errors
42    #[error("Market data error: {message}")]
43    MarketData {
44        message: String,
45        kind: MarketDataErrorKind,
46    },
47
48    /// WebSocket/streaming errors
49    #[error("Stream error: {message}")]
50    Stream {
51        message: String,
52        kind: StreamErrorKind,
53    },
54
55    /// Configuration errors
56    #[error("Config error: {message}")]
57    Config { message: String },
58
59    /// Parsing/serialization errors
60    #[error("Parse error: {message}")]
61    Parse {
62        message: String,
63        #[source]
64        source: Option<Box<dyn std::error::Error + Send + Sync>>,
65    },
66
67    /// Timeout errors
68    #[error("Timeout: operation timed out after {duration:?}")]
69    Timeout {
70        duration: Duration,
71        operation: String,
72    },
73
74    /// Rate limiting errors
75    #[error("Rate limit exceeded: {message}")]
76    RateLimit {
77        message: String,
78        retry_after: Option<Duration>,
79    },
80
81    /// Validation errors
82    #[error("Validation error: {message}")]
83    Validation {
84        message: String,
85        field: Option<String>,
86    },
87
88    /// Internal errors (bugs)
89    #[error("Internal error: {message}")]
90    Internal {
91        message: String,
92        #[source]
93        source: Option<Box<dyn std::error::Error + Send + Sync>>,
94    },
95}
96
97/// Authentication error subcategories.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum AuthErrorKind {
100    /// Invalid API credentials
101    InvalidCredentials,
102    /// Expired credentials
103    ExpiredCredentials,
104    /// Insufficient permissions
105    InsufficientPermissions,
106    /// Signature error
107    SignatureError,
108    /// Nonce error
109    NonceError,
110}
111
112/// Order error subcategories.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum OrderErrorKind {
115    /// Invalid price
116    InvalidPrice,
117    /// Invalid size
118    InvalidSize,
119    /// Insufficient balance
120    InsufficientBalance,
121    /// Market closed
122    MarketClosed,
123    /// Duplicate order
124    DuplicateOrder,
125    /// Order not found
126    OrderNotFound,
127    /// Cancellation failed
128    CancellationFailed,
129    /// Execution failed
130    ExecutionFailed,
131    /// Size constraint violation
132    SizeConstraint,
133    /// Price constraint violation
134    PriceConstraint,
135}
136
137/// Market data error subcategories.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum MarketDataErrorKind {
140    /// Token not found
141    TokenNotFound,
142    /// Market not found
143    MarketNotFound,
144    /// Stale data
145    StaleData,
146    /// Incomplete data
147    IncompleteData,
148    /// Book unavailable
149    BookUnavailable,
150}
151
152/// Streaming error subcategories.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum StreamErrorKind {
155    /// Failed to establish connection
156    ConnectionFailed,
157    /// Connection was lost
158    ConnectionLost,
159    /// Subscription request failed
160    SubscriptionFailed,
161    /// Received corrupted message
162    MessageCorrupted,
163    /// Currently reconnecting
164    Reconnecting,
165    /// Unknown error
166    Unknown,
167}
168
169impl PolymarketError {
170    /// Check if this error is retryable.
171    #[must_use]
172    pub fn is_retryable(&self) -> bool {
173        match self {
174            Self::Network { .. } => true,
175            Self::Api { status, .. } => *status >= 500 && *status < 600,
176            Self::Timeout { .. } => true,
177            Self::RateLimit { .. } => true,
178            Self::Stream { kind, .. } => {
179                matches!(
180                    kind,
181                    StreamErrorKind::ConnectionLost | StreamErrorKind::Reconnecting
182                )
183            }
184            _ => false,
185        }
186    }
187
188    /// Get suggested retry delay.
189    #[must_use]
190    pub fn retry_delay(&self) -> Option<Duration> {
191        match self {
192            Self::Network { .. } => Some(Duration::from_millis(100)),
193            Self::Api { status, .. } if *status >= 500 => Some(Duration::from_millis(500)),
194            Self::Timeout { .. } => Some(Duration::from_millis(50)),
195            Self::RateLimit { retry_after, .. } => retry_after.or(Some(Duration::from_secs(1))),
196            Self::Stream { .. } => Some(Duration::from_millis(250)),
197            _ => None,
198        }
199    }
200
201    /// Check if this error indicates the wallet is not registered with Polymarket CLOB.
202    ///
203    /// This happens when calling `/auth/derive-api-key` for a wallet that has never
204    /// been registered via `/auth/api-key` (create API key).
205    ///
206    /// # Returns
207    /// `true` if the error message indicates wallet not registered
208    #[must_use]
209    pub fn is_wallet_not_registered(&self) -> bool {
210        match self {
211            Self::Api { status, message, .. } => {
212                *status == 400 && message.contains("Could not derive api key")
213            }
214            _ => false,
215        }
216    }
217
218    /// Check if this is a critical error that should stop trading.
219    #[must_use]
220    pub fn is_critical(&self) -> bool {
221        match self {
222            Self::Auth { .. } => true,
223            Self::Config { .. } => true,
224            Self::Internal { .. } => true,
225            Self::Order { kind, .. } => matches!(kind, OrderErrorKind::InsufficientBalance),
226            _ => false,
227        }
228    }
229
230    /// Get error category for metrics.
231    #[must_use]
232    pub fn category(&self) -> &'static str {
233        match self {
234            Self::Network { .. } => "network",
235            Self::Api { .. } => "api",
236            Self::Auth { .. } => "auth",
237            Self::Order { .. } => "order",
238            Self::MarketData { .. } => "market_data",
239            Self::Stream { .. } => "stream",
240            Self::Config { .. } => "config",
241            Self::Parse { .. } => "parse",
242            Self::Timeout { .. } => "timeout",
243            Self::RateLimit { .. } => "rate_limit",
244            Self::Validation { .. } => "validation",
245            Self::Internal { .. } => "internal",
246        }
247    }
248}
249
250// Convenience constructors
251impl PolymarketError {
252    /// Create a network error with source.
253    pub fn network<E: std::error::Error + Send + Sync + 'static>(
254        message: impl Into<String>,
255        source: E,
256    ) -> Self {
257        Self::Network {
258            message: message.into(),
259            source: Some(Box::new(source)),
260        }
261    }
262
263    /// Create a network error without source.
264    pub fn network_simple(message: impl Into<String>) -> Self {
265        Self::Network {
266            message: message.into(),
267            source: None,
268        }
269    }
270
271    /// Create an API error.
272    pub fn api(status: u16, message: impl Into<String>) -> Self {
273        Self::Api {
274            status,
275            message: message.into(),
276            error_code: None,
277        }
278    }
279
280    /// Create an auth error.
281    pub fn auth(message: impl Into<String>) -> Self {
282        Self::Auth {
283            message: message.into(),
284            kind: AuthErrorKind::SignatureError,
285        }
286    }
287
288    /// Create a crypto/signature error (alias for auth).
289    pub fn crypto(message: impl Into<String>) -> Self {
290        Self::Auth {
291            message: message.into(),
292            kind: AuthErrorKind::SignatureError,
293        }
294    }
295
296    /// Create an order error.
297    pub fn order(message: impl Into<String>, kind: OrderErrorKind) -> Self {
298        Self::Order {
299            message: message.into(),
300            kind,
301        }
302    }
303
304    /// Create a market data error.
305    pub fn market_data(message: impl Into<String>, kind: MarketDataErrorKind) -> Self {
306        Self::MarketData {
307            message: message.into(),
308            kind,
309        }
310    }
311
312    /// Create a stream error.
313    pub fn stream(message: impl Into<String>, kind: StreamErrorKind) -> Self {
314        Self::Stream {
315            message: message.into(),
316            kind,
317        }
318    }
319
320    /// Create a config error.
321    pub fn config(message: impl Into<String>) -> Self {
322        Self::Config {
323            message: message.into(),
324        }
325    }
326
327    /// Create a parse error.
328    pub fn parse(message: impl Into<String>) -> Self {
329        Self::Parse {
330            message: message.into(),
331            source: None,
332        }
333    }
334
335    /// Create a parse error with source.
336    pub fn parse_with_source<E: std::error::Error + Send + Sync + 'static>(
337        message: impl Into<String>,
338        source: E,
339    ) -> Self {
340        Self::Parse {
341            message: message.into(),
342            source: Some(Box::new(source)),
343        }
344    }
345
346    /// Create a timeout error.
347    pub fn timeout(duration: Duration, operation: impl Into<String>) -> Self {
348        Self::Timeout {
349            duration,
350            operation: operation.into(),
351        }
352    }
353
354    /// Create a rate limit error.
355    pub fn rate_limit(message: impl Into<String>) -> Self {
356        Self::RateLimit {
357            message: message.into(),
358            retry_after: None,
359        }
360    }
361
362    /// Create a validation error.
363    pub fn validation(message: impl Into<String>) -> Self {
364        Self::Validation {
365            message: message.into(),
366            field: None,
367        }
368    }
369
370    /// Create an internal error.
371    pub fn internal(message: impl Into<String>) -> Self {
372        Self::Internal {
373            message: message.into(),
374            source: None,
375        }
376    }
377
378    /// Create an internal error with source.
379    pub fn internal_with_source<E: std::error::Error + Send + Sync + 'static>(
380        message: impl Into<String>,
381        source: E,
382    ) -> Self {
383        Self::Internal {
384            message: message.into(),
385            source: Some(Box::new(source)),
386        }
387    }
388}
389
390// Implement From for common external error types
391impl From<reqwest::Error> for PolymarketError {
392    fn from(err: reqwest::Error) -> Self {
393        if err.is_timeout() {
394            Self::Timeout {
395                duration: Duration::from_secs(30),
396                operation: "HTTP request".to_string(),
397            }
398        } else if err.is_connect() || err.is_request() {
399            Self::network("HTTP request failed", err)
400        } else {
401            Self::internal_with_source("Unexpected reqwest error", err)
402        }
403    }
404}
405
406impl From<serde_json::Error> for PolymarketError {
407    fn from(err: serde_json::Error) -> Self {
408        Self::parse_with_source(format!("JSON parsing failed: {err}"), err)
409    }
410}
411
412impl From<url::ParseError> for PolymarketError {
413    fn from(err: url::ParseError) -> Self {
414        Self::config(format!("Invalid URL: {err}"))
415    }
416}
417
418impl From<tokio_tungstenite::tungstenite::Error> for PolymarketError {
419    fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
420        use tokio_tungstenite::tungstenite::Error as WsError;
421
422        let kind = match &err {
423            WsError::ConnectionClosed | WsError::AlreadyClosed => StreamErrorKind::ConnectionLost,
424            WsError::Io(_) => StreamErrorKind::ConnectionFailed,
425            WsError::Protocol(_) => StreamErrorKind::MessageCorrupted,
426            _ => StreamErrorKind::ConnectionFailed,
427        };
428
429        Self::stream(format!("WebSocket error: {err}"), kind)
430    }
431}
432
433// Manual Clone implementation since Box<dyn Error> doesn't implement Clone
434impl Clone for PolymarketError {
435    fn clone(&self) -> Self {
436        match self {
437            Self::Network { message, .. } => Self::Network {
438                message: message.clone(),
439                source: None,
440            },
441            Self::Api {
442                status,
443                message,
444                error_code,
445            } => Self::Api {
446                status: *status,
447                message: message.clone(),
448                error_code: error_code.clone(),
449            },
450            Self::Auth { message, kind } => Self::Auth {
451                message: message.clone(),
452                kind: kind.clone(),
453            },
454            Self::Order { message, kind } => Self::Order {
455                message: message.clone(),
456                kind: kind.clone(),
457            },
458            Self::MarketData { message, kind } => Self::MarketData {
459                message: message.clone(),
460                kind: kind.clone(),
461            },
462            Self::Stream { message, kind } => Self::Stream {
463                message: message.clone(),
464                kind: kind.clone(),
465            },
466            Self::Config { message } => Self::Config {
467                message: message.clone(),
468            },
469            Self::Parse { message, .. } => Self::Parse {
470                message: message.clone(),
471                source: None,
472            },
473            Self::Timeout {
474                duration,
475                operation,
476            } => Self::Timeout {
477                duration: *duration,
478                operation: operation.clone(),
479            },
480            Self::RateLimit {
481                message,
482                retry_after,
483            } => Self::RateLimit {
484                message: message.clone(),
485                retry_after: *retry_after,
486            },
487            Self::Validation { message, field } => Self::Validation {
488                message: message.clone(),
489                field: field.clone(),
490            },
491            Self::Internal { message, .. } => Self::Internal {
492                message: message.clone(),
493                source: None,
494            },
495        }
496    }
497}
498
499/// Result type alias for convenience.
500pub type Result<T> = std::result::Result<T, PolymarketError>;
501
502/// Alias for backward compatibility.
503pub type Error = PolymarketError;