use std::time::Duration;
use alloy_primitives::hex;
use reqwest::StatusCode;
use thiserror::Error;
use crate::{
error_code::{OdosErrorCode, TraceId},
OdosChainError,
};
pub type Result<T> = std::result::Result<T, OdosError>;
#[derive(Error, Debug)]
pub enum OdosError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Odos API error (status: {status}): {message}{}", trace_id.map(|tid| format!(" [trace: {}]", tid)).unwrap_or_default())]
Api {
status: StatusCode,
message: String,
code: OdosErrorCode,
trace_id: Option<TraceId>,
},
#[error("JSON processing error: {0}")]
Json(#[from] serde_json::Error),
#[error("Hex decoding error: {0}")]
Hex(#[from] hex::FromHexError),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Missing required data: {0}")]
MissingData(String),
#[error("Chain not supported: {chain_id}")]
UnsupportedChain { chain_id: u64 },
#[error("Contract error: {0}")]
Contract(String),
#[error("Transaction assembly failed: {0}")]
TransactionAssembly(String),
#[error("Quote request failed: {0}")]
QuoteRequest(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Operation timed out: {0}")]
Timeout(String),
#[error("Rate limit exceeded: {message}{}", trace_id.map(|tid| format!(" [trace: {}]", tid)).unwrap_or_default())]
RateLimit {
message: String,
retry_after: Option<Duration>,
code: OdosErrorCode,
trace_id: Option<TraceId>,
},
#[error("Internal error: {0}")]
Internal(String),
}
impl OdosError {
pub fn api_error(status: StatusCode, message: String) -> Self {
Self::api_error_with_code(status, message, OdosErrorCode::Unknown(0), None)
}
pub fn api_error_with_code(
status: StatusCode,
message: String,
code: OdosErrorCode,
trace_id: Option<TraceId>,
) -> Self {
Self::Api {
status,
message,
code,
trace_id,
}
}
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::InvalidInput(message.into())
}
pub fn missing_data(message: impl Into<String>) -> Self {
Self::MissingData(message.into())
}
pub fn unsupported_chain(chain_id: u64) -> Self {
Self::UnsupportedChain { chain_id }
}
pub fn contract_error(message: impl Into<String>) -> Self {
Self::Contract(message.into())
}
pub fn transaction_assembly_error(message: impl Into<String>) -> Self {
Self::TransactionAssembly(message.into())
}
pub fn quote_request_error(message: impl Into<String>) -> Self {
Self::QuoteRequest(message.into())
}
pub fn configuration_error(message: impl Into<String>) -> Self {
Self::Configuration(message.into())
}
pub fn timeout_error(message: impl Into<String>) -> Self {
Self::Timeout(message.into())
}
pub fn rate_limit_error(message: impl Into<String>) -> Self {
Self::rate_limit_error_with_retry_after(message, None)
}
pub fn rate_limit_error_with_retry_after(
message: impl Into<String>,
retry_after: Option<Duration>,
) -> Self {
Self::rate_limit_error_with_retry_after_and_trace(
message,
retry_after,
OdosErrorCode::Unknown(429),
None,
)
}
pub fn rate_limit_error_with_retry_after_and_trace(
message: impl Into<String>,
retry_after: Option<Duration>,
code: OdosErrorCode,
trace_id: Option<TraceId>,
) -> Self {
Self::RateLimit {
message: message.into(),
retry_after,
code,
trace_id,
}
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn is_retryable(&self) -> bool {
match self {
OdosError::Http(err) => err.is_timeout() || err.is_connect() || err.is_request(),
OdosError::Api { status, code, .. } => {
if matches!(code, OdosErrorCode::Unknown(_)) {
matches!(
*status,
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::BAD_GATEWAY
| StatusCode::SERVICE_UNAVAILABLE
| StatusCode::GATEWAY_TIMEOUT
)
} else {
code.is_retryable()
}
}
OdosError::Timeout(_) => true,
OdosError::RateLimit { .. } => false,
OdosError::Json(_)
| OdosError::Hex(_)
| OdosError::InvalidInput(_)
| OdosError::MissingData(_)
| OdosError::UnsupportedChain { .. }
| OdosError::Contract(_)
| OdosError::TransactionAssembly(_)
| OdosError::QuoteRequest(_)
| OdosError::Configuration(_)
| OdosError::Internal(_) => false,
}
}
pub fn is_rate_limit(&self) -> bool {
matches!(self, OdosError::RateLimit { .. })
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
OdosError::RateLimit { retry_after, .. } => *retry_after,
_ => None,
}
}
pub fn error_code(&self) -> Option<&OdosErrorCode> {
match self {
OdosError::Api { code, .. } => Some(code),
OdosError::RateLimit { code, .. } => Some(code),
_ => None,
}
}
pub fn trace_id(&self) -> Option<TraceId> {
match self {
OdosError::Api { trace_id, .. } => *trace_id,
OdosError::RateLimit { trace_id, .. } => *trace_id,
_ => None,
}
}
pub fn category(&self) -> &'static str {
match self {
OdosError::Http(_) => "http",
OdosError::Api { .. } => "api",
OdosError::Json(_) => "json",
OdosError::Hex(_) => "hex",
OdosError::InvalidInput(_) => "invalid_input",
OdosError::MissingData(_) => "missing_data",
OdosError::UnsupportedChain { .. } => "unsupported_chain",
OdosError::Contract(_) => "contract",
OdosError::TransactionAssembly(_) => "transaction_assembly",
OdosError::QuoteRequest(_) => "quote_request",
OdosError::Configuration(_) => "configuration",
OdosError::Timeout(_) => "timeout",
OdosError::RateLimit { .. } => "rate_limit",
OdosError::Internal(_) => "internal",
}
}
pub fn suggested_retry_delay(&self) -> Option<Duration> {
match self {
OdosError::RateLimit { retry_after, .. } => {
Some(retry_after.unwrap_or(Duration::from_secs(60)))
}
OdosError::Timeout(_) => Some(Duration::from_secs(1)),
OdosError::Api { status, .. } if status.is_server_error() => {
Some(Duration::from_secs(2))
}
OdosError::Http(err) => {
if err.is_timeout() {
Some(Duration::from_secs(1))
} else if err.is_connect() || err.is_request() {
Some(Duration::from_millis(500))
} else {
None
}
}
_ => None,
}
}
pub fn is_client_error(&self) -> bool {
matches!(self, OdosError::Api { status, .. } if status.is_client_error())
}
pub fn is_server_error(&self) -> bool {
matches!(self, OdosError::Api { status, .. } if status.is_server_error())
}
}
impl From<OdosChainError> for OdosError {
fn from(err: OdosChainError) -> Self {
match err {
OdosChainError::LimitOrderNotAvailable { chain } => Self::contract_error(format!(
"Limit Order router not available on chain: {chain}"
)),
OdosChainError::V2NotAvailable { chain } => {
Self::contract_error(format!("V2 router not available on chain: {chain}"))
}
OdosChainError::V3NotAvailable { chain } => {
Self::contract_error(format!("V3 router not available on chain: {chain}"))
}
OdosChainError::UnsupportedChain { chain } => {
Self::contract_error(format!("Unsupported chain: {chain}"))
}
OdosChainError::InvalidAddress { address } => {
Self::invalid_input(format!("Invalid address format: {address}"))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::StatusCode;
#[test]
fn test_retryable_errors() {
let timeout_err = OdosError::timeout_error("Request timed out");
assert!(timeout_err.is_retryable());
let api_err = OdosError::api_error(
StatusCode::INTERNAL_SERVER_ERROR,
"Server error".to_string(),
);
assert!(api_err.is_retryable());
let invalid_err = OdosError::invalid_input("Bad parameter");
assert!(!invalid_err.is_retryable());
let rate_limit_err = OdosError::rate_limit_error("Too many requests");
assert!(!rate_limit_err.is_retryable());
}
#[test]
fn test_error_categories() {
let api_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
assert_eq!(api_err.category(), "api");
let timeout_err = OdosError::timeout_error("Timeout");
assert_eq!(timeout_err.category(), "timeout");
let invalid_err = OdosError::invalid_input("Invalid");
assert_eq!(invalid_err.category(), "invalid_input");
}
#[test]
fn test_suggested_retry_delay() {
let rate_limit_with_retry = OdosError::rate_limit_error_with_retry_after(
"Rate limited",
Some(Duration::from_secs(30)),
);
assert_eq!(
rate_limit_with_retry.suggested_retry_delay(),
Some(Duration::from_secs(30))
);
let rate_limit_no_retry = OdosError::rate_limit_error("Rate limited");
assert_eq!(
rate_limit_no_retry.suggested_retry_delay(),
Some(Duration::from_secs(60))
);
let timeout_err = OdosError::timeout_error("Timeout");
assert_eq!(
timeout_err.suggested_retry_delay(),
Some(Duration::from_secs(1))
);
let server_err = OdosError::api_error(
StatusCode::INTERNAL_SERVER_ERROR,
"Server error".to_string(),
);
assert_eq!(
server_err.suggested_retry_delay(),
Some(Duration::from_secs(2))
);
let client_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
assert_eq!(client_err.suggested_retry_delay(), None);
let invalid_err = OdosError::invalid_input("Invalid");
assert_eq!(invalid_err.suggested_retry_delay(), None);
}
#[test]
fn test_client_and_server_errors() {
let client_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
assert!(client_err.is_client_error());
assert!(!client_err.is_server_error());
let server_err = OdosError::api_error(
StatusCode::INTERNAL_SERVER_ERROR,
"Server error".to_string(),
);
assert!(!server_err.is_client_error());
assert!(server_err.is_server_error());
let other_err = OdosError::invalid_input("Invalid");
assert!(!other_err.is_client_error());
assert!(!other_err.is_server_error());
}
}