1use std::time::Duration;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
10pub enum PolymarketError {
11 #[error("Network error: {message}")]
13 Network {
14 message: String,
15 #[source]
16 source: Option<Box<dyn std::error::Error + Send + Sync>>,
17 },
18
19 #[error("API error ({status}): {message}")]
21 Api {
22 status: u16,
23 message: String,
24 error_code: Option<String>,
25 },
26
27 #[error("Auth error: {message}")]
29 Auth {
30 message: String,
31 kind: AuthErrorKind,
32 },
33
34 #[error("Order error: {message}")]
36 Order {
37 message: String,
38 kind: OrderErrorKind,
39 },
40
41 #[error("Market data error: {message}")]
43 MarketData {
44 message: String,
45 kind: MarketDataErrorKind,
46 },
47
48 #[error("Stream error: {message}")]
50 Stream {
51 message: String,
52 kind: StreamErrorKind,
53 },
54
55 #[error("Config error: {message}")]
57 Config { message: String },
58
59 #[error("Parse error: {message}")]
61 Parse {
62 message: String,
63 #[source]
64 source: Option<Box<dyn std::error::Error + Send + Sync>>,
65 },
66
67 #[error("Timeout: operation timed out after {duration:?}")]
69 Timeout {
70 duration: Duration,
71 operation: String,
72 },
73
74 #[error("Rate limit exceeded: {message}")]
76 RateLimit {
77 message: String,
78 retry_after: Option<Duration>,
79 },
80
81 #[error("Validation error: {message}")]
83 Validation {
84 message: String,
85 field: Option<String>,
86 },
87
88 #[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#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum AuthErrorKind {
100 InvalidCredentials,
102 ExpiredCredentials,
104 InsufficientPermissions,
106 SignatureError,
108 NonceError,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum OrderErrorKind {
115 InvalidPrice,
117 InvalidSize,
119 InsufficientBalance,
121 MarketClosed,
123 DuplicateOrder,
125 OrderNotFound,
127 CancellationFailed,
129 ExecutionFailed,
131 SizeConstraint,
133 PriceConstraint,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum MarketDataErrorKind {
140 TokenNotFound,
142 MarketNotFound,
144 StaleData,
146 IncompleteData,
148 BookUnavailable,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum StreamErrorKind {
155 ConnectionFailed,
157 ConnectionLost,
159 SubscriptionFailed,
161 MessageCorrupted,
163 Reconnecting,
165 Unknown,
167}
168
169impl PolymarketError {
170 #[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 #[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 #[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 #[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 #[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
250impl PolymarketError {
252 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 pub fn network_simple(message: impl Into<String>) -> Self {
265 Self::Network {
266 message: message.into(),
267 source: None,
268 }
269 }
270
271 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 pub fn auth(message: impl Into<String>) -> Self {
282 Self::Auth {
283 message: message.into(),
284 kind: AuthErrorKind::SignatureError,
285 }
286 }
287
288 pub fn crypto(message: impl Into<String>) -> Self {
290 Self::Auth {
291 message: message.into(),
292 kind: AuthErrorKind::SignatureError,
293 }
294 }
295
296 pub fn order(message: impl Into<String>, kind: OrderErrorKind) -> Self {
298 Self::Order {
299 message: message.into(),
300 kind,
301 }
302 }
303
304 pub fn market_data(message: impl Into<String>, kind: MarketDataErrorKind) -> Self {
306 Self::MarketData {
307 message: message.into(),
308 kind,
309 }
310 }
311
312 pub fn stream(message: impl Into<String>, kind: StreamErrorKind) -> Self {
314 Self::Stream {
315 message: message.into(),
316 kind,
317 }
318 }
319
320 pub fn config(message: impl Into<String>) -> Self {
322 Self::Config {
323 message: message.into(),
324 }
325 }
326
327 pub fn parse(message: impl Into<String>) -> Self {
329 Self::Parse {
330 message: message.into(),
331 source: None,
332 }
333 }
334
335 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 pub fn timeout(duration: Duration, operation: impl Into<String>) -> Self {
348 Self::Timeout {
349 duration,
350 operation: operation.into(),
351 }
352 }
353
354 pub fn rate_limit(message: impl Into<String>) -> Self {
356 Self::RateLimit {
357 message: message.into(),
358 retry_after: None,
359 }
360 }
361
362 pub fn validation(message: impl Into<String>) -> Self {
364 Self::Validation {
365 message: message.into(),
366 field: None,
367 }
368 }
369
370 pub fn internal(message: impl Into<String>) -> Self {
372 Self::Internal {
373 message: message.into(),
374 source: None,
375 }
376 }
377
378 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
390impl 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
433impl 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
499pub type Result<T> = std::result::Result<T, PolymarketError>;
501
502pub type Error = PolymarketError;