1use std::time::Duration;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum PolymarketError {
9 #[error("HTTP error: {0}")]
11 Http(#[from] reqwest::Error),
12
13 #[error("JSON error: {0}")]
15 Json(#[from] serde_json::Error),
16
17 #[error("API error (HTTP {status}): {message}")]
19 Api {
20 status: u16,
22 message: String,
24 },
25
26 #[error("Rate limited: retry after {retry_after:?}")]
28 RateLimit {
29 retry_after: Duration,
31 },
32
33 #[error("Order rejected: {reason}")]
35 OrderRejected {
36 reason: String,
38 },
39
40 #[error("Signing error: {0}")]
42 Signing(String),
43
44 #[error("Auth error: {0}")]
46 Auth(String),
47
48 #[error("URL error: {0}")]
50 Url(#[from] url::ParseError),
51
52 #[error("Invalid parameter: {0}")]
54 InvalidParam(String),
55
56 #[error("Signer error: {0}")]
58 Signer(#[from] alloy::signers::Error),
59
60 #[cfg(feature = "ws")]
62 #[error("WebSocket error: {0}")]
63 WebSocket(String),
64}
65
66impl PolymarketError {
67 pub fn is_retryable(&self) -> bool {
72 match self {
73 Self::RateLimit { .. } => true,
74 Self::Http(e) => e.is_timeout() || e.is_connect(),
75 Self::Api { status, .. } => *status == 429 || *status >= 500,
76 _ => false,
77 }
78 }
79
80 pub fn retry_after(&self) -> Option<Duration> {
82 match self {
83 Self::RateLimit { retry_after } => Some(*retry_after),
84 _ => None,
85 }
86 }
87}
88
89pub type Result<T> = std::result::Result<T, PolymarketError>;
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_rate_limit_is_retryable() {
98 let err = PolymarketError::RateLimit {
99 retry_after: Duration::from_secs(5),
100 };
101 assert!(err.is_retryable());
102 assert_eq!(err.retry_after(), Some(Duration::from_secs(5)));
103 }
104
105 #[test]
106 fn test_api_5xx_is_retryable() {
107 let err = PolymarketError::Api {
108 status: 503,
109 message: "Service Unavailable".into(),
110 };
111 assert!(err.is_retryable());
112 }
113
114 #[test]
115 fn test_auth_error_not_retryable() {
116 let err = PolymarketError::Auth("bad key".into());
117 assert!(!err.is_retryable());
118 }
119
120 #[test]
121 fn test_order_rejected_not_retryable() {
122 let err = PolymarketError::OrderRejected {
123 reason: "insufficient balance".into(),
124 };
125 assert!(!err.is_retryable());
126 assert!(err.retry_after().is_none());
127 }
128}