odos_sdk/
error_code.rs

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