Skip to main content

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    ///
92    /// Returned when the Odos routing algorithm cannot stabilise on a path,
93    /// typically for marginal-liquidity or low-value tokens. Despite the
94    /// "please try again" wording in the API response, production evidence
95    /// shows these do not recover within request timescales — successive
96    /// retries return the same error until upstream liquidity changes.
97    /// Classified as **not** in-call retryable; consumers who need to opt
98    /// back in to retrying 2999 can replace the default policy via
99    /// [`RetryPredicate::Replace`] (a `DefaultExcept` veto can only subtract
100    /// from the default tree, so it cannot promote a non-retryable code).
101    ///
102    /// [`RetryPredicate::Replace`]: crate::RetryPredicate::Replace
103    AlgoInternal,
104
105    // Odos Internal Service errors (3XXX)
106    /// Internal service error (3000)
107    InternalServiceError,
108
109    // Config service errors (31XX)
110    /// Config service internal error (3100)
111    ConfigInternal,
112    /// Config service connection error (3101)
113    ConfigConnectionError,
114    /// Config service timeout (3102)
115    ConfigTimeout,
116
117    // Transaction assembly errors (311X)
118    /// Transaction assembly internal error (3110)
119    TxnAssemblyInternal,
120    /// Transaction assembly connection error (3111)
121    TxnAssemblyConnectionError,
122    /// Transaction assembly timeout (3112)
123    TxnAssemblyTimeout,
124
125    // Chain data errors (312X)
126    /// Chain data internal error (3120)
127    ChainDataInternal,
128    /// Chain data connection error (3121)
129    ChainDataConnectionError,
130    /// Chain data timeout (3122)
131    ChainDataTimeout,
132
133    // Pricing service errors (313X)
134    /// Pricing service internal error (3130)
135    PricingInternal,
136    /// Pricing service connection error (3131)
137    PricingConnectionError,
138    /// Pricing service timeout (3132)
139    PricingTimeout,
140
141    // Gas service errors (314X)
142    /// Gas service internal error (3140)
143    GasInternal,
144    /// Gas service connection error (3141)
145    GasConnectionError,
146    /// Gas service timeout (3142)
147    GasTimeout,
148    /// Gas data unavailable (3143)
149    GasUnavailable,
150
151    // Odos Validation errors (4XXX)
152    /// Invalid request (4000)
153    InvalidRequest,
154
155    // General/Quote errors (40XX)
156    /// Invalid chain ID (4001)
157    InvalidChainId,
158    /// Invalid input tokens (4002)
159    InvalidInputTokens,
160    /// Invalid output tokens (4003)
161    InvalidOutputTokens,
162    /// Invalid user address (4004)
163    InvalidUserAddr,
164    /// Blocked user address (4005)
165    BlockedUserAddr,
166    /// Slippage tolerance too high (4006)
167    TooSlippery,
168    /// Same token for input and output (4007)
169    SameInputOutput,
170    /// Multiple zap outputs not supported (4008)
171    MultiZapOutput,
172    /// Invalid token count (4009)
173    InvalidTokenCount,
174    /// Invalid token address (4010)
175    InvalidTokenAddr,
176    /// Non-integer token amount (4011)
177    NonIntegerTokenAmount,
178    /// Negative token amount (4012)
179    NegativeTokenAmount,
180    /// Same tokens in input and output (4013)
181    SameInputOutputTokens,
182    /// Token is blacklisted (4014)
183    TokenBlacklisted,
184    /// Invalid token proportions (4015)
185    InvalidTokenProportions,
186    /// Token routing unavailable (4016)
187    TokenRoutingUnavailable,
188    /// Invalid referral code (4017)
189    InvalidReferralCode,
190    /// Invalid token amount (4018)
191    InvalidTokenAmount,
192    /// Non-string token amount (4019)
193    NonStringTokenAmount,
194
195    // Assembly errors (41XX)
196    /// Invalid assembly request (4100)
197    InvalidAssemblyRequest,
198    /// Invalid user address in assembly (4101)
199    InvalidAssemblyUserAddr,
200    /// Invalid receiver address (4102)
201    InvalidReceiverAddr,
202
203    // Swap errors (42XX)
204    /// Invalid swap request (4200)
205    InvalidSwapRequest,
206    /// User address required (4201)
207    UserAddrRequired,
208
209    // Odos Internal errors (5XXX)
210    /// Internal error (5000)
211    InternalError,
212    /// Swap unavailable (5001)
213    SwapUnavailable,
214    /// Price check failure (5002)
215    PriceCheckFailure,
216    /// Default gas calculation failure (5003)
217    DefaultGasFailure,
218
219    /// Unknown error code
220    Unknown(u16),
221}
222
223impl OdosErrorCode {
224    /// Get the numeric error code value
225    pub fn code(&self) -> u16 {
226        match self {
227            Self::ApiError => 1000,
228            Self::NoViablePath => 2000,
229            Self::AlgoValidationError => 2400,
230            Self::AlgoConnectionError => 2997,
231            Self::AlgoTimeout => 2998,
232            Self::AlgoInternal => 2999,
233            Self::InternalServiceError => 3000,
234            Self::ConfigInternal => 3100,
235            Self::ConfigConnectionError => 3101,
236            Self::ConfigTimeout => 3102,
237            Self::TxnAssemblyInternal => 3110,
238            Self::TxnAssemblyConnectionError => 3111,
239            Self::TxnAssemblyTimeout => 3112,
240            Self::ChainDataInternal => 3120,
241            Self::ChainDataConnectionError => 3121,
242            Self::ChainDataTimeout => 3122,
243            Self::PricingInternal => 3130,
244            Self::PricingConnectionError => 3131,
245            Self::PricingTimeout => 3132,
246            Self::GasInternal => 3140,
247            Self::GasConnectionError => 3141,
248            Self::GasTimeout => 3142,
249            Self::GasUnavailable => 3143,
250            Self::InvalidRequest => 4000,
251            Self::InvalidChainId => 4001,
252            Self::InvalidInputTokens => 4002,
253            Self::InvalidOutputTokens => 4003,
254            Self::InvalidUserAddr => 4004,
255            Self::BlockedUserAddr => 4005,
256            Self::TooSlippery => 4006,
257            Self::SameInputOutput => 4007,
258            Self::MultiZapOutput => 4008,
259            Self::InvalidTokenCount => 4009,
260            Self::InvalidTokenAddr => 4010,
261            Self::NonIntegerTokenAmount => 4011,
262            Self::NegativeTokenAmount => 4012,
263            Self::SameInputOutputTokens => 4013,
264            Self::TokenBlacklisted => 4014,
265            Self::InvalidTokenProportions => 4015,
266            Self::TokenRoutingUnavailable => 4016,
267            Self::InvalidReferralCode => 4017,
268            Self::InvalidTokenAmount => 4018,
269            Self::NonStringTokenAmount => 4019,
270            Self::InvalidAssemblyRequest => 4100,
271            Self::InvalidAssemblyUserAddr => 4101,
272            Self::InvalidReceiverAddr => 4102,
273            Self::InvalidSwapRequest => 4200,
274            Self::UserAddrRequired => 4201,
275            Self::InternalError => 5000,
276            Self::SwapUnavailable => 5001,
277            Self::PriceCheckFailure => 5002,
278            Self::DefaultGasFailure => 5003,
279            Self::Unknown(code) => *code,
280        }
281    }
282
283    /// Get the error category
284    pub fn category(&self) -> ErrorCategory {
285        let code = self.code();
286        match code {
287            1000..=1999 => ErrorCategory::General,
288            2000..=2999 => ErrorCategory::Algo,
289            3000..=3999 => ErrorCategory::InternalService,
290            4000..=4999 => ErrorCategory::Validation,
291            5000..=5999 => ErrorCategory::Internal,
292            _ => ErrorCategory::Unknown,
293        }
294    }
295
296    /// Check if this is a general API error
297    pub fn is_general_error(&self) -> bool {
298        matches!(self.category(), ErrorCategory::General)
299    }
300
301    /// Check if this is an algorithm/quote error
302    pub fn is_algo_error(&self) -> bool {
303        matches!(self.category(), ErrorCategory::Algo)
304    }
305
306    /// Check if this is an internal service error
307    pub fn is_internal_service_error(&self) -> bool {
308        matches!(self.category(), ErrorCategory::InternalService)
309    }
310
311    /// Check if this is a validation error
312    pub fn is_validation_error(&self) -> bool {
313        matches!(self.category(), ErrorCategory::Validation)
314    }
315
316    /// Check if this is an internal error
317    pub fn is_internal_error(&self) -> bool {
318        matches!(self.category(), ErrorCategory::Internal)
319    }
320
321    /// Check if this specific error is no viable path
322    pub fn is_no_viable_path(&self) -> bool {
323        matches!(self, Self::NoViablePath)
324    }
325
326    /// Check if this is an invalid chain ID error
327    pub fn is_invalid_chain_id(&self) -> bool {
328        matches!(self, Self::InvalidChainId)
329    }
330
331    /// Check if this is a blocked user address error
332    pub fn is_blocked_user(&self) -> bool {
333        matches!(self, Self::BlockedUserAddr)
334    }
335
336    /// Check if this is a timeout error (any service)
337    pub fn is_timeout(&self) -> bool {
338        matches!(
339            self,
340            Self::AlgoTimeout
341                | Self::ConfigTimeout
342                | Self::TxnAssemblyTimeout
343                | Self::ChainDataTimeout
344                | Self::PricingTimeout
345                | Self::GasTimeout
346        )
347    }
348
349    /// Check if this is a connection error (any service)
350    pub fn is_connection_error(&self) -> bool {
351        matches!(
352            self,
353            Self::AlgoConnectionError
354                | Self::ConfigConnectionError
355                | Self::TxnAssemblyConnectionError
356                | Self::ChainDataConnectionError
357                | Self::PricingConnectionError
358                | Self::GasConnectionError
359        )
360    }
361
362    /// Check if this error indicates the request should be retried
363    ///
364    /// Retryable errors include:
365    /// - Timeouts (algo, config, assembly, chain data, pricing, gas)
366    /// - Connection errors (all services)
367    /// - Internal errors (config, assembly, chain data, pricing, gas, services, general)
368    /// - Gas unavailable
369    ///
370    /// `AlgoInternal` (2999) is **not** classified as retryable: production
371    /// evidence shows it reflects routing-algorithm state for marginal-liquidity
372    /// tokens that does not stabilise within request timescales. Consumers who
373    /// want in-call retries for 2999 can opt back in by replacing the default
374    /// policy with
375    /// [`RetryPredicate::Replace`](crate::RetryPredicate::Replace).
376    /// Conversely,
377    /// [`RetryPredicate::DefaultExcept`](crate::RetryPredicate::DefaultExcept)
378    /// can be used to veto retries for any *currently retryable* code without
379    /// reimplementing the default decision tree.
380    pub fn is_retryable(&self) -> bool {
381        self.is_timeout()
382            || self.is_connection_error()
383            || matches!(
384                self,
385                Self::ConfigInternal
386                    | Self::TxnAssemblyInternal
387                    | Self::ChainDataInternal
388                    | Self::PricingInternal
389                    | Self::GasInternal
390                    | Self::GasUnavailable
391                    | Self::InternalServiceError
392                    | Self::InternalError
393            )
394    }
395
396    /// Check if this error indicates the token cannot be routed
397    ///
398    /// This is NOT an error condition - it's Odos correctly responding that
399    /// the token cannot be swapped. Common reasons include:
400    /// - Token is not supported by Odos (TokenRoutingUnavailable)
401    /// - Token is blacklisted (TokenBlacklisted)
402    /// - Token is not recognized (InvalidInputTokens/InvalidOutputTokens)
403    /// - No liquidity path exists (NoViablePath)
404    ///
405    /// These should be tracked separately from actual errors for metrics purposes.
406    pub fn is_unroutable_token(&self) -> bool {
407        matches!(
408            self,
409            Self::TokenRoutingUnavailable
410                | Self::TokenBlacklisted
411                | Self::InvalidInputTokens
412                | Self::InvalidOutputTokens
413                | Self::NoViablePath
414        )
415    }
416}
417
418impl From<u16> for OdosErrorCode {
419    fn from(code: u16) -> Self {
420        match code {
421            1000 => Self::ApiError,
422            2000 => Self::NoViablePath,
423            2400 => Self::AlgoValidationError,
424            2997 => Self::AlgoConnectionError,
425            2998 => Self::AlgoTimeout,
426            2999 => Self::AlgoInternal,
427            3000 => Self::InternalServiceError,
428            3100 => Self::ConfigInternal,
429            3101 => Self::ConfigConnectionError,
430            3102 => Self::ConfigTimeout,
431            3110 => Self::TxnAssemblyInternal,
432            3111 => Self::TxnAssemblyConnectionError,
433            3112 => Self::TxnAssemblyTimeout,
434            3120 => Self::ChainDataInternal,
435            3121 => Self::ChainDataConnectionError,
436            3122 => Self::ChainDataTimeout,
437            3130 => Self::PricingInternal,
438            3131 => Self::PricingConnectionError,
439            3132 => Self::PricingTimeout,
440            3140 => Self::GasInternal,
441            3141 => Self::GasConnectionError,
442            3142 => Self::GasTimeout,
443            3143 => Self::GasUnavailable,
444            4000 => Self::InvalidRequest,
445            4001 => Self::InvalidChainId,
446            4002 => Self::InvalidInputTokens,
447            4003 => Self::InvalidOutputTokens,
448            4004 => Self::InvalidUserAddr,
449            4005 => Self::BlockedUserAddr,
450            4006 => Self::TooSlippery,
451            4007 => Self::SameInputOutput,
452            4008 => Self::MultiZapOutput,
453            4009 => Self::InvalidTokenCount,
454            4010 => Self::InvalidTokenAddr,
455            4011 => Self::NonIntegerTokenAmount,
456            4012 => Self::NegativeTokenAmount,
457            4013 => Self::SameInputOutputTokens,
458            4014 => Self::TokenBlacklisted,
459            4015 => Self::InvalidTokenProportions,
460            4016 => Self::TokenRoutingUnavailable,
461            4017 => Self::InvalidReferralCode,
462            4018 => Self::InvalidTokenAmount,
463            4019 => Self::NonStringTokenAmount,
464            4100 => Self::InvalidAssemblyRequest,
465            4101 => Self::InvalidAssemblyUserAddr,
466            4102 => Self::InvalidReceiverAddr,
467            4200 => Self::InvalidSwapRequest,
468            4201 => Self::UserAddrRequired,
469            5000 => Self::InternalError,
470            5001 => Self::SwapUnavailable,
471            5002 => Self::PriceCheckFailure,
472            5003 => Self::DefaultGasFailure,
473            _ => Self::Unknown(code),
474        }
475    }
476}
477
478impl From<OdosErrorCode> for u16 {
479    fn from(error_code: OdosErrorCode) -> Self {
480        error_code.code()
481    }
482}
483
484impl fmt::Display for OdosErrorCode {
485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486        match self {
487            Self::ApiError => write!(f, "1000 (API_ERROR)"),
488            Self::NoViablePath => write!(f, "2000 (NO_VIABLE_PATH)"),
489            Self::AlgoValidationError => write!(f, "2400 (ALGO_VALIDATION_ERR)"),
490            Self::AlgoConnectionError => write!(f, "2997 (ALGO_CONN_ERR)"),
491            Self::AlgoTimeout => write!(f, "2998 (ALGO_TIMEOUT)"),
492            Self::AlgoInternal => write!(f, "2999 (ALGO_INTERNAL)"),
493            Self::InternalServiceError => write!(f, "3000 (INTERNAL_SERVICE_ERROR)"),
494            Self::ConfigInternal => write!(f, "3100 (CONFIG_INTERNAL)"),
495            Self::ConfigConnectionError => write!(f, "3101 (CONFIG_CONN_ERR)"),
496            Self::ConfigTimeout => write!(f, "3102 (CONFIG_TIMEOUT)"),
497            Self::TxnAssemblyInternal => write!(f, "3110 (TXN_ASSEMBLY_INTERNAL)"),
498            Self::TxnAssemblyConnectionError => write!(f, "3111 (TXN_ASSEMBLY_CONN_ERR)"),
499            Self::TxnAssemblyTimeout => write!(f, "3112 (TXN_ASSEMBLY_TIMEOUT)"),
500            Self::ChainDataInternal => write!(f, "3120 (CHAIN_DATA_INTERNAL)"),
501            Self::ChainDataConnectionError => write!(f, "3121 (CHAIN_DATA_CONN_ERR)"),
502            Self::ChainDataTimeout => write!(f, "3122 (CHAIN_DATA_TIMEOUT)"),
503            Self::PricingInternal => write!(f, "3130 (PRICING_INTERNAL)"),
504            Self::PricingConnectionError => write!(f, "3131 (PRICING_CONN_ERR)"),
505            Self::PricingTimeout => write!(f, "3132 (PRICING_TIMEOUT)"),
506            Self::GasInternal => write!(f, "3140 (GAS_INTERNAL)"),
507            Self::GasConnectionError => write!(f, "3141 (GAS_CONN_ERR)"),
508            Self::GasTimeout => write!(f, "3142 (GAS_TIMEOUT)"),
509            Self::GasUnavailable => write!(f, "3143 (GAS_UNAVAILABLE)"),
510            Self::InvalidRequest => write!(f, "4000 (INVALID_REQUEST)"),
511            Self::InvalidChainId => write!(f, "4001 (INVALID_CHAIN_ID)"),
512            Self::InvalidInputTokens => write!(f, "4002 (INVALID_INPUT_TOKENS)"),
513            Self::InvalidOutputTokens => write!(f, "4003 (INVALID_OUTPUT_TOKENS)"),
514            Self::InvalidUserAddr => write!(f, "4004 (INVALID_USER_ADDR)"),
515            Self::BlockedUserAddr => write!(f, "4005 (BLOCKED_USER_ADDR)"),
516            Self::TooSlippery => write!(f, "4006 (TOO_SLIPPERY)"),
517            Self::SameInputOutput => write!(f, "4007 (SAME_INPUT_OUTPUT)"),
518            Self::MultiZapOutput => write!(f, "4008 (MULTI_ZAP_OUTPUT)"),
519            Self::InvalidTokenCount => write!(f, "4009 (INVALID_TOKEN_COUNT)"),
520            Self::InvalidTokenAddr => write!(f, "4010 (INVALID_TOKEN_ADDR)"),
521            Self::NonIntegerTokenAmount => write!(f, "4011 (NON_INTEGER_TOKEN_AMOUNT)"),
522            Self::NegativeTokenAmount => write!(f, "4012 (NEGATIVE_TOKEN_AMOUNT)"),
523            Self::SameInputOutputTokens => write!(f, "4013 (SAME_INPUT_OUTPUT_TOKENS)"),
524            Self::TokenBlacklisted => write!(f, "4014 (TOKEN_BLACKLISTED)"),
525            Self::InvalidTokenProportions => write!(f, "4015 (INVALID_TOKEN_PROPORTIONS)"),
526            Self::TokenRoutingUnavailable => write!(f, "4016 (TOKEN_ROUTING_UNAVAILABLE)"),
527            Self::InvalidReferralCode => write!(f, "4017 (INVALID_REFERRAL_CODE)"),
528            Self::InvalidTokenAmount => write!(f, "4018 (INVALID_TOKEN_AMOUNT)"),
529            Self::NonStringTokenAmount => write!(f, "4019 (NON_STRING_TOKEN_AMOUNT)"),
530            Self::InvalidAssemblyRequest => write!(f, "4100 (INVALID_ASSEMBLY_REQUEST)"),
531            Self::InvalidAssemblyUserAddr => write!(f, "4101 (INVALID_USER_ADDR)"),
532            Self::InvalidReceiverAddr => write!(f, "4102 (INVALID_RECEIVER_ADDR)"),
533            Self::InvalidSwapRequest => write!(f, "4200 (INVALID_SWAP_REQUEST)"),
534            Self::UserAddrRequired => write!(f, "4201 (USER_ADDR_REQ)"),
535            Self::InternalError => write!(f, "5000 (INTERNAL_ERROR)"),
536            Self::SwapUnavailable => write!(f, "5001 (SWAP_UNAVAILABLE)"),
537            Self::PriceCheckFailure => write!(f, "5002 (PRICE_CHECK_FAILURE)"),
538            Self::DefaultGasFailure => write!(f, "5003 (DEFAULT_GAS_FAILURE)"),
539            Self::Unknown(code) => write!(f, "{code} (UNKNOWN)"),
540        }
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_trace_id_creation() {
550        let uuid = Uuid::parse_str("10becdc8-a021-4491-8201-a17b657204e0").unwrap();
551        let trace_id = TraceId::new(uuid);
552        assert_eq!(trace_id.as_uuid(), uuid);
553        assert_eq!(trace_id.to_string(), "10becdc8-a021-4491-8201-a17b657204e0");
554    }
555
556    #[test]
557    fn test_error_code_from_u16() {
558        assert_eq!(OdosErrorCode::from(2999), OdosErrorCode::AlgoInternal);
559        assert_eq!(OdosErrorCode::from(4001), OdosErrorCode::InvalidChainId);
560        assert_eq!(OdosErrorCode::from(3142), OdosErrorCode::GasTimeout);
561        assert_eq!(OdosErrorCode::from(9999), OdosErrorCode::Unknown(9999));
562    }
563
564    #[test]
565    fn test_error_code_to_u16() {
566        assert_eq!(OdosErrorCode::AlgoInternal.code(), 2999);
567        assert_eq!(OdosErrorCode::InvalidChainId.code(), 4001);
568        assert_eq!(OdosErrorCode::Unknown(9999).code(), 9999);
569    }
570
571    #[test]
572    fn test_error_categories() {
573        assert!(OdosErrorCode::ApiError.is_general_error());
574        assert!(OdosErrorCode::NoViablePath.is_algo_error());
575        assert!(OdosErrorCode::ConfigInternal.is_internal_service_error());
576        assert!(OdosErrorCode::InvalidChainId.is_validation_error());
577        assert!(OdosErrorCode::InternalError.is_internal_error());
578    }
579
580    #[test]
581    fn test_specific_error_checks() {
582        assert!(OdosErrorCode::NoViablePath.is_no_viable_path());
583        assert!(OdosErrorCode::InvalidChainId.is_invalid_chain_id());
584        assert!(OdosErrorCode::BlockedUserAddr.is_blocked_user());
585
586        assert!(!OdosErrorCode::ApiError.is_no_viable_path());
587        assert!(!OdosErrorCode::AlgoInternal.is_invalid_chain_id());
588    }
589
590    #[test]
591    fn test_timeout_detection() {
592        assert!(OdosErrorCode::AlgoTimeout.is_timeout());
593        assert!(OdosErrorCode::ConfigTimeout.is_timeout());
594        assert!(OdosErrorCode::TxnAssemblyTimeout.is_timeout());
595        assert!(OdosErrorCode::ChainDataTimeout.is_timeout());
596        assert!(OdosErrorCode::PricingTimeout.is_timeout());
597        assert!(OdosErrorCode::GasTimeout.is_timeout());
598
599        assert!(!OdosErrorCode::AlgoInternal.is_timeout());
600        assert!(!OdosErrorCode::InvalidChainId.is_timeout());
601    }
602
603    #[test]
604    fn test_connection_error_detection() {
605        assert!(OdosErrorCode::AlgoConnectionError.is_connection_error());
606        assert!(OdosErrorCode::ConfigConnectionError.is_connection_error());
607        assert!(OdosErrorCode::GasConnectionError.is_connection_error());
608
609        assert!(!OdosErrorCode::AlgoInternal.is_connection_error());
610        assert!(!OdosErrorCode::InvalidChainId.is_connection_error());
611    }
612
613    #[test]
614    fn test_retryability() {
615        // Timeouts are retryable
616        assert!(OdosErrorCode::AlgoTimeout.is_retryable());
617        assert!(OdosErrorCode::GasTimeout.is_retryable());
618
619        // Connection errors are retryable
620        assert!(OdosErrorCode::AlgoConnectionError.is_retryable());
621        assert!(OdosErrorCode::PricingConnectionError.is_retryable());
622
623        // Internal errors are retryable
624        assert!(OdosErrorCode::InternalServiceError.is_retryable());
625        assert!(OdosErrorCode::GasUnavailable.is_retryable());
626
627        // AlgoInternal is not retryable: routing-algorithm state for
628        // marginal-liquidity tokens does not stabilise within request timescales.
629        assert!(!OdosErrorCode::AlgoInternal.is_retryable());
630
631        // Validation errors are not retryable
632        assert!(!OdosErrorCode::InvalidChainId.is_retryable());
633        assert!(!OdosErrorCode::BlockedUserAddr.is_retryable());
634        assert!(!OdosErrorCode::InvalidTokenAmount.is_retryable());
635
636        // No viable path is not retryable
637        assert!(!OdosErrorCode::NoViablePath.is_retryable());
638    }
639
640    #[test]
641    fn test_display_format() {
642        assert_eq!(
643            OdosErrorCode::AlgoInternal.to_string(),
644            "2999 (ALGO_INTERNAL)"
645        );
646        assert_eq!(
647            OdosErrorCode::InvalidChainId.to_string(),
648            "4001 (INVALID_CHAIN_ID)"
649        );
650        assert_eq!(OdosErrorCode::Unknown(9999).to_string(), "9999 (UNKNOWN)");
651    }
652
653    #[test]
654    fn test_unroutable_token_detection() {
655        // These indicate the token cannot be routed (expected behavior, not errors)
656        assert!(OdosErrorCode::NoViablePath.is_unroutable_token());
657        assert!(OdosErrorCode::TokenRoutingUnavailable.is_unroutable_token());
658        assert!(OdosErrorCode::TokenBlacklisted.is_unroutable_token());
659        assert!(OdosErrorCode::InvalidInputTokens.is_unroutable_token());
660        assert!(OdosErrorCode::InvalidOutputTokens.is_unroutable_token());
661
662        // These are NOT unroutable token indicators
663        assert!(!OdosErrorCode::AlgoTimeout.is_unroutable_token());
664        assert!(!OdosErrorCode::InternalError.is_unroutable_token());
665        assert!(!OdosErrorCode::InvalidChainId.is_unroutable_token());
666        assert!(!OdosErrorCode::BlockedUserAddr.is_unroutable_token());
667    }
668}