1use 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#[derive(Error, Debug)]
11#[allow(clippy::result_large_err)]
12#[allow(clippy::large_enum_variant)]
13pub enum SolanaToolError {
14 #[error("Core tool error: {0}")]
16 ToolError(#[from] ToolError),
17
18 #[error("Signer context error: {0}")]
20 SignerError(#[from] SignerError),
21
22 #[error("RPC error: {0}")]
25 Rpc(String),
26
27 #[error("Solana client error: {0}")]
29 SolanaClient(Box<ClientError>),
30
31 #[error("Invalid address: {0}")]
33 InvalidAddress(String),
34
35 #[error("Invalid key: {0}")]
37 InvalidKey(String),
38
39 #[error("Invalid signature: {0}")]
41 InvalidSignature(String),
42
43 #[error("Transaction error: {0}")]
45 Transaction(String),
46
47 #[error("Insufficient funds for operation")]
49 InsufficientFunds,
50
51 #[error("Invalid token mint: {0}")]
53 InvalidTokenMint(String),
54
55 #[error("Serialization error: {0}")]
57 Serialization(#[from] serde_json::Error),
58
59 #[error("HTTP error: {0}")]
62 Http(#[from] reqwest::Error),
63
64 #[error("Core error: {0}")]
66 Core(#[from] riglr_core::CoreError),
67
68 #[error("Solana tool error: {0}")]
70 Generic(String),
71}
72
73pub type Result<T> = std::result::Result<T, SolanaToolError>;
75
76#[derive(Debug, PartialEq)]
78enum ErrorClassification {
79 Permanent,
81 Retriable,
83 RateLimited { delay: Option<std::time::Duration> },
85 InvalidInput,
87 SignerContext,
89 ToolErrorPassthrough(ToolError),
91}
92
93impl SolanaToolError {
94 fn classify(&self) -> ErrorClassification {
99 match self {
100 SolanaToolError::ToolError(e) => ErrorClassification::ToolErrorPassthrough(e.clone()),
102
103 SolanaToolError::SignerError(_) => ErrorClassification::SignerContext,
105
106 SolanaToolError::InvalidAddress(_)
108 | SolanaToolError::InvalidKey(_)
109 | SolanaToolError::InvalidSignature(_)
110 | SolanaToolError::InvalidTokenMint(_) => ErrorClassification::InvalidInput,
111
112 SolanaToolError::InsufficientFunds => ErrorClassification::Permanent,
114
115 SolanaToolError::Serialization(_) => ErrorClassification::Permanent,
117
118 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 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 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 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 SolanaToolError::Core(_) => ErrorClassification::Retriable,
185
186 SolanaToolError::Generic(_) => ErrorClassification::Retriable,
188 }
189 }
190
191 pub fn is_retriable(&self) -> bool {
196 match self {
197 SolanaToolError::ToolError(tool_err) => tool_err.is_retriable(),
199 SolanaToolError::SignerError(_) => false, SolanaToolError::Core(_) => true, 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 SolanaToolError::SolanaClient(ref client_err) => {
215 let error_type = classify_transaction_error(client_err);
216 error_type.is_retryable()
217 }
218
219 SolanaToolError::InvalidAddress(_) => false,
221 SolanaToolError::InvalidKey(_) => false,
222 SolanaToolError::InvalidSignature(_) => false,
223 SolanaToolError::InvalidTokenMint(_) => false,
224
225 SolanaToolError::InsufficientFunds => false,
227
228 SolanaToolError::Transaction(msg) => {
230 !(msg.contains("insufficient funds") || msg.contains("invalid"))
231 }
232
233 SolanaToolError::Serialization(_) => false,
235
236 SolanaToolError::Generic(_) => true,
238 }
239 }
240
241 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 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#[derive(Debug, Clone, PartialEq)]
274pub enum TransactionErrorType {
275 Retryable(RetryableError),
277 Permanent(PermanentError),
279 RateLimited(RateLimitError),
281 Unknown(String),
283}
284
285#[derive(Debug, Clone, PartialEq)]
287pub enum RetryableError {
288 NetworkConnectivity,
290 TemporaryRpcFailure,
292 NetworkCongestion,
294 TransactionPoolFull,
296}
297
298#[derive(Debug, Clone, PartialEq)]
300pub enum PermanentError {
301 InsufficientFunds,
303 InvalidSignature,
305 InvalidAccount,
307 InstructionError,
309 InvalidTransaction,
311 DuplicateTransaction,
313}
314
315#[derive(Debug, Clone, PartialEq)]
317pub enum RateLimitError {
318 RpcRateLimit,
320 TooManyRequests,
322}
323
324impl TransactionErrorType {
325 pub fn is_retryable(&self) -> bool {
327 matches!(
328 self,
329 TransactionErrorType::Retryable(_) | TransactionErrorType::RateLimited(_)
330 )
331 }
332
333 pub fn is_rate_limited(&self) -> bool {
335 matches!(self, TransactionErrorType::RateLimited(_))
336 }
337}
338
339pub 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 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
381fn 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 match *code {
399 429 => TransactionErrorType::RateLimited(RateLimitError::RpcRateLimit),
400 -32603 => TransactionErrorType::Retryable(RetryableError::TemporaryRpcFailure), -32002 => TransactionErrorType::Retryable(RetryableError::NetworkCongestion), -32005 => TransactionErrorType::Retryable(RetryableError::NetworkCongestion), _ => {
404 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
424impl From<SolanaToolError> for ToolError {
428 fn from(err: SolanaToolError) -> Self {
429 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]
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 let insufficient =
552 SolanaToolError::Transaction("insufficient funds for transaction".to_string());
553 assert_eq!(insufficient.classify(), ErrorClassification::Permanent);
554
555 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 let signer_err = SignerError::Signing("Invalid signature".to_string());
826 let error = SolanaToolError::SignerError(signer_err);
827 assert!(!error.is_retriable());
828
829 let core_err = riglr_core::CoreError::Queue("test".to_string());
831 let error = SolanaToolError::Core(core_err);
832 assert!(error.is_retriable());
833
834 let error = SolanaToolError::Rpc("test rpc error".to_string());
836 assert!(error.is_retriable());
837
838 let error = SolanaToolError::Rpc("timeout error".to_string());
841 assert!(error.is_retriable());
842
843 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 let error = SolanaToolError::InsufficientFunds;
858 assert!(!error.is_retriable());
859
860 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let solana_err = SolanaToolError::Rpc("429 Too Many Requests".to_string());
1020 let converted: ToolError = solana_err.into();
1021 assert!(converted.to_string().contains("Rate limited"));
1023
1024 let solana_err = SolanaToolError::Rpc("network timeout".to_string());
1026 let converted: ToolError = solana_err.into();
1027 assert!(converted.to_string().contains("network timeout"));
1029
1030 let solana_err = SolanaToolError::InsufficientFunds;
1032 let converted: ToolError = solana_err.into();
1033 assert!(converted.to_string().contains("Insufficient funds"));
1035 }
1036
1037 #[test]
1038 fn test_reqwest_error_classification() {
1039 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(
1240 RetryableError::NetworkConnectivity,
1241 RetryableError::NetworkConnectivity
1242 );
1243 assert_ne!(
1244 RetryableError::NetworkConnectivity,
1245 RetryableError::TemporaryRpcFailure
1246 );
1247
1248 assert_eq!(
1250 PermanentError::InsufficientFunds,
1251 PermanentError::InsufficientFunds
1252 );
1253 assert_ne!(
1254 PermanentError::InsufficientFunds,
1255 PermanentError::InvalidSignature
1256 );
1257
1258 assert_eq!(RateLimitError::RpcRateLimit, RateLimitError::RpcRateLimit);
1260 assert_ne!(
1261 RateLimitError::RpcRateLimit,
1262 RateLimitError::TooManyRequests
1263 );
1264
1265 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 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 let solana_error = SolanaToolError::InvalidAddress("bad_address".to_string());
1298 let tool_error: ToolError = solana_error.into();
1299
1300 assert!(
1302 tool_error.source().is_some(),
1303 "ToolError should have a source"
1304 );
1305
1306 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 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 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 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 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 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 assert_eq!(
1399 converted.to_string(),
1400 inner_tool_error.to_string(),
1401 "ToolError passthrough should not add extra wrapping"
1402 );
1403 }
1404}