polyfill_rs/
errors.rs

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