1use alloy_primitives::hex;
2use reqwest::StatusCode;
3use thiserror::Error;
4
5use crate::OdosChainError;
6
7pub type Result<T> = std::result::Result<T, OdosError>;
9
10#[derive(Error, Debug)]
52pub enum OdosError {
53 #[error("HTTP request failed: {0}")]
55 Http(#[from] reqwest::Error),
56
57 #[error("Odos API error (status: {status}): {message}")]
59 Api { status: StatusCode, message: String },
60
61 #[error("JSON processing error: {0}")]
63 Json(#[from] serde_json::Error),
64
65 #[error("Hex decoding error: {0}")]
67 Hex(#[from] hex::FromHexError),
68
69 #[error("Invalid input: {0}")]
71 InvalidInput(String),
72
73 #[error("Missing required data: {0}")]
75 MissingData(String),
76
77 #[error("Chain not supported: {chain_id}")]
79 UnsupportedChain { chain_id: u64 },
80
81 #[error("Contract error: {0}")]
83 Contract(String),
84
85 #[error("Transaction assembly failed: {0}")]
87 TransactionAssembly(String),
88
89 #[error("Quote request failed: {0}")]
91 QuoteRequest(String),
92
93 #[error("Configuration error: {0}")]
95 Configuration(String),
96
97 #[error("Operation timed out: {0}")]
99 Timeout(String),
100
101 #[error("Rate limit exceeded: {0}")]
103 RateLimit(String),
104
105 #[error("Circuit breaker is open: {0}")]
126 CircuitBreakerOpen(String),
127
128 #[error("Internal error: {0}")]
130 Internal(String),
131}
132
133impl OdosError {
134 pub fn api_error(status: StatusCode, message: String) -> Self {
136 Self::Api { status, message }
137 }
138
139 pub fn invalid_input(message: impl Into<String>) -> Self {
141 Self::InvalidInput(message.into())
142 }
143
144 pub fn missing_data(message: impl Into<String>) -> Self {
146 Self::MissingData(message.into())
147 }
148
149 pub fn unsupported_chain(chain_id: u64) -> Self {
151 Self::UnsupportedChain { chain_id }
152 }
153
154 pub fn contract_error(message: impl Into<String>) -> Self {
156 Self::Contract(message.into())
157 }
158
159 pub fn transaction_assembly_error(message: impl Into<String>) -> Self {
161 Self::TransactionAssembly(message.into())
162 }
163
164 pub fn quote_request_error(message: impl Into<String>) -> Self {
166 Self::QuoteRequest(message.into())
167 }
168
169 pub fn configuration_error(message: impl Into<String>) -> Self {
171 Self::Configuration(message.into())
172 }
173
174 pub fn timeout_error(message: impl Into<String>) -> Self {
176 Self::Timeout(message.into())
177 }
178
179 pub fn rate_limit_error(message: impl Into<String>) -> Self {
181 Self::RateLimit(message.into())
182 }
183
184 pub fn circuit_breaker_error(message: impl Into<String>) -> Self {
186 Self::CircuitBreakerOpen(message.into())
187 }
188
189 pub fn internal_error(message: impl Into<String>) -> Self {
191 Self::Internal(message.into())
192 }
193
194 pub fn is_retryable(&self) -> bool {
196 match self {
197 OdosError::Http(err) => {
199 err.is_timeout() || err.is_connect() || err.is_request()
201 }
202 OdosError::Api { status, .. } => {
204 matches!(
205 *status,
206 StatusCode::TOO_MANY_REQUESTS
207 | StatusCode::INTERNAL_SERVER_ERROR
208 | StatusCode::BAD_GATEWAY
209 | StatusCode::SERVICE_UNAVAILABLE
210 | StatusCode::GATEWAY_TIMEOUT
211 )
212 }
213 OdosError::Timeout(_) => true,
215 OdosError::RateLimit(_) => true,
216 OdosError::Json(_)
218 | OdosError::Hex(_)
219 | OdosError::InvalidInput(_)
220 | OdosError::MissingData(_)
221 | OdosError::UnsupportedChain { .. }
222 | OdosError::Contract(_)
223 | OdosError::TransactionAssembly(_)
224 | OdosError::QuoteRequest(_)
225 | OdosError::Configuration(_)
226 | OdosError::CircuitBreakerOpen(_)
227 | OdosError::Internal(_) => false,
228 }
229 }
230
231 pub fn category(&self) -> &'static str {
233 match self {
234 OdosError::Http(_) => "http",
235 OdosError::Api { .. } => "api",
236 OdosError::Json(_) => "json",
237 OdosError::Hex(_) => "hex",
238 OdosError::InvalidInput(_) => "invalid_input",
239 OdosError::MissingData(_) => "missing_data",
240 OdosError::UnsupportedChain { .. } => "unsupported_chain",
241 OdosError::Contract(_) => "contract",
242 OdosError::TransactionAssembly(_) => "transaction_assembly",
243 OdosError::QuoteRequest(_) => "quote_request",
244 OdosError::Configuration(_) => "configuration",
245 OdosError::Timeout(_) => "timeout",
246 OdosError::RateLimit(_) => "rate_limit",
247 OdosError::CircuitBreakerOpen(_) => "circuit_breaker",
248 OdosError::Internal(_) => "internal",
249 }
250 }
251}
252
253impl From<anyhow::Error> for OdosError {
255 fn from(err: anyhow::Error) -> Self {
256 Self::Internal(err.to_string())
257 }
258}
259
260impl From<OdosChainError> for OdosError {
262 fn from(err: OdosChainError) -> Self {
263 match err {
264 OdosChainError::V2NotAvailable { chain } => {
265 Self::contract_error(format!("V2 router not available on chain: {chain}"))
266 }
267 OdosChainError::V3NotAvailable { chain } => {
268 Self::contract_error(format!("V3 router not available on chain: {chain}"))
269 }
270 OdosChainError::UnsupportedChain { chain } => {
271 Self::contract_error(format!("Unsupported chain: {chain}"))
272 }
273 OdosChainError::InvalidAddress { address } => {
274 Self::invalid_input(format!("Invalid address format: {address}"))
275 }
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use reqwest::StatusCode;
284
285 #[test]
286 fn test_retryable_errors() {
287 let timeout_err = OdosError::timeout_error("Request timed out");
289 assert!(timeout_err.is_retryable());
290
291 let api_err = OdosError::api_error(
293 StatusCode::INTERNAL_SERVER_ERROR,
294 "Server error".to_string(),
295 );
296 assert!(api_err.is_retryable());
297
298 let invalid_err = OdosError::invalid_input("Bad parameter");
300 assert!(!invalid_err.is_retryable());
301
302 let rate_limit_err = OdosError::rate_limit_error("Too many requests");
304 assert!(rate_limit_err.is_retryable());
305 }
306
307 #[test]
308 fn test_error_categories() {
309 let api_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
310 assert_eq!(api_err.category(), "api");
311
312 let timeout_err = OdosError::timeout_error("Timeout");
313 assert_eq!(timeout_err.category(), "timeout");
314
315 let invalid_err = OdosError::invalid_input("Invalid");
316 assert_eq!(invalid_err.category(), "invalid_input");
317 }
318}