o2_sdk/
errors.rs

1/// Error types for O2 Exchange SDK.
2///
3/// Maps all error codes from the O2 API (Section 8) to typed Rust errors.
4/// Also handles the two distinct error formats for POST /v1/session/actions.
5use thiserror::Error;
6
7/// The primary error type for the O2 SDK.
8#[derive(Error, Debug)]
9#[non_exhaustive]
10pub enum O2Error {
11    // General (1xxx)
12    #[error("Internal server error (1000): {0}")]
13    InternalError(String),
14
15    #[error("Invalid request (1001): {0}")]
16    InvalidRequest(String),
17
18    #[error("Parse error (1002): {0}")]
19    ParseError(String),
20
21    #[error("Rate limit exceeded (1003): {0}")]
22    RateLimitExceeded(String),
23
24    #[error("Geo restricted (1004): {0}")]
25    GeoRestricted(String),
26
27    // Market (2xxx)
28    #[error("Market not found (2000): {0}")]
29    MarketNotFound(String),
30
31    #[error("Market paused (2001): {0}")]
32    MarketPaused(String),
33
34    #[error("Market already exists (2002): {0}")]
35    MarketAlreadyExists(String),
36
37    // Order (3xxx)
38    #[error("Order not found (3000): {0}")]
39    OrderNotFound(String),
40
41    #[error("Order not active (3001): {0}")]
42    OrderNotActive(String),
43
44    #[error("Invalid order params (3002): {0}")]
45    InvalidOrderParams(String),
46
47    // Account/Session (4xxx)
48    #[error("Invalid signature (4000): {0}")]
49    InvalidSignature(String),
50
51    #[error("Invalid session (4001): {0}")]
52    InvalidSession(String),
53
54    #[error("Account not found (4002): {0}")]
55    AccountNotFound(String),
56
57    #[error("Whitelist not configured (4003): {0}")]
58    WhitelistNotConfigured(String),
59
60    // Trade (5xxx)
61    #[error("Trade not found (5000): {0}")]
62    TradeNotFound(String),
63
64    #[error("Invalid trade count (5001): {0}")]
65    InvalidTradeCount(String),
66
67    // WebSocket (6xxx)
68    #[error("Already subscribed (6000): {0}")]
69    AlreadySubscribed(String),
70
71    #[error("Too many subscriptions (6001): {0}")]
72    TooManySubscriptions(String),
73
74    #[error("Subscription error (6002): {0}")]
75    SubscriptionError(String),
76
77    // Validation (7xxx)
78    #[error("Invalid amount (7000): {0}")]
79    InvalidAmount(String),
80
81    #[error("Invalid time range (7001): {0}")]
82    InvalidTimeRange(String),
83
84    #[error("Invalid pagination (7002): {0}")]
85    InvalidPagination(String),
86
87    #[error("No actions provided (7003): {0}")]
88    NoActionsProvided(String),
89
90    #[error("Too many actions (7004): {0}")]
91    TooManyActions(String),
92
93    // Block/Events (8xxx)
94    #[error("Block not found (8000): {0}")]
95    BlockNotFound(String),
96
97    #[error("Events not found (8001): {0}")]
98    EventsNotFound(String),
99
100    // On-chain revert (no code)
101    #[error("On-chain revert: {message}, reason: {reason}")]
102    OnChainRevert {
103        message: String,
104        reason: String,
105        receipts: Option<serde_json::Value>,
106    },
107
108    // Client-side errors
109    #[error("Session expired: {0}")]
110    SessionExpired(String),
111
112    // Transport errors
113    #[error("HTTP error: {0}")]
114    HttpError(String),
115
116    #[error("WebSocket error: {0}")]
117    WebSocketError(String),
118
119    #[error("WebSocket reconnected")]
120    WebSocketReconnected,
121
122    #[error("WebSocket disconnected: {0}")]
123    WebSocketDisconnected(String),
124
125    #[error("JSON error: {0}")]
126    JsonError(String),
127
128    // Crypto errors
129    #[error("Crypto error: {0}")]
130    CryptoError(String),
131
132    // Generic
133    #[error("{0}")]
134    Other(String),
135}
136
137impl O2Error {
138    /// Create an O2Error from an API error code and message.
139    pub fn from_code(code: u32, message: String) -> Self {
140        match code {
141            1000 => O2Error::InternalError(message),
142            1001 => O2Error::InvalidRequest(message),
143            1002 => O2Error::ParseError(message),
144            1003 => O2Error::RateLimitExceeded(message),
145            1004 => O2Error::GeoRestricted(message),
146            2000 => O2Error::MarketNotFound(message),
147            2001 => O2Error::MarketPaused(message),
148            2002 => O2Error::MarketAlreadyExists(message),
149            3000 => O2Error::OrderNotFound(message),
150            3001 => O2Error::OrderNotActive(message),
151            3002 => O2Error::InvalidOrderParams(message),
152            4000 => O2Error::InvalidSignature(message),
153            4001 => O2Error::InvalidSession(message),
154            4002 => O2Error::AccountNotFound(message),
155            4003 => O2Error::WhitelistNotConfigured(message),
156            5000 => O2Error::TradeNotFound(message),
157            5001 => O2Error::InvalidTradeCount(message),
158            6000 => O2Error::AlreadySubscribed(message),
159            6001 => O2Error::TooManySubscriptions(message),
160            6002 => O2Error::SubscriptionError(message),
161            7000 => O2Error::InvalidAmount(message),
162            7001 => O2Error::InvalidTimeRange(message),
163            7002 => O2Error::InvalidPagination(message),
164            7003 => O2Error::NoActionsProvided(message),
165            7004 => O2Error::TooManyActions(message),
166            8000 => O2Error::BlockNotFound(message),
167            8001 => O2Error::EventsNotFound(message),
168            _ => O2Error::Other(format!("Unknown error code {code}: {message}")),
169        }
170    }
171
172    /// Returns the error code if this is a coded API error.
173    pub fn error_code(&self) -> Option<u32> {
174        match self {
175            O2Error::InternalError(_) => Some(1000),
176            O2Error::InvalidRequest(_) => Some(1001),
177            O2Error::ParseError(_) => Some(1002),
178            O2Error::RateLimitExceeded(_) => Some(1003),
179            O2Error::GeoRestricted(_) => Some(1004),
180            O2Error::MarketNotFound(_) => Some(2000),
181            O2Error::MarketPaused(_) => Some(2001),
182            O2Error::MarketAlreadyExists(_) => Some(2002),
183            O2Error::OrderNotFound(_) => Some(3000),
184            O2Error::OrderNotActive(_) => Some(3001),
185            O2Error::InvalidOrderParams(_) => Some(3002),
186            O2Error::InvalidSignature(_) => Some(4000),
187            O2Error::InvalidSession(_) => Some(4001),
188            O2Error::AccountNotFound(_) => Some(4002),
189            O2Error::WhitelistNotConfigured(_) => Some(4003),
190            O2Error::TradeNotFound(_) => Some(5000),
191            O2Error::InvalidTradeCount(_) => Some(5001),
192            O2Error::AlreadySubscribed(_) => Some(6000),
193            O2Error::TooManySubscriptions(_) => Some(6001),
194            O2Error::SubscriptionError(_) => Some(6002),
195            O2Error::InvalidAmount(_) => Some(7000),
196            O2Error::InvalidTimeRange(_) => Some(7001),
197            O2Error::InvalidPagination(_) => Some(7002),
198            O2Error::NoActionsProvided(_) => Some(7003),
199            O2Error::TooManyActions(_) => Some(7004),
200            O2Error::BlockNotFound(_) => Some(8000),
201            O2Error::EventsNotFound(_) => Some(8001),
202            _ => None,
203        }
204    }
205
206    /// Returns true if this error suggests retrying with backoff.
207    pub fn is_retryable(&self) -> bool {
208        matches!(
209            self,
210            O2Error::InternalError(_) | O2Error::RateLimitExceeded(_)
211        )
212    }
213}
214
215impl From<reqwest::Error> for O2Error {
216    fn from(err: reqwest::Error) -> Self {
217        O2Error::HttpError(err.to_string())
218    }
219}
220
221impl From<serde_json::Error> for O2Error {
222    fn from(err: serde_json::Error) -> Self {
223        O2Error::JsonError(err.to_string())
224    }
225}
226
227impl From<url::ParseError> for O2Error {
228    fn from(err: url::ParseError) -> Self {
229        O2Error::Other(format!("URL parse error: {err}"))
230    }
231}
232
233impl From<tokio_tungstenite::tungstenite::Error> for O2Error {
234    fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
235        O2Error::WebSocketError(err.to_string())
236    }
237}
238
239impl From<std::convert::Infallible> for O2Error {
240    fn from(_: std::convert::Infallible) -> Self {
241        unreachable!()
242    }
243}