riglr_solana_tools/
error.rs

1//! Error types for riglr-solana-tools.
2
3use riglr_core::error::ToolError;
4use riglr_core::SignerError;
5use solana_client::client_error::{ClientError, ClientErrorKind};
6use solana_client::rpc_request::RpcError;
7use thiserror::Error;
8
9/// Main error type for Solana tool operations.
10#[derive(Error, Debug)]
11#[allow(clippy::result_large_err)]
12#[allow(clippy::large_enum_variant)]
13pub enum SolanaToolError {
14    /// Core tool error - passthrough
15    #[error("Core tool error: {0}")]
16    ToolError(#[from] ToolError),
17
18    /// Signer context error - configuration issue
19    #[error("Signer context error: {0}")]
20    SignerError(#[from] SignerError),
21
22    /// RPC client error - network issues are typically retriable
23    /// Note: May be rate-limited if message contains "429" or "rate limit"
24    #[error("RPC error: {0}")]
25    Rpc(String),
26
27    /// Solana client error - classification depends on inner error
28    #[error("Solana client error: {0}")]
29    SolanaClient(Box<ClientError>),
30
31    /// Invalid address format - user input error
32    #[error("Invalid address: {0}")]
33    InvalidAddress(String),
34
35    /// Invalid key format - user input error
36    #[error("Invalid key: {0}")]
37    InvalidKey(String),
38
39    /// Invalid signature format - user input error  
40    #[error("Invalid signature: {0}")]
41    InvalidSignature(String),
42
43    /// Transaction failed - may be retriable depending on message
44    #[error("Transaction error: {0}")]
45    Transaction(String),
46
47    /// Insufficient funds for operation - permanent error
48    #[error("Insufficient funds for operation")]
49    InsufficientFunds,
50
51    /// Invalid token mint - user input error
52    #[error("Invalid token mint: {0}")]
53    InvalidTokenMint(String),
54
55    /// Serialization error - data corruption/format issue
56    #[error("Serialization error: {0}")]
57    Serialization(#[from] serde_json::Error),
58
59    /// HTTP request error - network issues are typically retriable
60    /// Note: May be rate-limited if status is 429
61    #[error("HTTP error: {0}")]
62    Http(#[from] reqwest::Error),
63
64    /// Core riglr error - typically retriable
65    #[error("Core error: {0}")]
66    Core(#[from] riglr_core::CoreError),
67
68    /// Generic error - default to retriable
69    #[error("Solana tool error: {0}")]
70    Generic(String),
71}
72
73/// Result type alias for Solana tool operations.
74pub type Result<T> = std::result::Result<T, SolanaToolError>;
75
76/// Internal classification of errors for conversion to ToolError
77#[derive(Debug, PartialEq)]
78enum ErrorClassification {
79    /// Permanent errors that should not be retried
80    Permanent,
81    /// Errors that can be retried
82    Retriable,
83    /// Rate-limited errors with optional retry delay
84    RateLimited { delay: Option<std::time::Duration> },
85    /// Invalid input errors
86    InvalidInput,
87    /// Signer context errors (special case)
88    SignerContext,
89    /// Pass through an existing ToolError without re-wrapping
90    ToolErrorPassthrough(ToolError),
91}
92
93impl SolanaToolError {
94    /// Classify this error for conversion to ToolError
95    ///
96    /// This method encapsulates all the complex classification logic,
97    /// including dynamic checks based on message content and error types.
98    fn classify(&self) -> ErrorClassification {
99        match self {
100            // Passthrough ToolError without re-wrapping
101            SolanaToolError::ToolError(e) => ErrorClassification::ToolErrorPassthrough(e.clone()),
102
103            // Signer errors are configuration issues
104            SolanaToolError::SignerError(_) => ErrorClassification::SignerContext,
105
106            // Input validation errors
107            SolanaToolError::InvalidAddress(_)
108            | SolanaToolError::InvalidKey(_)
109            | SolanaToolError::InvalidSignature(_)
110            | SolanaToolError::InvalidTokenMint(_) => ErrorClassification::InvalidInput,
111
112            // Insufficient funds is always permanent
113            SolanaToolError::InsufficientFunds => ErrorClassification::Permanent,
114
115            // Serialization errors are permanent
116            SolanaToolError::Serialization(_) => ErrorClassification::Permanent,
117
118            // RPC errors - check for rate limiting indicators
119            SolanaToolError::Rpc(msg) => {
120                if msg.contains("429")
121                    || msg.contains("rate limit")
122                    || msg.contains("too many requests")
123                {
124                    ErrorClassification::RateLimited {
125                        delay: Some(std::time::Duration::from_secs(1)),
126                    }
127                } else {
128                    ErrorClassification::Retriable
129                }
130            }
131
132            // HTTP errors - check status code for rate limiting
133            SolanaToolError::Http(http_err) => {
134                if http_err.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS) {
135                    ErrorClassification::RateLimited {
136                        delay: Some(std::time::Duration::from_secs(1)),
137                    }
138                } else if http_err.is_timeout() || http_err.is_connect() {
139                    ErrorClassification::Retriable
140                } else if matches!(
141                    http_err.status(),
142                    Some(
143                        reqwest::StatusCode::BAD_REQUEST
144                            | reqwest::StatusCode::UNAUTHORIZED
145                            | reqwest::StatusCode::FORBIDDEN
146                    )
147                ) {
148                    ErrorClassification::Permanent
149                } else {
150                    ErrorClassification::Retriable
151                }
152            }
153
154            // Solana client errors - use the classify_transaction_error helper
155            SolanaToolError::SolanaClient(client_err) => {
156                let error_type = classify_transaction_error(client_err);
157                match error_type {
158                    TransactionErrorType::RateLimited(_) => ErrorClassification::RateLimited {
159                        delay: Some(std::time::Duration::from_secs(1)),
160                    },
161                    TransactionErrorType::Retryable(_) => ErrorClassification::Retriable,
162                    TransactionErrorType::Permanent(PermanentError::InsufficientFunds) => {
163                        ErrorClassification::Permanent
164                    }
165                    TransactionErrorType::Permanent(_) => ErrorClassification::Permanent,
166                    TransactionErrorType::Unknown(_) => ErrorClassification::Retriable,
167                }
168            }
169
170            // Transaction errors - check message for patterns
171            SolanaToolError::Transaction(msg) => {
172                if msg.contains("insufficient") || msg.contains("InsufficientFunds") {
173                    ErrorClassification::Permanent
174                } else if msg.contains("rate limit") || msg.contains("429") {
175                    ErrorClassification::RateLimited {
176                        delay: Some(std::time::Duration::from_secs(1)),
177                    }
178                } else {
179                    ErrorClassification::Retriable
180                }
181            }
182
183            // Core errors are typically retriable
184            SolanaToolError::Core(_) => ErrorClassification::Retriable,
185
186            // Generic errors default to retriable
187            SolanaToolError::Generic(_) => ErrorClassification::Retriable,
188        }
189    }
190
191    /// Check if this error is retriable.
192    /// Note: The IntoToolError macro generates a basic From implementation, but for complex
193    /// cases that need runtime logic (like checking message content), we keep this method
194    /// for backward compatibility and to support custom logic.
195    pub fn is_retriable(&self) -> bool {
196        match self {
197            // Core errors inherit their retriable nature
198            SolanaToolError::ToolError(tool_err) => tool_err.is_retriable(),
199            SolanaToolError::SignerError(_) => false, // Generally configuration issues
200            SolanaToolError::Core(_) => true,         // Core errors are typically retriable
201
202            // RPC and HTTP errors are often retriable
203            SolanaToolError::Rpc(_) => true,
204            SolanaToolError::Http(ref http_err) => !matches!(
205                http_err.status(),
206                Some(
207                    reqwest::StatusCode::BAD_REQUEST
208                        | reqwest::StatusCode::UNAUTHORIZED
209                        | reqwest::StatusCode::FORBIDDEN
210                )
211            ),
212
213            // Client errors need classification
214            SolanaToolError::SolanaClient(ref client_err) => {
215                let error_type = classify_transaction_error(client_err);
216                error_type.is_retryable()
217            }
218
219            // Address/key validation errors are permanent
220            SolanaToolError::InvalidAddress(_) => false,
221            SolanaToolError::InvalidKey(_) => false,
222            SolanaToolError::InvalidSignature(_) => false,
223            SolanaToolError::InvalidTokenMint(_) => false,
224
225            // Insufficient funds is permanent
226            SolanaToolError::InsufficientFunds => false,
227
228            // Transaction errors depend on content
229            SolanaToolError::Transaction(msg) => {
230                !(msg.contains("insufficient funds") || msg.contains("invalid"))
231            }
232
233            // Serialization errors are permanent
234            SolanaToolError::Serialization(_) => false,
235
236            // Generic errors default to retriable
237            SolanaToolError::Generic(_) => true,
238        }
239    }
240
241    /// Check if this error is rate-limited.
242    pub fn is_rate_limited(&self) -> bool {
243        match self {
244            SolanaToolError::Rpc(msg) => {
245                msg.contains("429")
246                    || msg.contains("rate limit")
247                    || msg.contains("too many requests")
248            }
249            SolanaToolError::Http(ref http_err) => {
250                http_err.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS)
251            }
252            SolanaToolError::SolanaClient(ref client_err) => {
253                let error_type = classify_transaction_error(client_err);
254                error_type.is_rate_limited()
255            }
256            _ => false,
257        }
258    }
259
260    /// Get appropriate retry delay for rate-limited errors.
261    pub fn retry_delay(&self) -> Option<std::time::Duration> {
262        if self.is_rate_limited() {
263            Some(std::time::Duration::from_secs(1))
264        } else if self.is_retriable() {
265            Some(std::time::Duration::from_millis(500))
266        } else {
267            None
268        }
269    }
270}
271
272/// Structured classification of transaction errors for intelligent retry logic
273#[derive(Debug, Clone, PartialEq)]
274pub enum TransactionErrorType {
275    /// Errors that can be retried with appropriate backoff
276    Retryable(RetryableError),
277    /// Errors that represent permanent failures and should not be retried
278    Permanent(PermanentError),
279    /// Rate limiting errors that require special handling with delays
280    RateLimited(RateLimitError),
281    /// Unknown error types that don't fit other categories
282    Unknown(String),
283}
284
285/// Errors that can be retried with appropriate backoff
286#[derive(Debug, Clone, PartialEq)]
287pub enum RetryableError {
288    /// Network connectivity issues
289    NetworkConnectivity,
290    /// RPC service temporary unavailability
291    TemporaryRpcFailure,
292    /// Blockchain congestion
293    NetworkCongestion,
294    /// Transaction pool full
295    TransactionPoolFull,
296}
297
298/// Permanent errors that should not be retried
299#[derive(Debug, Clone, PartialEq)]
300pub enum PermanentError {
301    /// Insufficient funds for transaction
302    InsufficientFunds,
303    /// Invalid signature provided
304    InvalidSignature,
305    /// Invalid account referenced
306    InvalidAccount,
307    /// Program execution error
308    InstructionError,
309    /// Invalid transaction structure
310    InvalidTransaction,
311    /// Duplicate transaction
312    DuplicateTransaction,
313}
314
315/// Rate limiting errors with special handling
316#[derive(Debug, Clone, PartialEq)]
317pub enum RateLimitError {
318    /// Standard RPC rate limiting
319    RpcRateLimit,
320    /// Too many requests error
321    TooManyRequests,
322}
323
324impl TransactionErrorType {
325    /// Check if this error type is retryable
326    pub fn is_retryable(&self) -> bool {
327        matches!(
328            self,
329            TransactionErrorType::Retryable(_) | TransactionErrorType::RateLimited(_)
330        )
331    }
332
333    /// Check if this is a rate limiting error (special case of retryable)
334    pub fn is_rate_limited(&self) -> bool {
335        matches!(self, TransactionErrorType::RateLimited(_))
336    }
337}
338
339/// Classify a Solana ClientError into a structured transaction error type
340///
341/// This function provides intelligent error classification based on the actual
342/// error types from the Solana client, rather than brittle string matching.
343/// It handles the most common error scenarios and provides appropriate
344/// retry guidance.
345pub fn classify_transaction_error(error: &ClientError) -> TransactionErrorType {
346    match &*error.kind {
347        ClientErrorKind::RpcError(rpc_error) => classify_rpc_error(rpc_error),
348        ClientErrorKind::SerdeJson(_) => {
349            TransactionErrorType::Permanent(PermanentError::InvalidTransaction)
350        }
351        ClientErrorKind::Io(_) => {
352            TransactionErrorType::Retryable(RetryableError::NetworkConnectivity)
353        }
354        ClientErrorKind::Reqwest(reqwest_error) => {
355            if reqwest_error.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS) {
356                TransactionErrorType::RateLimited(RateLimitError::TooManyRequests)
357            } else if reqwest_error.is_timeout() || reqwest_error.is_connect() {
358                TransactionErrorType::Retryable(RetryableError::NetworkConnectivity)
359            } else {
360                TransactionErrorType::Unknown(error.to_string())
361            }
362        }
363        ClientErrorKind::Custom(msg) => {
364            // Handle custom error messages with more sophisticated logic than string matching
365            if msg.contains("InsufficientFundsForRent") || msg.contains("insufficient funds") {
366                TransactionErrorType::Permanent(PermanentError::InsufficientFunds)
367            } else if msg.contains("InvalidAccountIndex") {
368                TransactionErrorType::Permanent(PermanentError::InvalidAccount)
369            } else if msg.contains("InvalidSignature") {
370                TransactionErrorType::Permanent(PermanentError::InvalidSignature)
371            } else if msg.contains("DuplicateSignature") {
372                TransactionErrorType::Permanent(PermanentError::DuplicateTransaction)
373            } else {
374                TransactionErrorType::Unknown(error.to_string())
375            }
376        }
377        _ => TransactionErrorType::Unknown(error.to_string()),
378    }
379}
380
381/// Classify RPC-specific errors
382fn classify_rpc_error(rpc_error: &RpcError) -> TransactionErrorType {
383    use solana_client::rpc_request::RpcError::*;
384
385    match rpc_error {
386        RpcRequestError(msg) => {
387            if msg.contains("rate limit")
388                || msg.contains("429")
389                || msg.contains("too many requests")
390            {
391                TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit)
392            } else {
393                TransactionErrorType::Retryable(RetryableError::TemporaryRpcFailure)
394            }
395        }
396        RpcResponseError { code, message, .. } => {
397            // Standard JSON-RPC error codes
398            match *code {
399                429 => TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit),
400                -32603 => TransactionErrorType::Retryable(RetryableError::TemporaryRpcFailure), // Internal error
401                -32002 => TransactionErrorType::Retryable(RetryableError::NetworkCongestion), // Transaction pool full
402                -32005 => TransactionErrorType::Retryable(RetryableError::NetworkCongestion), // Node behind
403                _ => {
404                    // Analyze message for specific transaction errors
405                    if message.contains("InsufficientFundsForRent") {
406                        TransactionErrorType::Permanent(PermanentError::InsufficientFunds)
407                    } else if message.contains("invalid") && message.contains("signature") {
408                        TransactionErrorType::Permanent(PermanentError::InvalidSignature)
409                    } else if message.contains("invalid") && message.contains("account") {
410                        TransactionErrorType::Permanent(PermanentError::InvalidAccount)
411                    } else if message.contains("Instruction") && message.contains("error") {
412                        TransactionErrorType::Permanent(PermanentError::InstructionError)
413                    } else {
414                        TransactionErrorType::Unknown(format!("RPC Error {}: {}", code, message))
415                    }
416                }
417            }
418        }
419        ParseError(_msg) => TransactionErrorType::Permanent(PermanentError::InvalidTransaction),
420        ForUser(msg) => TransactionErrorType::Unknown(msg.clone()),
421    }
422}
423
424// Implement From conversion to riglr_core::ToolError for proper error handling
425// This implementation preserves the source error for downcasting and uses
426// the classification methods to determine retriability.
427impl From<SolanaToolError> for ToolError {
428    fn from(err: SolanaToolError) -> Self {
429        // Use the classify method to determine error handling
430        match err.classify() {
431            ErrorClassification::ToolErrorPassthrough(tool_err) => tool_err,
432
433            ErrorClassification::Permanent => {
434                ToolError::permanent_with_source(err, "Solana operation failed")
435            }
436
437            ErrorClassification::Retriable => {
438                ToolError::retriable_with_source(err, "Solana operation can be retried")
439            }
440
441            ErrorClassification::RateLimited { delay } => {
442                ToolError::rate_limited_with_source(err, "Solana rate limit exceeded", delay)
443            }
444
445            ErrorClassification::InvalidInput => {
446                ToolError::invalid_input_with_source(err, "Invalid input for Solana operation")
447            }
448
449            ErrorClassification::SignerContext => ToolError::SignerContext(err.to_string()),
450        }
451    }
452}
453
454impl From<ClientError> for SolanaToolError {
455    fn from(error: ClientError) -> Self {
456        SolanaToolError::SolanaClient(Box::new(error))
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use solana_client::client_error::{ClientError, ClientErrorKind};
464    use solana_client::rpc_request::RpcError;
465
466    // Test the classify method for all SolanaToolError variants
467    #[test]
468    fn test_classify_tool_error_passthrough() {
469        let tool_err = ToolError::permanent_string("test error");
470        let solana_err = SolanaToolError::ToolError(tool_err.clone());
471
472        match solana_err.classify() {
473            ErrorClassification::ToolErrorPassthrough(e) => {
474                assert_eq!(e.to_string(), tool_err.to_string());
475            }
476            _ => panic!("Expected ToolErrorPassthrough"),
477        }
478    }
479
480    #[test]
481    fn test_classify_signer_error() {
482        let signer_err = SignerError::NoSignerContext;
483        let solana_err = SolanaToolError::SignerError(signer_err);
484
485        assert_eq!(solana_err.classify(), ErrorClassification::SignerContext);
486    }
487
488    #[test]
489    fn test_classify_invalid_input_errors() {
490        let test_cases = vec![
491            SolanaToolError::InvalidAddress("bad address".to_string()),
492            SolanaToolError::InvalidKey("bad key".to_string()),
493            SolanaToolError::InvalidSignature("bad sig".to_string()),
494            SolanaToolError::InvalidTokenMint("bad mint".to_string()),
495        ];
496
497        for error in test_cases {
498            assert_eq!(
499                error.classify(),
500                ErrorClassification::InvalidInput,
501                "Failed for error: {:?}",
502                error
503            );
504        }
505    }
506
507    #[test]
508    fn test_classify_permanent_errors() {
509        let test_cases = vec![
510            SolanaToolError::InsufficientFunds,
511            SolanaToolError::Serialization(serde_json::from_str::<String>("invalid").unwrap_err()),
512        ];
513
514        for error in test_cases {
515            assert_eq!(
516                error.classify(),
517                ErrorClassification::Permanent,
518                "Failed for error: {:?}",
519                error
520            );
521        }
522    }
523
524    #[test]
525    fn test_classify_rpc_error_rate_limited() {
526        let test_cases = vec![
527            SolanaToolError::Rpc("Error 429: Too many requests".to_string()),
528            SolanaToolError::Rpc("rate limit exceeded".to_string()),
529            SolanaToolError::Rpc("too many requests".to_string()),
530        ];
531
532        for error in test_cases {
533            match error.classify() {
534                ErrorClassification::RateLimited { delay } => {
535                    assert!(delay.is_some(), "Expected delay for rate limited error");
536                }
537                _ => panic!("Expected RateLimited classification for: {:?}", error),
538            }
539        }
540    }
541
542    #[test]
543    fn test_classify_rpc_error_retriable() {
544        let error = SolanaToolError::Rpc("Connection timeout".to_string());
545        assert_eq!(error.classify(), ErrorClassification::Retriable);
546    }
547
548    #[test]
549    fn test_classify_transaction_error() {
550        // Test insufficient funds detection
551        let insufficient =
552            SolanaToolError::Transaction("insufficient funds for transaction".to_string());
553        assert_eq!(insufficient.classify(), ErrorClassification::Permanent);
554
555        // Test rate limit detection
556        let rate_limited = SolanaToolError::Transaction("rate limit exceeded".to_string());
557        match rate_limited.classify() {
558            ErrorClassification::RateLimited { delay } => {
559                assert!(delay.is_some());
560            }
561            _ => panic!("Expected RateLimited classification"),
562        }
563
564        // Test retriable transaction error
565        let retriable = SolanaToolError::Transaction("network congestion".to_string());
566        assert_eq!(retriable.classify(), ErrorClassification::Retriable);
567    }
568
569    #[test]
570    fn test_classify_core_and_generic_errors() {
571        let core_err = SolanaToolError::Core(riglr_core::CoreError::Queue("test".to_string()));
572        assert_eq!(core_err.classify(), ErrorClassification::Retriable);
573
574        let generic_err = SolanaToolError::Generic("some error".to_string());
575        assert_eq!(generic_err.classify(), ErrorClassification::Retriable);
576    }
577
578    #[test]
579    fn test_transaction_error_type_methods() {
580        let retryable = TransactionErrorType::Retryable(RetryableError::NetworkConnectivity);
581        let permanent = TransactionErrorType::Permanent(PermanentError::InsufficientFunds);
582        let rate_limited = TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit);
583        let unknown = TransactionErrorType::Unknown("test error".to_string());
584
585        assert!(retryable.is_retryable());
586        assert!(!retryable.is_rate_limited());
587
588        assert!(!permanent.is_retryable());
589        assert!(!permanent.is_rate_limited());
590
591        assert!(rate_limited.is_retryable());
592        assert!(rate_limited.is_rate_limited());
593
594        assert!(!unknown.is_retryable());
595        assert!(!unknown.is_rate_limited());
596    }
597
598    #[test]
599    fn test_io_error_classification() {
600        let io_error =
601            std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
602        let client_error = ClientError::new_with_request(
603            ClientErrorKind::Io(io_error),
604            solana_client::rpc_request::RpcRequest::GetAccountInfo,
605        );
606
607        let result = classify_transaction_error(&client_error);
608        assert_eq!(
609            result,
610            TransactionErrorType::Retryable(RetryableError::NetworkConnectivity)
611        );
612    }
613
614    #[test]
615    fn test_serde_error_classification() {
616        // Create a serde error by trying to parse invalid JSON
617        let serde_error: serde_json::Error =
618            serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
619        let client_error = ClientError::new_with_request(
620            ClientErrorKind::SerdeJson(serde_error),
621            solana_client::rpc_request::RpcRequest::GetAccountInfo,
622        );
623
624        let result = classify_transaction_error(&client_error);
625        assert_eq!(
626            result,
627            TransactionErrorType::Permanent(PermanentError::InvalidTransaction)
628        );
629    }
630
631    #[test]
632    fn test_custom_error_classification() {
633        // Test insufficient funds
634        let client_error = ClientError::new_with_request(
635            ClientErrorKind::Custom("InsufficientFundsForRent".to_string()),
636            solana_client::rpc_request::RpcRequest::SendTransaction,
637        );
638        let result = classify_transaction_error(&client_error);
639        assert_eq!(
640            result,
641            TransactionErrorType::Permanent(PermanentError::InsufficientFunds)
642        );
643
644        // Test invalid signature
645        let client_error = ClientError::new_with_request(
646            ClientErrorKind::Custom("InvalidSignature".to_string()),
647            solana_client::rpc_request::RpcRequest::SendTransaction,
648        );
649        let result = classify_transaction_error(&client_error);
650        assert_eq!(
651            result,
652            TransactionErrorType::Permanent(PermanentError::InvalidSignature)
653        );
654
655        // Test invalid account
656        let client_error = ClientError::new_with_request(
657            ClientErrorKind::Custom("InvalidAccountIndex".to_string()),
658            solana_client::rpc_request::RpcRequest::SendTransaction,
659        );
660        let result = classify_transaction_error(&client_error);
661        assert_eq!(
662            result,
663            TransactionErrorType::Permanent(PermanentError::InvalidAccount)
664        );
665
666        // Test duplicate signature
667        let client_error = ClientError::new_with_request(
668            ClientErrorKind::Custom("DuplicateSignature".to_string()),
669            solana_client::rpc_request::RpcRequest::SendTransaction,
670        );
671        let result = classify_transaction_error(&client_error);
672        assert_eq!(
673            result,
674            TransactionErrorType::Permanent(PermanentError::DuplicateTransaction)
675        );
676    }
677
678    #[cfg(test)]
679    use solana_client::rpc_request::RpcResponseErrorData;
680
681    #[test]
682    fn test_rpc_error_classification() {
683        // Test rate limiting
684        let rpc_error = RpcError::RpcResponseError {
685            code: 429,
686            message: "Too Many Requests".to_string(),
687            data: RpcResponseErrorData::Empty,
688        };
689        let client_error = ClientError::new_with_request(
690            ClientErrorKind::RpcError(rpc_error),
691            solana_client::rpc_request::RpcRequest::SendTransaction,
692        );
693        let result = classify_transaction_error(&client_error);
694        assert_eq!(
695            result,
696            TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit)
697        );
698
699        // Test network congestion (transaction pool full)
700        let rpc_error = RpcError::RpcResponseError {
701            code: -32002,
702            message: "Transaction pool is full".to_string(),
703            data: RpcResponseErrorData::Empty,
704        };
705        let client_error = ClientError::new_with_request(
706            ClientErrorKind::RpcError(rpc_error),
707            solana_client::rpc_request::RpcRequest::SendTransaction,
708        );
709        let result = classify_transaction_error(&client_error);
710        assert_eq!(
711            result,
712            TransactionErrorType::Retryable(RetryableError::NetworkCongestion)
713        );
714
715        // Test insufficient funds in RPC response
716        let rpc_error = RpcError::RpcResponseError {
717            code: -32602,
718            message: "InsufficientFundsForRent".to_string(),
719            data: RpcResponseErrorData::Empty,
720        };
721        let client_error = ClientError::new_with_request(
722            ClientErrorKind::RpcError(rpc_error),
723            solana_client::rpc_request::RpcRequest::SendTransaction,
724        );
725        let result = classify_transaction_error(&client_error);
726        assert_eq!(
727            result,
728            TransactionErrorType::Permanent(PermanentError::InsufficientFunds)
729        );
730    }
731
732    #[test]
733    fn test_rpc_request_error_classification() {
734        // Test rate limit in request error
735        let rpc_error = RpcError::RpcRequestError("rate limit exceeded".to_string());
736        let client_error = ClientError::new_with_request(
737            ClientErrorKind::RpcError(rpc_error),
738            solana_client::rpc_request::RpcRequest::SendTransaction,
739        );
740        let result = classify_transaction_error(&client_error);
741        assert_eq!(
742            result,
743            TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit)
744        );
745
746        // Test other RPC request error
747        let rpc_error = RpcError::RpcRequestError("network timeout".to_string());
748        let client_error = ClientError::new_with_request(
749            ClientErrorKind::RpcError(rpc_error),
750            solana_client::rpc_request::RpcRequest::SendTransaction,
751        );
752        let result = classify_transaction_error(&client_error);
753        assert_eq!(
754            result,
755            TransactionErrorType::Retryable(RetryableError::TemporaryRpcFailure)
756        );
757    }
758
759    #[test]
760    fn test_unknown_error_fallback() {
761        let client_error = ClientError::new_with_request(
762            ClientErrorKind::Custom("Unknown error type".to_string()),
763            solana_client::rpc_request::RpcRequest::GetAccountInfo,
764        );
765
766        let result = classify_transaction_error(&client_error);
767        assert!(matches!(result, TransactionErrorType::Unknown(_)));
768    }
769
770    // Additional tests for 100% coverage
771
772    #[test]
773    fn test_solana_tool_error_display() {
774        let tool_err = ToolError::invalid_input_string("test".to_string());
775        let error = SolanaToolError::ToolError(tool_err);
776        assert_eq!(
777            error.to_string(),
778            "Core tool error: Invalid input: test - test"
779        );
780
781        let signer_err = SignerError::Signing("Invalid signature".to_string());
782        let error = SolanaToolError::SignerError(signer_err);
783        assert_eq!(
784            error.to_string(),
785            "Signer context error: Signing error: Invalid signature"
786        );
787
788        let error = SolanaToolError::Rpc("test rpc error".to_string());
789        assert_eq!(error.to_string(), "RPC error: test rpc error");
790
791        let error = SolanaToolError::InvalidAddress("invalid addr".to_string());
792        assert_eq!(error.to_string(), "Invalid address: invalid addr");
793
794        let error = SolanaToolError::InvalidKey("invalid key".to_string());
795        assert_eq!(error.to_string(), "Invalid key: invalid key");
796
797        let error = SolanaToolError::InvalidSignature("invalid sig".to_string());
798        assert_eq!(error.to_string(), "Invalid signature: invalid sig");
799
800        let error = SolanaToolError::Transaction("tx error".to_string());
801        assert_eq!(error.to_string(), "Transaction error: tx error");
802
803        let error = SolanaToolError::InsufficientFunds;
804        assert_eq!(error.to_string(), "Insufficient funds for operation");
805
806        let error = SolanaToolError::InvalidTokenMint("invalid mint".to_string());
807        assert_eq!(error.to_string(), "Invalid token mint: invalid mint");
808
809        let error = SolanaToolError::Generic("generic error".to_string());
810        assert_eq!(error.to_string(), "Solana tool error: generic error");
811    }
812
813    #[test]
814    fn test_solana_tool_error_is_retriable() {
815        // Test ToolError is_retriable delegation
816        let tool_err = ToolError::invalid_input_string("test".to_string());
817        let error = SolanaToolError::ToolError(tool_err);
818        assert!(!error.is_retriable());
819
820        let tool_err = ToolError::retriable_string("test".to_string());
821        let error = SolanaToolError::ToolError(tool_err);
822        assert!(error.is_retriable());
823
824        // Test SignerError (non-retriable)
825        let signer_err = SignerError::Signing("Invalid signature".to_string());
826        let error = SolanaToolError::SignerError(signer_err);
827        assert!(!error.is_retriable());
828
829        // Test Core error (retriable)
830        let core_err = riglr_core::CoreError::Queue("test".to_string());
831        let error = SolanaToolError::Core(core_err);
832        assert!(error.is_retriable());
833
834        // Test RPC error (retriable)
835        let error = SolanaToolError::Rpc("test rpc error".to_string());
836        assert!(error.is_retriable());
837
838        // Test HTTP errors with different status codes
839        // Note: Creating a specific reqwest::Error is complex, so we test the logic path instead
840        let error = SolanaToolError::Rpc("timeout error".to_string());
841        assert!(error.is_retriable());
842
843        // Test invalid address/key/signature/token mint (non-retriable)
844        let error = SolanaToolError::InvalidAddress("invalid addr".to_string());
845        assert!(!error.is_retriable());
846
847        let error = SolanaToolError::InvalidKey("invalid key".to_string());
848        assert!(!error.is_retriable());
849
850        let error = SolanaToolError::InvalidSignature("invalid sig".to_string());
851        assert!(!error.is_retriable());
852
853        let error = SolanaToolError::InvalidTokenMint("invalid mint".to_string());
854        assert!(!error.is_retriable());
855
856        // Test insufficient funds (non-retriable)
857        let error = SolanaToolError::InsufficientFunds;
858        assert!(!error.is_retriable());
859
860        // Test transaction errors with different messages
861        let error = SolanaToolError::Transaction("insufficient funds detected".to_string());
862        assert!(!error.is_retriable());
863
864        let error = SolanaToolError::Transaction("invalid parameter".to_string());
865        assert!(!error.is_retriable());
866
867        let error = SolanaToolError::Transaction("network timeout".to_string());
868        assert!(error.is_retriable());
869
870        // Test serialization error (non-retriable)
871        let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
872        let error = SolanaToolError::Serialization(serde_err);
873        assert!(!error.is_retriable());
874
875        // Test generic error (retriable)
876        let error = SolanaToolError::Generic("generic error".to_string());
877        assert!(error.is_retriable());
878    }
879
880    #[test]
881    fn test_solana_tool_error_is_rate_limited() {
882        // Test RPC rate limit messages
883        let error = SolanaToolError::Rpc("429 Too Many Requests".to_string());
884        assert!(error.is_rate_limited());
885
886        let error = SolanaToolError::Rpc("rate limit exceeded".to_string());
887        assert!(error.is_rate_limited());
888
889        let error = SolanaToolError::Rpc("too many requests".to_string());
890        assert!(error.is_rate_limited());
891
892        let error = SolanaToolError::Rpc("normal error".to_string());
893        assert!(!error.is_rate_limited());
894
895        // Test HTTP rate limit status
896        // Note: Creating a reqwest::Error with specific status is complex,
897        // so we'll test the logic through SolanaClient error path
898
899        // Test non-rate-limited errors
900        let error = SolanaToolError::InvalidAddress("invalid addr".to_string());
901        assert!(!error.is_rate_limited());
902
903        let error = SolanaToolError::Generic("generic error".to_string());
904        assert!(!error.is_rate_limited());
905    }
906
907    #[test]
908    fn test_solana_tool_error_retry_delay() {
909        // Test rate-limited error delay
910        let error = SolanaToolError::Rpc("429 Too Many Requests".to_string());
911        assert_eq!(error.retry_delay(), Some(std::time::Duration::from_secs(1)));
912
913        // Test retriable but not rate-limited error delay
914        let error = SolanaToolError::Rpc("network error".to_string());
915        assert_eq!(
916            error.retry_delay(),
917            Some(std::time::Duration::from_millis(500))
918        );
919
920        // Test non-retriable error (no delay)
921        let error = SolanaToolError::InvalidAddress("invalid addr".to_string());
922        assert_eq!(error.retry_delay(), None);
923    }
924
925    #[test]
926    fn test_solana_client_error_is_retriable() {
927        // Create a retryable client error
928        let io_error =
929            std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
930        let client_error = ClientError::new_with_request(
931            ClientErrorKind::Io(io_error),
932            solana_client::rpc_request::RpcRequest::GetAccountInfo,
933        );
934        let error = SolanaToolError::SolanaClient(Box::new(client_error));
935        assert!(error.is_retriable());
936
937        // Create a non-retryable client error
938        let serde_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
939        let client_error = ClientError::new_with_request(
940            ClientErrorKind::SerdeJson(serde_error),
941            solana_client::rpc_request::RpcRequest::GetAccountInfo,
942        );
943        let error = SolanaToolError::SolanaClient(Box::new(client_error));
944        assert!(!error.is_retriable());
945    }
946
947    #[test]
948    fn test_solana_client_error_is_rate_limited() {
949        // Create a rate-limited client error
950        let rpc_error = RpcError::RpcResponseError {
951            code: 429,
952            message: "Too Many Requests".to_string(),
953            data: RpcResponseErrorData::Empty,
954        };
955        let client_error = ClientError::new_with_request(
956            ClientErrorKind::RpcError(rpc_error),
957            solana_client::rpc_request::RpcRequest::SendTransaction,
958        );
959        let error = SolanaToolError::SolanaClient(Box::new(client_error));
960        assert!(error.is_rate_limited());
961
962        // Create a non-rate-limited client error
963        let io_error =
964            std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
965        let client_error = ClientError::new_with_request(
966            ClientErrorKind::Io(io_error),
967            solana_client::rpc_request::RpcRequest::GetAccountInfo,
968        );
969        let error = SolanaToolError::SolanaClient(Box::new(client_error));
970        assert!(!error.is_rate_limited());
971    }
972
973    #[test]
974    fn test_from_client_error() {
975        let io_error =
976            std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
977        let client_error = ClientError::new_with_request(
978            ClientErrorKind::Io(io_error),
979            solana_client::rpc_request::RpcRequest::GetAccountInfo,
980        );
981
982        let solana_error: SolanaToolError = client_error.into();
983        assert!(matches!(solana_error, SolanaToolError::SolanaClient(_)));
984    }
985
986    #[test]
987    fn test_from_solana_tool_error_to_tool_error() {
988        // Test ToolError passthrough
989        let tool_err = ToolError::invalid_input_string("test".to_string());
990        let expected_string = tool_err.to_string();
991        let solana_err = SolanaToolError::ToolError(tool_err);
992        let converted: ToolError = solana_err.into();
993        assert_eq!(converted.to_string(), expected_string);
994
995        // Test SignerError conversion
996        let signer_err = SignerError::Signing("Invalid signature".to_string());
997        let solana_err = SolanaToolError::SignerError(signer_err);
998        let converted: ToolError = solana_err.into();
999        assert!(matches!(converted, ToolError::SignerContext(_)));
1000
1001        // Test invalid input conversions
1002        let solana_err = SolanaToolError::InvalidAddress("test addr".to_string());
1003        let converted: ToolError = solana_err.into();
1004        assert!(converted.to_string().contains("Invalid input"));
1005
1006        let solana_err = SolanaToolError::InvalidKey("test key".to_string());
1007        let converted: ToolError = solana_err.into();
1008        assert!(converted.to_string().contains("Invalid input"));
1009
1010        let solana_err = SolanaToolError::InvalidSignature("test sig".to_string());
1011        let converted: ToolError = solana_err.into();
1012        assert!(converted.to_string().contains("Invalid input"));
1013
1014        let solana_err = SolanaToolError::InvalidTokenMint("test mint".to_string());
1015        let converted: ToolError = solana_err.into();
1016        assert!(converted.to_string().contains("Invalid input"));
1017
1018        // Test rate-limited error conversion
1019        let solana_err = SolanaToolError::Rpc("429 Too Many Requests".to_string());
1020        let converted: ToolError = solana_err.into();
1021        // This should be a rate-limited error
1022        assert!(converted.to_string().contains("Rate limited"));
1023
1024        // Test retriable error conversion
1025        let solana_err = SolanaToolError::Rpc("network timeout".to_string());
1026        let converted: ToolError = solana_err.into();
1027        // This should be a retriable error
1028        assert!(converted.to_string().contains("network timeout"));
1029
1030        // Test generic error conversion (non-retriable/non-rate-limited)
1031        let solana_err = SolanaToolError::InsufficientFunds;
1032        let converted: ToolError = solana_err.into();
1033        // This should be converted as retriable (the default case)
1034        assert!(converted.to_string().contains("Insufficient funds"));
1035    }
1036
1037    #[test]
1038    fn test_reqwest_error_classification() {
1039        // Test timeout error - we'll use a serde error instead since reqwest::Error creation is complex
1040        let serde_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
1041        let client_error = ClientError::new_with_request(
1042            ClientErrorKind::SerdeJson(serde_error),
1043            solana_client::rpc_request::RpcRequest::GetAccountInfo,
1044        );
1045        let result = classify_transaction_error(&client_error);
1046        assert_eq!(
1047            result,
1048            TransactionErrorType::Permanent(PermanentError::InvalidTransaction)
1049        );
1050    }
1051
1052    #[test]
1053    fn test_rpc_error_response_edge_cases() {
1054        // Test internal error (-32603)
1055        let rpc_error = RpcError::RpcResponseError {
1056            code: -32603,
1057            message: "Internal error".to_string(),
1058            data: RpcResponseErrorData::Empty,
1059        };
1060        let client_error = ClientError::new_with_request(
1061            ClientErrorKind::RpcError(rpc_error),
1062            solana_client::rpc_request::RpcRequest::SendTransaction,
1063        );
1064        let result = classify_transaction_error(&client_error);
1065        assert_eq!(
1066            result,
1067            TransactionErrorType::Retryable(RetryableError::TemporaryRpcFailure)
1068        );
1069
1070        // Test node behind (-32005)
1071        let rpc_error = RpcError::RpcResponseError {
1072            code: -32005,
1073            message: "Node is behind".to_string(),
1074            data: RpcResponseErrorData::Empty,
1075        };
1076        let client_error = ClientError::new_with_request(
1077            ClientErrorKind::RpcError(rpc_error),
1078            solana_client::rpc_request::RpcRequest::SendTransaction,
1079        );
1080        let result = classify_transaction_error(&client_error);
1081        assert_eq!(
1082            result,
1083            TransactionErrorType::Retryable(RetryableError::NetworkCongestion)
1084        );
1085
1086        // Test invalid signature in RPC message
1087        let rpc_error = RpcError::RpcResponseError {
1088            code: -32001,
1089            message: "invalid signature provided".to_string(),
1090            data: RpcResponseErrorData::Empty,
1091        };
1092        let client_error = ClientError::new_with_request(
1093            ClientErrorKind::RpcError(rpc_error),
1094            solana_client::rpc_request::RpcRequest::SendTransaction,
1095        );
1096        let result = classify_transaction_error(&client_error);
1097        assert_eq!(
1098            result,
1099            TransactionErrorType::Permanent(PermanentError::InvalidSignature)
1100        );
1101
1102        // Test invalid account in RPC message
1103        let rpc_error = RpcError::RpcResponseError {
1104            code: -32001,
1105            message: "invalid account reference".to_string(),
1106            data: RpcResponseErrorData::Empty,
1107        };
1108        let client_error = ClientError::new_with_request(
1109            ClientErrorKind::RpcError(rpc_error),
1110            solana_client::rpc_request::RpcRequest::SendTransaction,
1111        );
1112        let result = classify_transaction_error(&client_error);
1113        assert_eq!(
1114            result,
1115            TransactionErrorType::Permanent(PermanentError::InvalidAccount)
1116        );
1117
1118        // Test instruction error in RPC message
1119        let rpc_error = RpcError::RpcResponseError {
1120            code: -32001,
1121            message: "Instruction error occurred".to_string(),
1122            data: RpcResponseErrorData::Empty,
1123        };
1124        let client_error = ClientError::new_with_request(
1125            ClientErrorKind::RpcError(rpc_error),
1126            solana_client::rpc_request::RpcRequest::SendTransaction,
1127        );
1128        let result = classify_transaction_error(&client_error);
1129        assert_eq!(
1130            result,
1131            TransactionErrorType::Permanent(PermanentError::InstructionError)
1132        );
1133
1134        // Test unknown error code with message
1135        let rpc_error = RpcError::RpcResponseError {
1136            code: -99999,
1137            message: "Unknown error".to_string(),
1138            data: RpcResponseErrorData::Empty,
1139        };
1140        let client_error = ClientError::new_with_request(
1141            ClientErrorKind::RpcError(rpc_error),
1142            solana_client::rpc_request::RpcRequest::SendTransaction,
1143        );
1144        let result = classify_transaction_error(&client_error);
1145        assert!(matches!(result, TransactionErrorType::Unknown(_)));
1146        if let TransactionErrorType::Unknown(msg) = result {
1147            assert!(msg.contains("RPC Error -99999"));
1148            assert!(msg.contains("Unknown error"));
1149        }
1150    }
1151
1152    #[test]
1153    fn test_rpc_parse_error_classification() {
1154        let rpc_error = RpcError::ParseError("Invalid JSON".to_string());
1155        let client_error = ClientError::new_with_request(
1156            ClientErrorKind::RpcError(rpc_error),
1157            solana_client::rpc_request::RpcRequest::SendTransaction,
1158        );
1159        let result = classify_transaction_error(&client_error);
1160        assert_eq!(
1161            result,
1162            TransactionErrorType::Permanent(PermanentError::InvalidTransaction)
1163        );
1164    }
1165
1166    #[test]
1167    fn test_rpc_for_user_error_classification() {
1168        let rpc_error = RpcError::ForUser("User-facing error message".to_string());
1169        let client_error = ClientError::new_with_request(
1170            ClientErrorKind::RpcError(rpc_error),
1171            solana_client::rpc_request::RpcRequest::SendTransaction,
1172        );
1173        let result = classify_transaction_error(&client_error);
1174        assert!(matches!(result, TransactionErrorType::Unknown(_)));
1175        if let TransactionErrorType::Unknown(msg) = result {
1176            assert_eq!(msg, "User-facing error message");
1177        }
1178    }
1179
1180    #[test]
1181    fn test_rpc_request_error_with_different_messages() {
1182        // Test "429" in message
1183        let rpc_error = RpcError::RpcRequestError("HTTP 429 rate limit".to_string());
1184        let client_error = ClientError::new_with_request(
1185            ClientErrorKind::RpcError(rpc_error),
1186            solana_client::rpc_request::RpcRequest::SendTransaction,
1187        );
1188        let result = classify_transaction_error(&client_error);
1189        assert_eq!(
1190            result,
1191            TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit)
1192        );
1193
1194        // Test "too many requests" in message
1195        let rpc_error = RpcError::RpcRequestError("too many requests received".to_string());
1196        let client_error = ClientError::new_with_request(
1197            ClientErrorKind::RpcError(rpc_error),
1198            solana_client::rpc_request::RpcRequest::SendTransaction,
1199        );
1200        let result = classify_transaction_error(&client_error);
1201        assert_eq!(
1202            result,
1203            TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit)
1204        );
1205    }
1206
1207    #[test]
1208    fn test_classify_transaction_error_with_unknown_client_error_kind() {
1209        // Create a client error with an unhandled error kind
1210        // We'll use a custom error for this test
1211        let custom_msg = "Custom unknown error".to_string();
1212        let client_error = ClientError::new_with_request(
1213            ClientErrorKind::Custom(custom_msg.clone()),
1214            solana_client::rpc_request::RpcRequest::GetAccountInfo,
1215        );
1216
1217        // Since our custom message doesn't match any known patterns, it should be Unknown
1218        let result = classify_transaction_error(&client_error);
1219        assert!(matches!(result, TransactionErrorType::Unknown(_)));
1220    }
1221
1222    #[test]
1223    fn test_custom_error_with_insufficient_funds_lowercase() {
1224        // Test "insufficient funds" (lowercase) in custom error
1225        let client_error = ClientError::new_with_request(
1226            ClientErrorKind::Custom("insufficient funds for transaction".to_string()),
1227            solana_client::rpc_request::RpcRequest::SendTransaction,
1228        );
1229        let result = classify_transaction_error(&client_error);
1230        assert_eq!(
1231            result,
1232            TransactionErrorType::Permanent(PermanentError::InsufficientFunds)
1233        );
1234    }
1235
1236    #[test]
1237    fn test_error_variants_equality() {
1238        // Test RetryableError variants
1239        assert_eq!(
1240            RetryableError::NetworkConnectivity,
1241            RetryableError::NetworkConnectivity
1242        );
1243        assert_ne!(
1244            RetryableError::NetworkConnectivity,
1245            RetryableError::TemporaryRpcFailure
1246        );
1247
1248        // Test PermanentError variants
1249        assert_eq!(
1250            PermanentError::InsufficientFunds,
1251            PermanentError::InsufficientFunds
1252        );
1253        assert_ne!(
1254            PermanentError::InsufficientFunds,
1255            PermanentError::InvalidSignature
1256        );
1257
1258        // Test RateLimitError variants
1259        assert_eq!(RateLimitError::RpcRateLimit, RateLimitError::RpcRateLimit);
1260        assert_ne!(
1261            RateLimitError::RpcRateLimit,
1262            RateLimitError::TooManyRequests
1263        );
1264
1265        // Test TransactionErrorType variants
1266        assert_eq!(
1267            TransactionErrorType::Retryable(RetryableError::NetworkConnectivity),
1268            TransactionErrorType::Retryable(RetryableError::NetworkConnectivity)
1269        );
1270        assert_ne!(
1271            TransactionErrorType::Retryable(RetryableError::NetworkConnectivity),
1272            TransactionErrorType::Permanent(PermanentError::InsufficientFunds)
1273        );
1274    }
1275
1276    #[test]
1277    fn test_error_debug_format() {
1278        // Test Debug implementation for all error types
1279        let retryable = RetryableError::NetworkConnectivity;
1280        assert!(!format!("{:?}", retryable).is_empty());
1281
1282        let permanent = PermanentError::InsufficientFunds;
1283        assert!(!format!("{:?}", permanent).is_empty());
1284
1285        let rate_limit = RateLimitError::RpcRateLimit;
1286        assert!(!format!("{:?}", rate_limit).is_empty());
1287
1288        let transaction_error = TransactionErrorType::Unknown("test".to_string());
1289        assert!(!format!("{:?}", transaction_error).is_empty());
1290    }
1291
1292    #[test]
1293    fn test_error_downcasting_preserves_structured_context() {
1294        use std::error::Error;
1295
1296        // Test Case 1: InvalidAddress error should be downcastable
1297        let solana_error = SolanaToolError::InvalidAddress("bad_address".to_string());
1298        let tool_error: ToolError = solana_error.into();
1299
1300        // Verify the ToolError has a source
1301        assert!(
1302            tool_error.source().is_some(),
1303            "ToolError should have a source"
1304        );
1305
1306        // Downcast the source back to SolanaToolError
1307        let source = tool_error.source().unwrap();
1308        let downcasted = source.downcast_ref::<SolanaToolError>();
1309        assert!(
1310            downcasted.is_some(),
1311            "Should be able to downcast source to SolanaToolError"
1312        );
1313
1314        // Verify the downcast error matches the original
1315        if let Some(SolanaToolError::InvalidAddress(msg)) = downcasted {
1316            assert_eq!(msg, "bad_address", "Downcast should preserve error details");
1317        } else {
1318            panic!("Downcast error should be InvalidAddress variant");
1319        }
1320
1321        // Test Case 2: InsufficientFunds error should be downcastable
1322        let solana_error = SolanaToolError::InsufficientFunds;
1323        let tool_error: ToolError = solana_error.into();
1324
1325        assert!(
1326            tool_error.source().is_some(),
1327            "ToolError should have a source for InsufficientFunds"
1328        );
1329
1330        let source = tool_error.source().unwrap();
1331        let downcasted = source.downcast_ref::<SolanaToolError>();
1332        assert!(
1333            downcasted.is_some(),
1334            "Should be able to downcast InsufficientFunds error"
1335        );
1336
1337        assert!(
1338            matches!(downcasted, Some(SolanaToolError::InsufficientFunds)),
1339            "Downcast should preserve InsufficientFunds variant"
1340        );
1341
1342        // Test Case 3: Rate-limited RPC error should be downcastable
1343        let solana_error = SolanaToolError::Rpc("429 Too Many Requests".to_string());
1344        let tool_error: ToolError = solana_error.into();
1345
1346        assert!(
1347            tool_error.source().is_some(),
1348            "ToolError should have a source for rate-limited error"
1349        );
1350
1351        let source = tool_error.source().unwrap();
1352        let downcasted = source.downcast_ref::<SolanaToolError>();
1353        assert!(
1354            downcasted.is_some(),
1355            "Should be able to downcast rate-limited error"
1356        );
1357
1358        if let Some(SolanaToolError::Rpc(msg)) = downcasted {
1359            assert_eq!(
1360                msg, "429 Too Many Requests",
1361                "Downcast should preserve RPC error message"
1362            );
1363        } else {
1364            panic!("Downcast error should be Rpc variant");
1365        }
1366
1367        // Test Case 4: SolanaClient error should be downcastable
1368        let client_error = ClientError::new_with_request(
1369            ClientErrorKind::Custom("test error".to_string()),
1370            solana_client::rpc_request::RpcRequest::GetAccountInfo,
1371        );
1372        let solana_error = SolanaToolError::SolanaClient(Box::new(client_error));
1373        let tool_error: ToolError = solana_error.into();
1374
1375        assert!(
1376            tool_error.source().is_some(),
1377            "ToolError should have a source for SolanaClient error"
1378        );
1379
1380        let source = tool_error.source().unwrap();
1381        let downcasted = source.downcast_ref::<SolanaToolError>();
1382        assert!(
1383            downcasted.is_some(),
1384            "Should be able to downcast SolanaClient error"
1385        );
1386
1387        assert!(
1388            matches!(downcasted, Some(SolanaToolError::SolanaClient(_))),
1389            "Downcast should preserve SolanaClient variant"
1390        );
1391
1392        // Test Case 5: Verify ToolError passthrough doesn't add extra layer
1393        let inner_tool_error = ToolError::permanent_string("inner error".to_string());
1394        let solana_error = SolanaToolError::ToolError(inner_tool_error.clone());
1395        let converted: ToolError = solana_error.into();
1396
1397        // The converted error should be the inner ToolError, not wrapped again
1398        assert_eq!(
1399            converted.to_string(),
1400            inner_tool_error.to_string(),
1401            "ToolError passthrough should not add extra wrapping"
1402        );
1403    }
1404}