odos_sdk/
error_code.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4//! Strongly-typed Odos API error codes
5//!
6//! This module provides type-safe representations of error codes returned by the Odos API.
7//! Error codes are organized into categories (General, Algo, Internal Service, Validation)
8//! and provide helper methods for error inspection and retryability logic.
9//!
10//! # [Error Code Index](https://docs.odos.xyz/build/api_errors)
11//!
12//! - **General API Errors (1XXX)**: Basic API errors
13//! - **Algo/Quote Errors (2XXX)**: Routing and quote generation errors
14//! - **Internal Service Errors (3XXX)**: Backend service errors
15//! - **Validation Errors (4XXX)**: Request validation errors
16//! - **Internal Errors (5XXX)**: System-level internal errors
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use uuid::Uuid;
21
22/// Strongly-typed trace ID for Odos API error tracking
23///
24/// Wraps a UUID to prevent confusion with other UUID types in the system.
25/// Each error response from Odos includes a unique trace ID for debugging.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct TraceId(pub Uuid);
29
30impl TraceId {
31    /// Create a new TraceId from a UUID
32    pub fn new(uuid: Uuid) -> Self {
33        Self(uuid)
34    }
35
36    /// Get the inner UUID
37    pub fn as_uuid(&self) -> Uuid {
38        self.0
39    }
40}
41
42impl fmt::Display for TraceId {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "{}", self.0)
45    }
46}
47
48impl From<Uuid> for TraceId {
49    fn from(uuid: Uuid) -> Self {
50        Self(uuid)
51    }
52}
53
54/// Error code category for grouping related errors
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ErrorCategory {
57    /// General API errors (1XXX)
58    General,
59    /// Algorithm/Quote errors (2XXX)
60    Algo,
61    /// Internal service errors (3XXX)
62    InternalService,
63    /// Validation errors (4XXX)
64    Validation,
65    /// System internal errors (5XXX)
66    Internal,
67    /// Unknown error code
68    Unknown,
69}
70
71/// Strongly-typed Odos API error codes
72///
73/// Each variant represents a specific error condition documented by Odos.
74/// Error codes are grouped by category (1XXX-5XXX ranges).
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum OdosErrorCode {
77    // General Odos API errors (1XXX)
78    /// General API error (1000)
79    ApiError,
80
81    // Odos Algo/Quote errors (2XXX)
82    /// No viable routing path found (2000)
83    NoViablePath,
84    /// Algorithm validation error (2400)
85    AlgoValidationError,
86    /// Algorithm connection error (2997)
87    AlgoConnectionError,
88    /// Algorithm timeout (2998)
89    AlgoTimeout,
90    /// Algorithm internal error (2999)
91    AlgoInternal,
92
93    // Odos Internal Service errors (3XXX)
94    /// Internal service error (3000)
95    InternalServiceError,
96
97    // Config service errors (31XX)
98    /// Config service internal error (3100)
99    ConfigInternal,
100    /// Config service connection error (3101)
101    ConfigConnectionError,
102    /// Config service timeout (3102)
103    ConfigTimeout,
104
105    // Transaction assembly errors (311X)
106    /// Transaction assembly internal error (3110)
107    TxnAssemblyInternal,
108    /// Transaction assembly connection error (3111)
109    TxnAssemblyConnectionError,
110    /// Transaction assembly timeout (3112)
111    TxnAssemblyTimeout,
112
113    // Chain data errors (312X)
114    /// Chain data internal error (3120)
115    ChainDataInternal,
116    /// Chain data connection error (3121)
117    ChainDataConnectionError,
118    /// Chain data timeout (3122)
119    ChainDataTimeout,
120
121    // Pricing service errors (313X)
122    /// Pricing service internal error (3130)
123    PricingInternal,
124    /// Pricing service connection error (3131)
125    PricingConnectionError,
126    /// Pricing service timeout (3132)
127    PricingTimeout,
128
129    // Gas service errors (314X)
130    /// Gas service internal error (3140)
131    GasInternal,
132    /// Gas service connection error (3141)
133    GasConnectionError,
134    /// Gas service timeout (3142)
135    GasTimeout,
136    /// Gas data unavailable (3143)
137    GasUnavailable,
138
139    // Odos Validation errors (4XXX)
140    /// Invalid request (4000)
141    InvalidRequest,
142
143    // General/Quote errors (40XX)
144    /// Invalid chain ID (4001)
145    InvalidChainId,
146    /// Invalid input tokens (4002)
147    InvalidInputTokens,
148    /// Invalid output tokens (4003)
149    InvalidOutputTokens,
150    /// Invalid user address (4004)
151    InvalidUserAddr,
152    /// Blocked user address (4005)
153    BlockedUserAddr,
154    /// Slippage tolerance too high (4006)
155    TooSlippery,
156    /// Same token for input and output (4007)
157    SameInputOutput,
158    /// Multiple zap outputs not supported (4008)
159    MultiZapOutput,
160    /// Invalid token count (4009)
161    InvalidTokenCount,
162    /// Invalid token address (4010)
163    InvalidTokenAddr,
164    /// Non-integer token amount (4011)
165    NonIntegerTokenAmount,
166    /// Negative token amount (4012)
167    NegativeTokenAmount,
168    /// Same tokens in input and output (4013)
169    SameInputOutputTokens,
170    /// Token is blacklisted (4014)
171    TokenBlacklisted,
172    /// Invalid token proportions (4015)
173    InvalidTokenProportions,
174    /// Token routing unavailable (4016)
175    TokenRoutingUnavailable,
176    /// Invalid referral code (4017)
177    InvalidReferralCode,
178    /// Invalid token amount (4018)
179    InvalidTokenAmount,
180    /// Non-string token amount (4019)
181    NonStringTokenAmount,
182
183    // Assembly errors (41XX)
184    /// Invalid assembly request (4100)
185    InvalidAssemblyRequest,
186    /// Invalid user address in assembly (4101)
187    InvalidAssemblyUserAddr,
188    /// Invalid receiver address (4102)
189    InvalidReceiverAddr,
190
191    // Swap errors (42XX)
192    /// Invalid swap request (4200)
193    InvalidSwapRequest,
194    /// User address required (4201)
195    UserAddrRequired,
196
197    // Odos Internal errors (5XXX)
198    /// Internal error (5000)
199    InternalError,
200    /// Swap unavailable (5001)
201    SwapUnavailable,
202    /// Price check failure (5002)
203    PriceCheckFailure,
204    /// Default gas calculation failure (5003)
205    DefaultGasFailure,
206
207    /// Unknown error code
208    Unknown(u16),
209}
210
211impl OdosErrorCode {
212    /// Get the numeric error code value
213    pub fn code(&self) -> u16 {
214        match self {
215            Self::ApiError => 1000,
216            Self::NoViablePath => 2000,
217            Self::AlgoValidationError => 2400,
218            Self::AlgoConnectionError => 2997,
219            Self::AlgoTimeout => 2998,
220            Self::AlgoInternal => 2999,
221            Self::InternalServiceError => 3000,
222            Self::ConfigInternal => 3100,
223            Self::ConfigConnectionError => 3101,
224            Self::ConfigTimeout => 3102,
225            Self::TxnAssemblyInternal => 3110,
226            Self::TxnAssemblyConnectionError => 3111,
227            Self::TxnAssemblyTimeout => 3112,
228            Self::ChainDataInternal => 3120,
229            Self::ChainDataConnectionError => 3121,
230            Self::ChainDataTimeout => 3122,
231            Self::PricingInternal => 3130,
232            Self::PricingConnectionError => 3131,
233            Self::PricingTimeout => 3132,
234            Self::GasInternal => 3140,
235            Self::GasConnectionError => 3141,
236            Self::GasTimeout => 3142,
237            Self::GasUnavailable => 3143,
238            Self::InvalidRequest => 4000,
239            Self::InvalidChainId => 4001,
240            Self::InvalidInputTokens => 4002,
241            Self::InvalidOutputTokens => 4003,
242            Self::InvalidUserAddr => 4004,
243            Self::BlockedUserAddr => 4005,
244            Self::TooSlippery => 4006,
245            Self::SameInputOutput => 4007,
246            Self::MultiZapOutput => 4008,
247            Self::InvalidTokenCount => 4009,
248            Self::InvalidTokenAddr => 4010,
249            Self::NonIntegerTokenAmount => 4011,
250            Self::NegativeTokenAmount => 4012,
251            Self::SameInputOutputTokens => 4013,
252            Self::TokenBlacklisted => 4014,
253            Self::InvalidTokenProportions => 4015,
254            Self::TokenRoutingUnavailable => 4016,
255            Self::InvalidReferralCode => 4017,
256            Self::InvalidTokenAmount => 4018,
257            Self::NonStringTokenAmount => 4019,
258            Self::InvalidAssemblyRequest => 4100,
259            Self::InvalidAssemblyUserAddr => 4101,
260            Self::InvalidReceiverAddr => 4102,
261            Self::InvalidSwapRequest => 4200,
262            Self::UserAddrRequired => 4201,
263            Self::InternalError => 5000,
264            Self::SwapUnavailable => 5001,
265            Self::PriceCheckFailure => 5002,
266            Self::DefaultGasFailure => 5003,
267            Self::Unknown(code) => *code,
268        }
269    }
270
271    /// Get the error category
272    pub fn category(&self) -> ErrorCategory {
273        let code = self.code();
274        match code {
275            1000..=1999 => ErrorCategory::General,
276            2000..=2999 => ErrorCategory::Algo,
277            3000..=3999 => ErrorCategory::InternalService,
278            4000..=4999 => ErrorCategory::Validation,
279            5000..=5999 => ErrorCategory::Internal,
280            _ => ErrorCategory::Unknown,
281        }
282    }
283
284    /// Check if this is a general API error
285    pub fn is_general_error(&self) -> bool {
286        matches!(self.category(), ErrorCategory::General)
287    }
288
289    /// Check if this is an algorithm/quote error
290    pub fn is_algo_error(&self) -> bool {
291        matches!(self.category(), ErrorCategory::Algo)
292    }
293
294    /// Check if this is an internal service error
295    pub fn is_internal_service_error(&self) -> bool {
296        matches!(self.category(), ErrorCategory::InternalService)
297    }
298
299    /// Check if this is a validation error
300    pub fn is_validation_error(&self) -> bool {
301        matches!(self.category(), ErrorCategory::Validation)
302    }
303
304    /// Check if this is an internal error
305    pub fn is_internal_error(&self) -> bool {
306        matches!(self.category(), ErrorCategory::Internal)
307    }
308
309    /// Check if this specific error is no viable path
310    pub fn is_no_viable_path(&self) -> bool {
311        matches!(self, Self::NoViablePath)
312    }
313
314    /// Check if this is an invalid chain ID error
315    pub fn is_invalid_chain_id(&self) -> bool {
316        matches!(self, Self::InvalidChainId)
317    }
318
319    /// Check if this is a blocked user address error
320    pub fn is_blocked_user(&self) -> bool {
321        matches!(self, Self::BlockedUserAddr)
322    }
323
324    /// Check if this is a timeout error (any service)
325    pub fn is_timeout(&self) -> bool {
326        matches!(
327            self,
328            Self::AlgoTimeout
329                | Self::ConfigTimeout
330                | Self::TxnAssemblyTimeout
331                | Self::ChainDataTimeout
332                | Self::PricingTimeout
333                | Self::GasTimeout
334        )
335    }
336
337    /// Check if this is a connection error (any service)
338    pub fn is_connection_error(&self) -> bool {
339        matches!(
340            self,
341            Self::AlgoConnectionError
342                | Self::ConfigConnectionError
343                | Self::TxnAssemblyConnectionError
344                | Self::ChainDataConnectionError
345                | Self::PricingConnectionError
346                | Self::GasConnectionError
347        )
348    }
349
350    /// Check if this error indicates the request should be retried
351    ///
352    /// Retryable errors include:
353    /// - Timeouts (algo, config, assembly, chain data, pricing, gas)
354    /// - Connection errors (all services)
355    /// - Internal errors (algo, services, general)
356    /// - Gas unavailable
357    pub fn is_retryable(&self) -> bool {
358        self.is_timeout()
359            || self.is_connection_error()
360            || matches!(
361                self,
362                Self::AlgoInternal
363                    | Self::ConfigInternal
364                    | Self::TxnAssemblyInternal
365                    | Self::ChainDataInternal
366                    | Self::PricingInternal
367                    | Self::GasInternal
368                    | Self::GasUnavailable
369                    | Self::InternalServiceError
370                    | Self::InternalError
371            )
372    }
373
374    /// Check if this error indicates the token cannot be routed
375    ///
376    /// This is NOT an error condition - it's Odos correctly responding that
377    /// the token cannot be swapped. Common reasons include:
378    /// - Token is not supported by Odos (TokenRoutingUnavailable)
379    /// - Token is blacklisted (TokenBlacklisted)
380    /// - Token is not recognized (InvalidInputTokens/InvalidOutputTokens)
381    /// - No liquidity path exists (NoViablePath)
382    ///
383    /// These should be tracked separately from actual errors for metrics purposes.
384    pub fn is_unroutable_token(&self) -> bool {
385        matches!(
386            self,
387            Self::TokenRoutingUnavailable
388                | Self::TokenBlacklisted
389                | Self::InvalidInputTokens
390                | Self::InvalidOutputTokens
391                | Self::NoViablePath
392        )
393    }
394}
395
396impl From<u16> for OdosErrorCode {
397    fn from(code: u16) -> Self {
398        match code {
399            1000 => Self::ApiError,
400            2000 => Self::NoViablePath,
401            2400 => Self::AlgoValidationError,
402            2997 => Self::AlgoConnectionError,
403            2998 => Self::AlgoTimeout,
404            2999 => Self::AlgoInternal,
405            3000 => Self::InternalServiceError,
406            3100 => Self::ConfigInternal,
407            3101 => Self::ConfigConnectionError,
408            3102 => Self::ConfigTimeout,
409            3110 => Self::TxnAssemblyInternal,
410            3111 => Self::TxnAssemblyConnectionError,
411            3112 => Self::TxnAssemblyTimeout,
412            3120 => Self::ChainDataInternal,
413            3121 => Self::ChainDataConnectionError,
414            3122 => Self::ChainDataTimeout,
415            3130 => Self::PricingInternal,
416            3131 => Self::PricingConnectionError,
417            3132 => Self::PricingTimeout,
418            3140 => Self::GasInternal,
419            3141 => Self::GasConnectionError,
420            3142 => Self::GasTimeout,
421            3143 => Self::GasUnavailable,
422            4000 => Self::InvalidRequest,
423            4001 => Self::InvalidChainId,
424            4002 => Self::InvalidInputTokens,
425            4003 => Self::InvalidOutputTokens,
426            4004 => Self::InvalidUserAddr,
427            4005 => Self::BlockedUserAddr,
428            4006 => Self::TooSlippery,
429            4007 => Self::SameInputOutput,
430            4008 => Self::MultiZapOutput,
431            4009 => Self::InvalidTokenCount,
432            4010 => Self::InvalidTokenAddr,
433            4011 => Self::NonIntegerTokenAmount,
434            4012 => Self::NegativeTokenAmount,
435            4013 => Self::SameInputOutputTokens,
436            4014 => Self::TokenBlacklisted,
437            4015 => Self::InvalidTokenProportions,
438            4016 => Self::TokenRoutingUnavailable,
439            4017 => Self::InvalidReferralCode,
440            4018 => Self::InvalidTokenAmount,
441            4019 => Self::NonStringTokenAmount,
442            4100 => Self::InvalidAssemblyRequest,
443            4101 => Self::InvalidAssemblyUserAddr,
444            4102 => Self::InvalidReceiverAddr,
445            4200 => Self::InvalidSwapRequest,
446            4201 => Self::UserAddrRequired,
447            5000 => Self::InternalError,
448            5001 => Self::SwapUnavailable,
449            5002 => Self::PriceCheckFailure,
450            5003 => Self::DefaultGasFailure,
451            _ => Self::Unknown(code),
452        }
453    }
454}
455
456impl From<OdosErrorCode> for u16 {
457    fn from(error_code: OdosErrorCode) -> Self {
458        error_code.code()
459    }
460}
461
462impl fmt::Display for OdosErrorCode {
463    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
464        match self {
465            Self::ApiError => write!(f, "1000 (API_ERROR)"),
466            Self::NoViablePath => write!(f, "2000 (NO_VIABLE_PATH)"),
467            Self::AlgoValidationError => write!(f, "2400 (ALGO_VALIDATION_ERR)"),
468            Self::AlgoConnectionError => write!(f, "2997 (ALGO_CONN_ERR)"),
469            Self::AlgoTimeout => write!(f, "2998 (ALGO_TIMEOUT)"),
470            Self::AlgoInternal => write!(f, "2999 (ALGO_INTERNAL)"),
471            Self::InternalServiceError => write!(f, "3000 (INTERNAL_SERVICE_ERROR)"),
472            Self::ConfigInternal => write!(f, "3100 (CONFIG_INTERNAL)"),
473            Self::ConfigConnectionError => write!(f, "3101 (CONFIG_CONN_ERR)"),
474            Self::ConfigTimeout => write!(f, "3102 (CONFIG_TIMEOUT)"),
475            Self::TxnAssemblyInternal => write!(f, "3110 (TXN_ASSEMBLY_INTERNAL)"),
476            Self::TxnAssemblyConnectionError => write!(f, "3111 (TXN_ASSEMBLY_CONN_ERR)"),
477            Self::TxnAssemblyTimeout => write!(f, "3112 (TXN_ASSEMBLY_TIMEOUT)"),
478            Self::ChainDataInternal => write!(f, "3120 (CHAIN_DATA_INTERNAL)"),
479            Self::ChainDataConnectionError => write!(f, "3121 (CHAIN_DATA_CONN_ERR)"),
480            Self::ChainDataTimeout => write!(f, "3122 (CHAIN_DATA_TIMEOUT)"),
481            Self::PricingInternal => write!(f, "3130 (PRICING_INTERNAL)"),
482            Self::PricingConnectionError => write!(f, "3131 (PRICING_CONN_ERR)"),
483            Self::PricingTimeout => write!(f, "3132 (PRICING_TIMEOUT)"),
484            Self::GasInternal => write!(f, "3140 (GAS_INTERNAL)"),
485            Self::GasConnectionError => write!(f, "3141 (GAS_CONN_ERR)"),
486            Self::GasTimeout => write!(f, "3142 (GAS_TIMEOUT)"),
487            Self::GasUnavailable => write!(f, "3143 (GAS_UNAVAILABLE)"),
488            Self::InvalidRequest => write!(f, "4000 (INVALID_REQUEST)"),
489            Self::InvalidChainId => write!(f, "4001 (INVALID_CHAIN_ID)"),
490            Self::InvalidInputTokens => write!(f, "4002 (INVALID_INPUT_TOKENS)"),
491            Self::InvalidOutputTokens => write!(f, "4003 (INVALID_OUTPUT_TOKENS)"),
492            Self::InvalidUserAddr => write!(f, "4004 (INVALID_USER_ADDR)"),
493            Self::BlockedUserAddr => write!(f, "4005 (BLOCKED_USER_ADDR)"),
494            Self::TooSlippery => write!(f, "4006 (TOO_SLIPPERY)"),
495            Self::SameInputOutput => write!(f, "4007 (SAME_INPUT_OUTPUT)"),
496            Self::MultiZapOutput => write!(f, "4008 (MULTI_ZAP_OUTPUT)"),
497            Self::InvalidTokenCount => write!(f, "4009 (INVALID_TOKEN_COUNT)"),
498            Self::InvalidTokenAddr => write!(f, "4010 (INVALID_TOKEN_ADDR)"),
499            Self::NonIntegerTokenAmount => write!(f, "4011 (NON_INTEGER_TOKEN_AMOUNT)"),
500            Self::NegativeTokenAmount => write!(f, "4012 (NEGATIVE_TOKEN_AMOUNT)"),
501            Self::SameInputOutputTokens => write!(f, "4013 (SAME_INPUT_OUTPUT_TOKENS)"),
502            Self::TokenBlacklisted => write!(f, "4014 (TOKEN_BLACKLISTED)"),
503            Self::InvalidTokenProportions => write!(f, "4015 (INVALID_TOKEN_PROPORTIONS)"),
504            Self::TokenRoutingUnavailable => write!(f, "4016 (TOKEN_ROUTING_UNAVAILABLE)"),
505            Self::InvalidReferralCode => write!(f, "4017 (INVALID_REFERRAL_CODE)"),
506            Self::InvalidTokenAmount => write!(f, "4018 (INVALID_TOKEN_AMOUNT)"),
507            Self::NonStringTokenAmount => write!(f, "4019 (NON_STRING_TOKEN_AMOUNT)"),
508            Self::InvalidAssemblyRequest => write!(f, "4100 (INVALID_ASSEMBLY_REQUEST)"),
509            Self::InvalidAssemblyUserAddr => write!(f, "4101 (INVALID_USER_ADDR)"),
510            Self::InvalidReceiverAddr => write!(f, "4102 (INVALID_RECEIVER_ADDR)"),
511            Self::InvalidSwapRequest => write!(f, "4200 (INVALID_SWAP_REQUEST)"),
512            Self::UserAddrRequired => write!(f, "4201 (USER_ADDR_REQ)"),
513            Self::InternalError => write!(f, "5000 (INTERNAL_ERROR)"),
514            Self::SwapUnavailable => write!(f, "5001 (SWAP_UNAVAILABLE)"),
515            Self::PriceCheckFailure => write!(f, "5002 (PRICE_CHECK_FAILURE)"),
516            Self::DefaultGasFailure => write!(f, "5003 (DEFAULT_GAS_FAILURE)"),
517            Self::Unknown(code) => write!(f, "{code} (UNKNOWN)"),
518        }
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn test_trace_id_creation() {
528        let uuid = Uuid::parse_str("10becdc8-a021-4491-8201-a17b657204e0").unwrap();
529        let trace_id = TraceId::new(uuid);
530        assert_eq!(trace_id.as_uuid(), uuid);
531        assert_eq!(trace_id.to_string(), "10becdc8-a021-4491-8201-a17b657204e0");
532    }
533
534    #[test]
535    fn test_error_code_from_u16() {
536        assert_eq!(OdosErrorCode::from(2999), OdosErrorCode::AlgoInternal);
537        assert_eq!(OdosErrorCode::from(4001), OdosErrorCode::InvalidChainId);
538        assert_eq!(OdosErrorCode::from(3142), OdosErrorCode::GasTimeout);
539        assert_eq!(OdosErrorCode::from(9999), OdosErrorCode::Unknown(9999));
540    }
541
542    #[test]
543    fn test_error_code_to_u16() {
544        assert_eq!(OdosErrorCode::AlgoInternal.code(), 2999);
545        assert_eq!(OdosErrorCode::InvalidChainId.code(), 4001);
546        assert_eq!(OdosErrorCode::Unknown(9999).code(), 9999);
547    }
548
549    #[test]
550    fn test_error_categories() {
551        assert!(OdosErrorCode::ApiError.is_general_error());
552        assert!(OdosErrorCode::NoViablePath.is_algo_error());
553        assert!(OdosErrorCode::ConfigInternal.is_internal_service_error());
554        assert!(OdosErrorCode::InvalidChainId.is_validation_error());
555        assert!(OdosErrorCode::InternalError.is_internal_error());
556    }
557
558    #[test]
559    fn test_specific_error_checks() {
560        assert!(OdosErrorCode::NoViablePath.is_no_viable_path());
561        assert!(OdosErrorCode::InvalidChainId.is_invalid_chain_id());
562        assert!(OdosErrorCode::BlockedUserAddr.is_blocked_user());
563
564        assert!(!OdosErrorCode::ApiError.is_no_viable_path());
565        assert!(!OdosErrorCode::AlgoInternal.is_invalid_chain_id());
566    }
567
568    #[test]
569    fn test_timeout_detection() {
570        assert!(OdosErrorCode::AlgoTimeout.is_timeout());
571        assert!(OdosErrorCode::ConfigTimeout.is_timeout());
572        assert!(OdosErrorCode::TxnAssemblyTimeout.is_timeout());
573        assert!(OdosErrorCode::ChainDataTimeout.is_timeout());
574        assert!(OdosErrorCode::PricingTimeout.is_timeout());
575        assert!(OdosErrorCode::GasTimeout.is_timeout());
576
577        assert!(!OdosErrorCode::AlgoInternal.is_timeout());
578        assert!(!OdosErrorCode::InvalidChainId.is_timeout());
579    }
580
581    #[test]
582    fn test_connection_error_detection() {
583        assert!(OdosErrorCode::AlgoConnectionError.is_connection_error());
584        assert!(OdosErrorCode::ConfigConnectionError.is_connection_error());
585        assert!(OdosErrorCode::GasConnectionError.is_connection_error());
586
587        assert!(!OdosErrorCode::AlgoInternal.is_connection_error());
588        assert!(!OdosErrorCode::InvalidChainId.is_connection_error());
589    }
590
591    #[test]
592    fn test_retryability() {
593        // Timeouts are retryable
594        assert!(OdosErrorCode::AlgoTimeout.is_retryable());
595        assert!(OdosErrorCode::GasTimeout.is_retryable());
596
597        // Connection errors are retryable
598        assert!(OdosErrorCode::AlgoConnectionError.is_retryable());
599        assert!(OdosErrorCode::PricingConnectionError.is_retryable());
600
601        // Internal errors are retryable
602        assert!(OdosErrorCode::AlgoInternal.is_retryable());
603        assert!(OdosErrorCode::InternalServiceError.is_retryable());
604        assert!(OdosErrorCode::GasUnavailable.is_retryable());
605
606        // Validation errors are not retryable
607        assert!(!OdosErrorCode::InvalidChainId.is_retryable());
608        assert!(!OdosErrorCode::BlockedUserAddr.is_retryable());
609        assert!(!OdosErrorCode::InvalidTokenAmount.is_retryable());
610
611        // No viable path is not retryable
612        assert!(!OdosErrorCode::NoViablePath.is_retryable());
613    }
614
615    #[test]
616    fn test_display_format() {
617        assert_eq!(
618            OdosErrorCode::AlgoInternal.to_string(),
619            "2999 (ALGO_INTERNAL)"
620        );
621        assert_eq!(
622            OdosErrorCode::InvalidChainId.to_string(),
623            "4001 (INVALID_CHAIN_ID)"
624        );
625        assert_eq!(OdosErrorCode::Unknown(9999).to_string(), "9999 (UNKNOWN)");
626    }
627
628    #[test]
629    fn test_unroutable_token_detection() {
630        // These indicate the token cannot be routed (expected behavior, not errors)
631        assert!(OdosErrorCode::NoViablePath.is_unroutable_token());
632        assert!(OdosErrorCode::TokenRoutingUnavailable.is_unroutable_token());
633        assert!(OdosErrorCode::TokenBlacklisted.is_unroutable_token());
634        assert!(OdosErrorCode::InvalidInputTokens.is_unroutable_token());
635        assert!(OdosErrorCode::InvalidOutputTokens.is_unroutable_token());
636
637        // These are NOT unroutable token indicators
638        assert!(!OdosErrorCode::AlgoTimeout.is_unroutable_token());
639        assert!(!OdosErrorCode::InternalError.is_unroutable_token());
640        assert!(!OdosErrorCode::InvalidChainId.is_unroutable_token());
641        assert!(!OdosErrorCode::BlockedUserAddr.is_unroutable_token());
642    }
643}