odos_sdk/
error.rs

1use alloy_primitives::hex;
2use reqwest::StatusCode;
3use thiserror::Error;
4
5use crate::OdosChainError;
6
7/// Result type alias for Odos SDK operations
8pub type Result<T> = std::result::Result<T, OdosError>;
9
10/// Comprehensive error types for the Odos SDK
11///
12/// This enum provides detailed error types for different failure scenarios,
13/// allowing users to handle specific error conditions appropriately.
14///
15/// ## Error Categories
16///
17/// - **Network Errors**: HTTP, timeout, and connectivity issues
18/// - **API Errors**: Responses from the Odos service indicating various failures
19/// - **Input Errors**: Invalid parameters or missing required data
20/// - **System Errors**: Rate limiting and internal failures
21///
22/// ## Retryable Errors
23///
24/// Some error types are marked as retryable (see [`OdosError::is_retryable`]):
25/// - Timeout errors
26/// - Certain HTTP errors (5xx status codes, connection issues)
27/// - Rate limiting errors
28/// - Some API errors (server errors, rate limits)
29///
30/// ## Examples
31///
32/// ```rust
33/// use odos_sdk::{OdosError, Result};
34/// use reqwest::StatusCode;
35///
36/// // Create different error types
37/// let api_error = OdosError::api_error(StatusCode::BAD_REQUEST, "Invalid input".to_string());
38/// let timeout_error = OdosError::timeout_error("Request timed out");
39/// let rate_limit_error = OdosError::rate_limit_error("Too many requests");
40///
41/// // Check if errors are retryable
42/// assert!(!api_error.is_retryable());  // 4xx errors are not retryable
43/// assert!(timeout_error.is_retryable()); // Timeouts are retryable
44/// assert!(rate_limit_error.is_retryable()); // Rate limits are retryable
45///
46/// // Get error categories for metrics
47/// assert_eq!(api_error.category(), "api");
48/// assert_eq!(timeout_error.category(), "timeout");
49/// assert_eq!(rate_limit_error.category(), "rate_limit");
50/// ```
51#[derive(Error, Debug)]
52pub enum OdosError {
53    /// HTTP request errors
54    #[error("HTTP request failed: {0}")]
55    Http(#[from] reqwest::Error),
56
57    /// API errors returned by the Odos service
58    #[error("Odos API error (status: {status}): {message}")]
59    Api { status: StatusCode, message: String },
60
61    /// JSON serialization/deserialization errors
62    #[error("JSON processing error: {0}")]
63    Json(#[from] serde_json::Error),
64
65    /// Hex decoding errors
66    #[error("Hex decoding error: {0}")]
67    Hex(#[from] hex::FromHexError),
68
69    /// Invalid input parameters
70    #[error("Invalid input: {0}")]
71    InvalidInput(String),
72
73    /// Missing required data
74    #[error("Missing required data: {0}")]
75    MissingData(String),
76
77    /// Chain not supported
78    #[error("Chain not supported: {chain_id}")]
79    UnsupportedChain { chain_id: u64 },
80
81    /// Contract interaction errors
82    #[error("Contract error: {0}")]
83    Contract(String),
84
85    /// Transaction assembly errors
86    #[error("Transaction assembly failed: {0}")]
87    TransactionAssembly(String),
88
89    /// Quote request errors
90    #[error("Quote request failed: {0}")]
91    QuoteRequest(String),
92
93    /// Configuration errors
94    #[error("Configuration error: {0}")]
95    Configuration(String),
96
97    /// Timeout errors
98    #[error("Operation timed out: {0}")]
99    Timeout(String),
100
101    /// Rate limit exceeded
102    #[error("Rate limit exceeded: {0}")]
103    RateLimit(String),
104
105    /// Generic internal error
106    #[error("Internal error: {0}")]
107    Internal(String),
108}
109
110impl OdosError {
111    /// Create an API error from response
112    pub fn api_error(status: StatusCode, message: String) -> Self {
113        Self::Api { status, message }
114    }
115
116    /// Create an invalid input error
117    pub fn invalid_input(message: impl Into<String>) -> Self {
118        Self::InvalidInput(message.into())
119    }
120
121    /// Create a missing data error
122    pub fn missing_data(message: impl Into<String>) -> Self {
123        Self::MissingData(message.into())
124    }
125
126    /// Create an unsupported chain error
127    pub fn unsupported_chain(chain_id: u64) -> Self {
128        Self::UnsupportedChain { chain_id }
129    }
130
131    /// Create a contract error
132    pub fn contract_error(message: impl Into<String>) -> Self {
133        Self::Contract(message.into())
134    }
135
136    /// Create a transaction assembly error
137    pub fn transaction_assembly_error(message: impl Into<String>) -> Self {
138        Self::TransactionAssembly(message.into())
139    }
140
141    /// Create a quote request error
142    pub fn quote_request_error(message: impl Into<String>) -> Self {
143        Self::QuoteRequest(message.into())
144    }
145
146    /// Create a configuration error
147    pub fn configuration_error(message: impl Into<String>) -> Self {
148        Self::Configuration(message.into())
149    }
150
151    /// Create a timeout error
152    pub fn timeout_error(message: impl Into<String>) -> Self {
153        Self::Timeout(message.into())
154    }
155
156    /// Create a rate limit error
157    pub fn rate_limit_error(message: impl Into<String>) -> Self {
158        Self::RateLimit(message.into())
159    }
160
161    /// Create an internal error
162    pub fn internal_error(message: impl Into<String>) -> Self {
163        Self::Internal(message.into())
164    }
165
166    /// Check if the error is retryable
167    pub fn is_retryable(&self) -> bool {
168        match self {
169            // HTTP errors that are typically retryable
170            OdosError::Http(err) => {
171                // Timeout, connection errors, etc.
172                err.is_timeout() || err.is_connect() || err.is_request()
173            }
174            // API errors that might be retryable
175            OdosError::Api { status, .. } => {
176                matches!(
177                    *status,
178                    StatusCode::TOO_MANY_REQUESTS
179                        | StatusCode::INTERNAL_SERVER_ERROR
180                        | StatusCode::BAD_GATEWAY
181                        | StatusCode::SERVICE_UNAVAILABLE
182                        | StatusCode::GATEWAY_TIMEOUT
183                )
184            }
185            // Other retryable errors
186            OdosError::Timeout(_) => true,
187            OdosError::RateLimit(_) => true,
188            // Non-retryable errors
189            OdosError::Json(_)
190            | OdosError::Hex(_)
191            | OdosError::InvalidInput(_)
192            | OdosError::MissingData(_)
193            | OdosError::UnsupportedChain { .. }
194            | OdosError::Contract(_)
195            | OdosError::TransactionAssembly(_)
196            | OdosError::QuoteRequest(_)
197            | OdosError::Configuration(_)
198            | OdosError::Internal(_) => false,
199        }
200    }
201
202    /// Get the error category for metrics
203    pub fn category(&self) -> &'static str {
204        match self {
205            OdosError::Http(_) => "http",
206            OdosError::Api { .. } => "api",
207            OdosError::Json(_) => "json",
208            OdosError::Hex(_) => "hex",
209            OdosError::InvalidInput(_) => "invalid_input",
210            OdosError::MissingData(_) => "missing_data",
211            OdosError::UnsupportedChain { .. } => "unsupported_chain",
212            OdosError::Contract(_) => "contract",
213            OdosError::TransactionAssembly(_) => "transaction_assembly",
214            OdosError::QuoteRequest(_) => "quote_request",
215            OdosError::Configuration(_) => "configuration",
216            OdosError::Timeout(_) => "timeout",
217            OdosError::RateLimit(_) => "rate_limit",
218            OdosError::Internal(_) => "internal",
219        }
220    }
221}
222
223// Compatibility with anyhow for gradual migration
224impl From<anyhow::Error> for OdosError {
225    fn from(err: anyhow::Error) -> Self {
226        Self::Internal(err.to_string())
227    }
228}
229
230// Convert chain errors to appropriate error types
231impl From<OdosChainError> for OdosError {
232    fn from(err: OdosChainError) -> Self {
233        match err {
234            OdosChainError::V2NotAvailable { chain } => {
235                Self::contract_error(format!("V2 router not available on chain: {chain}"))
236            }
237            OdosChainError::V3NotAvailable { chain } => {
238                Self::contract_error(format!("V3 router not available on chain: {chain}"))
239            }
240            OdosChainError::UnsupportedChain { chain } => {
241                Self::contract_error(format!("Unsupported chain: {chain}"))
242            }
243            OdosChainError::InvalidAddress { address } => {
244                Self::invalid_input(format!("Invalid address format: {address}"))
245            }
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use reqwest::StatusCode;
254
255    #[test]
256    fn test_retryable_errors() {
257        // HTTP timeout should be retryable
258        let timeout_err = OdosError::timeout_error("Request timed out");
259        assert!(timeout_err.is_retryable());
260
261        // API 500 error should be retryable
262        let api_err = OdosError::api_error(
263            StatusCode::INTERNAL_SERVER_ERROR,
264            "Server error".to_string(),
265        );
266        assert!(api_err.is_retryable());
267
268        // Invalid input should not be retryable
269        let invalid_err = OdosError::invalid_input("Bad parameter");
270        assert!(!invalid_err.is_retryable());
271
272        // Rate limit should be retryable
273        let rate_limit_err = OdosError::rate_limit_error("Too many requests");
274        assert!(rate_limit_err.is_retryable());
275    }
276
277    #[test]
278    fn test_error_categories() {
279        let api_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
280        assert_eq!(api_err.category(), "api");
281
282        let timeout_err = OdosError::timeout_error("Timeout");
283        assert_eq!(timeout_err.category(), "timeout");
284
285        let invalid_err = OdosError::invalid_input("Invalid");
286        assert_eq!(invalid_err.category(), "invalid_input");
287    }
288}