1use std::fmt;
7use thiserror::Error;
8
9pub type AptosResult<T> = Result<T, AptosError>;
11
12#[derive(Error, Debug)]
32pub enum AptosError {
33 #[error("HTTP error: {0}")]
35 Http(#[from] reqwest::Error),
36
37 #[error("JSON error: {0}")]
39 Json(#[from] serde_json::Error),
40
41 #[error("BCS error: {0}")]
43 Bcs(String),
44
45 #[error("URL error: {0}")]
47 Url(#[from] url::ParseError),
48
49 #[error("Hex error: {0}")]
51 Hex(#[from] const_hex::FromHexError),
52
53 #[error("Invalid address: {0}")]
55 InvalidAddress(String),
56
57 #[error("Invalid public key: {0}")]
59 InvalidPublicKey(String),
60
61 #[error("Invalid private key: {0}")]
63 InvalidPrivateKey(String),
64
65 #[error("Invalid signature: {0}")]
67 InvalidSignature(String),
68
69 #[error("Signature verification failed")]
71 SignatureVerificationFailed,
72
73 #[error("Invalid type tag: {0}")]
75 InvalidTypeTag(String),
76
77 #[error("Transaction error: {0}")]
79 Transaction(String),
80
81 #[error("Simulation failed: {0}")]
83 SimulationFailed(String),
84
85 #[error("Submission failed: {0}")]
87 SubmissionFailed(String),
88
89 #[error("Execution failed: {vm_status}")]
91 ExecutionFailed {
92 vm_status: String,
94 },
95
96 #[error("Transaction timed out after {timeout_secs} seconds")]
98 TransactionTimeout {
99 hash: String,
101 timeout_secs: u64,
103 },
104
105 #[error("API error ({status_code}): {message}")]
107 Api {
108 status_code: u16,
110 message: String,
112 error_code: Option<String>,
114 vm_error_code: Option<u64>,
116 },
117
118 #[error("Rate limited: retry after {retry_after_secs:?} seconds")]
120 RateLimited {
121 retry_after_secs: Option<u64>,
123 },
124
125 #[error("Resource not found: {0}")]
127 NotFound(String),
128
129 #[error("Account not found: {0}")]
131 AccountNotFound(String),
132
133 #[error("Invalid mnemonic: {0}")]
135 InvalidMnemonic(String),
136
137 #[error("Invalid JWT: {0}")]
139 InvalidJwt(String),
140
141 #[error("Key derivation error: {0}")]
143 KeyDerivation(String),
144
145 #[error("Insufficient signatures: need {required}, got {provided}")]
147 InsufficientSignatures {
148 required: usize,
150 provided: usize,
152 },
153
154 #[error("Feature not enabled: {0}. Enable the '{0}' feature in Cargo.toml")]
156 FeatureNotEnabled(String),
157
158 #[error("Configuration error: {0}")]
160 Config(String),
161
162 #[error("Internal error: {0}")]
164 Internal(String),
165
166 #[error("{0}")]
168 Other(#[from] anyhow::Error),
169}
170
171const MAX_ERROR_MESSAGE_LENGTH: usize = 1000;
173
174const SENSITIVE_PATTERNS: &[&str] = &[
181 "private_key",
182 "secret",
183 "password",
184 "mnemonic",
185 "seed",
186 "bearer",
187 "authorization",
188 "token",
189 "jwt",
190 "credential",
191 "api_key",
192 "apikey",
193 "access_token",
194 "refresh_token",
195 "pepper",
196];
197
198impl AptosError {
199 pub fn bcs<E: fmt::Display>(err: E) -> Self {
201 Self::Bcs(err.to_string())
202 }
203
204 pub fn transaction<S: Into<String>>(msg: S) -> Self {
206 Self::Transaction(msg.into())
207 }
208
209 pub fn api(status_code: u16, message: impl Into<String>) -> Self {
211 Self::Api {
212 status_code,
213 message: message.into(),
214 error_code: None,
215 vm_error_code: None,
216 }
217 }
218
219 pub fn api_with_details(
221 status_code: u16,
222 message: impl Into<String>,
223 error_code: Option<String>,
224 vm_error_code: Option<u64>,
225 ) -> Self {
226 Self::Api {
227 status_code,
228 message: message.into(),
229 error_code,
230 vm_error_code,
231 }
232 }
233
234 pub fn is_not_found(&self) -> bool {
236 matches!(
237 self,
238 Self::NotFound(_)
239 | Self::AccountNotFound(_)
240 | Self::Api {
241 status_code: 404,
242 ..
243 }
244 )
245 }
246
247 pub fn is_timeout(&self) -> bool {
249 matches!(self, Self::TransactionTimeout { .. })
250 }
251
252 pub fn is_retryable(&self) -> bool {
254 match self {
255 Self::Http(e) => {
256 #[cfg(not(target_arch = "wasm32"))]
257 return e.is_timeout() || e.is_connect();
258 #[cfg(target_arch = "wasm32")]
259 return e.is_timeout();
260 }
261 Self::Api { status_code, .. } => {
262 matches!(status_code, 429 | 500 | 502 | 503 | 504)
263 }
264 Self::RateLimited { .. } => true,
265 _ => false,
266 }
267 }
268
269 pub fn sanitized_message(&self) -> String {
286 let raw_message = self.to_string();
287 Self::sanitize_string(&raw_message)
288 }
289
290 fn sanitize_string(s: &str) -> String {
292 let cleaned: String = s
294 .chars()
295 .filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
296 .collect();
297
298 let lower = cleaned.to_lowercase();
300 for pattern in SENSITIVE_PATTERNS {
301 if lower.contains(pattern) {
302 return format!("[REDACTED: message contained sensitive pattern '{pattern}']");
303 }
304 }
305
306 for scheme in ["http://", "https://"] {
314 let mut search_from = 0;
315 while let Some(rel_pos) = lower[search_from..].find(scheme) {
316 let url_start = search_from + rel_pos;
317 let url_rest = &lower[url_start..];
320 let url_end = url_rest
321 .find(|c: char| c.is_whitespace() || c == '>' || c == '"' || c == '\'')
322 .unwrap_or(url_rest.len());
323 let url_token = &url_rest[..url_end];
324 if url_token.contains('?') {
325 return "[REDACTED: message contained URL with query parameters]".into();
326 }
327 search_from = url_start + scheme.len();
329 }
330 }
331
332 if cleaned.len() > MAX_ERROR_MESSAGE_LENGTH {
334 let mut end = MAX_ERROR_MESSAGE_LENGTH;
335 while end > 0 && !cleaned.is_char_boundary(end) {
336 end -= 1;
337 }
338 format!(
339 "{}... [truncated, total length: {}]",
340 &cleaned[..end],
341 cleaned.len()
342 )
343 } else {
344 cleaned
345 }
346 }
347
348 pub fn user_message(&self) -> &'static str {
353 match self {
354 Self::Http(_) => "Network error occurred",
355 Self::Json(_) => "Failed to process response",
356 Self::Bcs(_) => "Failed to process data",
357 Self::Url(_) => "Invalid URL",
358 Self::Hex(_) => "Invalid hex format",
359 Self::InvalidAddress(_) => "Invalid account address",
360 Self::InvalidPublicKey(_) => "Invalid public key",
361 Self::InvalidPrivateKey(_) => "Invalid private key",
362 Self::InvalidSignature(_) => "Invalid signature",
363 Self::SignatureVerificationFailed => "Signature verification failed",
364 Self::InvalidTypeTag(_) => "Invalid type format",
365 Self::Transaction(_) => "Transaction error",
366 Self::SimulationFailed(_) => "Transaction simulation failed",
367 Self::SubmissionFailed(_) => "Transaction submission failed",
368 Self::ExecutionFailed { .. } => "Transaction execution failed",
369 Self::TransactionTimeout { .. } => "Transaction timed out",
370 Self::NotFound(_)
371 | Self::Api {
372 status_code: 404, ..
373 } => "Resource not found",
374 Self::RateLimited { .. }
375 | Self::Api {
376 status_code: 429, ..
377 } => "Rate limit exceeded",
378 Self::Api { status_code, .. } if *status_code >= 500 => "Server error",
379 Self::Api { .. } => "API error",
380 Self::AccountNotFound(_) => "Account not found",
381 Self::InvalidMnemonic(_) => "Invalid recovery phrase",
382 Self::InvalidJwt(_) => "Invalid authentication token",
383 Self::KeyDerivation(_) => "Key derivation failed",
384 Self::InsufficientSignatures { .. } => "Insufficient signatures",
385 Self::FeatureNotEnabled(_) => "Feature not enabled",
386 Self::Config(_) => "Configuration error",
387 Self::Internal(_) => "Internal error",
388 Self::Other(_) => "An error occurred",
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_error_display() {
399 let err = AptosError::InvalidAddress("bad address".to_string());
400 assert_eq!(err.to_string(), "Invalid address: bad address");
401 }
402
403 #[test]
404 fn test_is_not_found() {
405 assert!(AptosError::NotFound("test".to_string()).is_not_found());
406 assert!(AptosError::AccountNotFound("0x1".to_string()).is_not_found());
407 assert!(AptosError::api(404, "not found").is_not_found());
408 assert!(!AptosError::api(500, "server error").is_not_found());
409 }
410
411 #[test]
412 fn test_is_retryable() {
413 assert!(AptosError::api(429, "rate limited").is_retryable());
414 assert!(AptosError::api(503, "unavailable").is_retryable());
415 assert!(AptosError::api(500, "internal error").is_retryable());
416 assert!(AptosError::api(502, "bad gateway").is_retryable());
417 assert!(AptosError::api(504, "timeout").is_retryable());
418 assert!(!AptosError::api(400, "bad request").is_retryable());
419 }
420
421 #[test]
422 fn test_is_timeout() {
423 let err = AptosError::TransactionTimeout {
424 hash: "0x123".to_string(),
425 timeout_secs: 30,
426 };
427 assert!(err.is_timeout());
428 assert!(!AptosError::InvalidAddress("test".to_string()).is_timeout());
429 }
430
431 #[test]
432 fn test_bcs_error() {
433 let err = AptosError::bcs("serialization failed");
434 assert!(matches!(err, AptosError::Bcs(_)));
435 assert!(err.to_string().contains("serialization failed"));
436 }
437
438 #[test]
439 fn test_transaction_error() {
440 let err = AptosError::transaction("invalid payload");
441 assert!(matches!(err, AptosError::Transaction(_)));
442 assert!(err.to_string().contains("invalid payload"));
443 }
444
445 #[test]
446 fn test_api_error() {
447 let err = AptosError::api(400, "bad request");
448 assert!(err.to_string().contains("400"));
449 assert!(err.to_string().contains("bad request"));
450 }
451
452 #[test]
453 fn test_api_error_with_details() {
454 let err = AptosError::api_with_details(
455 400,
456 "invalid argument",
457 Some("INVALID_ARGUMENT".to_string()),
458 Some(42),
459 );
460 if let AptosError::Api {
461 status_code,
462 message,
463 error_code,
464 vm_error_code,
465 } = err
466 {
467 assert_eq!(status_code, 400);
468 assert_eq!(message, "invalid argument");
469 assert_eq!(error_code, Some("INVALID_ARGUMENT".to_string()));
470 assert_eq!(vm_error_code, Some(42));
471 } else {
472 panic!("Expected Api error variant");
473 }
474 }
475
476 #[test]
477 fn test_various_error_displays() {
478 assert!(
479 AptosError::InvalidPublicKey("bad key".to_string())
480 .to_string()
481 .contains("public key")
482 );
483 assert!(
484 AptosError::InvalidPrivateKey("bad key".to_string())
485 .to_string()
486 .contains("private key")
487 );
488 assert!(
489 AptosError::InvalidSignature("bad sig".to_string())
490 .to_string()
491 .contains("signature")
492 );
493 assert!(
494 AptosError::SignatureVerificationFailed
495 .to_string()
496 .contains("verification")
497 );
498 assert!(
499 AptosError::InvalidTypeTag("bad tag".to_string())
500 .to_string()
501 .contains("type tag")
502 );
503 assert!(
504 AptosError::SimulationFailed("error".to_string())
505 .to_string()
506 .contains("Simulation")
507 );
508 assert!(
509 AptosError::SubmissionFailed("error".to_string())
510 .to_string()
511 .contains("Submission")
512 );
513 }
514
515 #[test]
516 fn test_execution_failed() {
517 let err = AptosError::ExecutionFailed {
518 vm_status: "ABORTED".to_string(),
519 };
520 assert!(err.to_string().contains("ABORTED"));
521 }
522
523 #[test]
524 fn test_rate_limited() {
525 let err = AptosError::RateLimited {
526 retry_after_secs: Some(30),
527 };
528 assert!(err.to_string().contains("Rate limited"));
529 }
530
531 #[test]
532 fn test_insufficient_signatures() {
533 let err = AptosError::InsufficientSignatures {
534 required: 3,
535 provided: 1,
536 };
537 assert!(err.to_string().contains('3'));
538 assert!(err.to_string().contains('1'));
539 }
540
541 #[test]
542 fn test_feature_not_enabled() {
543 let err = AptosError::FeatureNotEnabled("ed25519".to_string());
544 assert!(err.to_string().contains("ed25519"));
545 assert!(err.to_string().contains("Cargo.toml"));
546 }
547
548 #[test]
549 fn test_config_error() {
550 let err = AptosError::Config("invalid config".to_string());
551 assert!(err.to_string().contains("Configuration"));
552 }
553
554 #[test]
555 fn test_internal_error() {
556 let err = AptosError::Internal("bug".to_string());
557 assert!(err.to_string().contains("Internal"));
558 }
559
560 #[test]
561 fn test_invalid_mnemonic() {
562 let err = AptosError::InvalidMnemonic("bad phrase".to_string());
563 assert!(err.to_string().contains("mnemonic"));
564 }
565
566 #[test]
567 fn test_invalid_jwt() {
568 let err = AptosError::InvalidJwt("bad token".to_string());
569 assert!(err.to_string().contains("JWT"));
570 }
571
572 #[test]
573 fn test_key_derivation() {
574 let err = AptosError::KeyDerivation("failed".to_string());
575 assert!(err.to_string().contains("derivation"));
576 }
577
578 #[test]
579 fn test_sanitized_message_basic() {
580 let err = AptosError::api(400, "bad request");
581 let sanitized = err.sanitized_message();
582 assert!(sanitized.contains("bad request"));
583 }
584
585 #[test]
586 fn test_sanitized_message_truncates_long_messages() {
587 let long_message = "x".repeat(2000);
588 let err = AptosError::api(500, long_message);
589 let sanitized = err.sanitized_message();
590 assert!(sanitized.len() < 1200); assert!(sanitized.contains("truncated"));
592 }
593
594 #[test]
595 fn test_sanitized_message_removes_control_chars() {
596 let err = AptosError::api(400, "bad\x00request\x1f");
597 let sanitized = err.sanitized_message();
598 assert!(!sanitized.contains('\x00'));
599 assert!(!sanitized.contains('\x1f'));
600 }
601
602 #[test]
603 fn test_sanitized_message_redacts_sensitive_patterns() {
604 let err = AptosError::Internal("private_key: abc123".to_string());
605 let sanitized = err.sanitized_message();
606 assert!(sanitized.contains("REDACTED"));
607 assert!(!sanitized.contains("abc123"));
608
609 let err = AptosError::Internal("mnemonic phrase here".to_string());
610 let sanitized = err.sanitized_message();
611 assert!(sanitized.contains("REDACTED"));
612 }
613
614 #[test]
615 fn test_sanitized_message_redacts_url_with_query() {
616 let err = AptosError::api(
620 500,
621 "request to https://node.example.com/v1?trace=abc failed",
622 );
623 let sanitized = err.sanitized_message();
624 assert!(sanitized.contains("REDACTED"));
625 assert!(sanitized.contains("query parameters"));
626 assert!(!sanitized.contains("trace=abc"));
627 }
628
629 #[test]
630 fn test_sanitized_message_redacts_second_url_with_query() {
631 let msg = "tried https://a.example.com/ok then https://b.example.com/p?ref=xyz";
634 let err = AptosError::api(502, msg);
635 let sanitized = err.sanitized_message();
636 assert!(sanitized.contains("REDACTED"));
637 assert!(sanitized.contains("query parameters"));
638 assert!(!sanitized.contains("ref=xyz"));
639 }
640
641 #[test]
642 fn test_sanitized_message_keeps_url_without_query() {
643 let err = AptosError::api(500, "request to https://node.example.com/v1 failed");
645 let sanitized = err.sanitized_message();
646 assert!(sanitized.contains("node.example.com"));
647 assert!(!sanitized.contains("REDACTED"));
648 }
649
650 #[test]
651 fn test_user_message() {
652 assert_eq!(
653 AptosError::api(404, "not found").user_message(),
654 "Resource not found"
655 );
656 assert_eq!(
657 AptosError::api(429, "rate limited").user_message(),
658 "Rate limit exceeded"
659 );
660 assert_eq!(
661 AptosError::api(500, "internal error").user_message(),
662 "Server error"
663 );
664 assert_eq!(
665 AptosError::InvalidAddress("bad".to_string()).user_message(),
666 "Invalid account address"
667 );
668 }
669
670 #[test]
671 fn test_user_message_all_variants() {
672 assert_eq!(
674 AptosError::InvalidPublicKey("bad".to_string()).user_message(),
675 "Invalid public key"
676 );
677 assert_eq!(
678 AptosError::InvalidPrivateKey("bad".to_string()).user_message(),
679 "Invalid private key"
680 );
681 assert_eq!(
682 AptosError::InvalidSignature("bad".to_string()).user_message(),
683 "Invalid signature"
684 );
685 assert_eq!(
686 AptosError::SignatureVerificationFailed.user_message(),
687 "Signature verification failed"
688 );
689 assert_eq!(
690 AptosError::InvalidTypeTag("bad".to_string()).user_message(),
691 "Invalid type format"
692 );
693 assert_eq!(
694 AptosError::Transaction("bad".to_string()).user_message(),
695 "Transaction error"
696 );
697 assert_eq!(
698 AptosError::SimulationFailed("bad".to_string()).user_message(),
699 "Transaction simulation failed"
700 );
701 assert_eq!(
702 AptosError::SubmissionFailed("bad".to_string()).user_message(),
703 "Transaction submission failed"
704 );
705 assert_eq!(
706 AptosError::ExecutionFailed {
707 vm_status: "ABORTED".to_string()
708 }
709 .user_message(),
710 "Transaction execution failed"
711 );
712 assert_eq!(
713 AptosError::TransactionTimeout {
714 hash: "0x1".to_string(),
715 timeout_secs: 30
716 }
717 .user_message(),
718 "Transaction timed out"
719 );
720 assert_eq!(
721 AptosError::NotFound("x".to_string()).user_message(),
722 "Resource not found"
723 );
724 assert_eq!(
725 AptosError::RateLimited {
726 retry_after_secs: Some(30)
727 }
728 .user_message(),
729 "Rate limit exceeded"
730 );
731 assert_eq!(
732 AptosError::api(503, "unavailable").user_message(),
733 "Server error"
734 );
735 assert_eq!(
736 AptosError::api(400, "bad request").user_message(),
737 "API error"
738 );
739 assert_eq!(
740 AptosError::AccountNotFound("0x1".to_string()).user_message(),
741 "Account not found"
742 );
743 assert_eq!(
744 AptosError::InvalidMnemonic("bad".to_string()).user_message(),
745 "Invalid recovery phrase"
746 );
747 assert_eq!(
748 AptosError::InvalidJwt("bad".to_string()).user_message(),
749 "Invalid authentication token"
750 );
751 assert_eq!(
752 AptosError::KeyDerivation("bad".to_string()).user_message(),
753 "Key derivation failed"
754 );
755 assert_eq!(
756 AptosError::InsufficientSignatures {
757 required: 3,
758 provided: 1
759 }
760 .user_message(),
761 "Insufficient signatures"
762 );
763 assert_eq!(
764 AptosError::FeatureNotEnabled("ed25519".to_string()).user_message(),
765 "Feature not enabled"
766 );
767 assert_eq!(
768 AptosError::Config("bad".to_string()).user_message(),
769 "Configuration error"
770 );
771 assert_eq!(
772 AptosError::Internal("bug".to_string()).user_message(),
773 "Internal error"
774 );
775 assert_eq!(
776 AptosError::Other(anyhow::anyhow!("misc")).user_message(),
777 "An error occurred"
778 );
779 }
780
781 #[test]
782 fn test_is_retryable_http_errors() {
783 assert!(!AptosError::InvalidAddress("x".to_string()).is_retryable());
785 assert!(!AptosError::Transaction("x".to_string()).is_retryable());
786 assert!(!AptosError::NotFound("x".to_string()).is_retryable());
787 }
788}