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("Internal error: {0}")]
107 Internal(String),
108}
109
110impl OdosError {
111 pub fn api_error(status: StatusCode, message: String) -> Self {
113 Self::Api { status, message }
114 }
115
116 pub fn invalid_input(message: impl Into<String>) -> Self {
118 Self::InvalidInput(message.into())
119 }
120
121 pub fn missing_data(message: impl Into<String>) -> Self {
123 Self::MissingData(message.into())
124 }
125
126 pub fn unsupported_chain(chain_id: u64) -> Self {
128 Self::UnsupportedChain { chain_id }
129 }
130
131 pub fn contract_error(message: impl Into<String>) -> Self {
133 Self::Contract(message.into())
134 }
135
136 pub fn transaction_assembly_error(message: impl Into<String>) -> Self {
138 Self::TransactionAssembly(message.into())
139 }
140
141 pub fn quote_request_error(message: impl Into<String>) -> Self {
143 Self::QuoteRequest(message.into())
144 }
145
146 pub fn configuration_error(message: impl Into<String>) -> Self {
148 Self::Configuration(message.into())
149 }
150
151 pub fn timeout_error(message: impl Into<String>) -> Self {
153 Self::Timeout(message.into())
154 }
155
156 pub fn rate_limit_error(message: impl Into<String>) -> Self {
158 Self::RateLimit(message.into())
159 }
160
161 pub fn internal_error(message: impl Into<String>) -> Self {
163 Self::Internal(message.into())
164 }
165
166 pub fn is_retryable(&self) -> bool {
168 match self {
169 OdosError::Http(err) => {
171 err.is_timeout() || err.is_connect() || err.is_request()
173 }
174 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 OdosError::Timeout(_) => true,
187 OdosError::RateLimit(_) => true,
188 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 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
223impl From<anyhow::Error> for OdosError {
225 fn from(err: anyhow::Error) -> Self {
226 Self::Internal(err.to_string())
227 }
228}
229
230impl 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 let timeout_err = OdosError::timeout_error("Request timed out");
259 assert!(timeout_err.is_retryable());
260
261 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 let invalid_err = OdosError::invalid_input("Bad parameter");
270 assert!(!invalid_err.is_retryable());
271
272 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}