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) => e.is_timeout() || e.is_connect(),
256 Self::Api { status_code, .. } => {
257 matches!(status_code, 429 | 500 | 502 | 503 | 504)
258 }
259 Self::RateLimited { .. } => true,
260 _ => false,
261 }
262 }
263
264 pub fn sanitized_message(&self) -> String {
281 let raw_message = self.to_string();
282 Self::sanitize_string(&raw_message)
283 }
284
285 fn sanitize_string(s: &str) -> String {
287 let cleaned: String = s
289 .chars()
290 .filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
291 .collect();
292
293 let lower = cleaned.to_lowercase();
295 for pattern in SENSITIVE_PATTERNS {
296 if lower.contains(pattern) {
297 return format!("[REDACTED: message contained sensitive pattern '{pattern}']");
298 }
299 }
300
301 for scheme in ["http://", "https://"] {
309 let mut search_from = 0;
310 while let Some(rel_pos) = lower[search_from..].find(scheme) {
311 let url_start = search_from + rel_pos;
312 let url_rest = &lower[url_start..];
315 let url_end = url_rest
316 .find(|c: char| c.is_whitespace() || c == '>' || c == '"' || c == '\'')
317 .unwrap_or(url_rest.len());
318 let url_token = &url_rest[..url_end];
319 if url_token.contains('?') {
320 return "[REDACTED: message contained URL with query parameters]".into();
321 }
322 search_from = url_start + scheme.len();
324 }
325 }
326
327 if cleaned.len() > MAX_ERROR_MESSAGE_LENGTH {
329 let mut end = MAX_ERROR_MESSAGE_LENGTH;
330 while end > 0 && !cleaned.is_char_boundary(end) {
331 end -= 1;
332 }
333 format!(
334 "{}... [truncated, total length: {}]",
335 &cleaned[..end],
336 cleaned.len()
337 )
338 } else {
339 cleaned
340 }
341 }
342
343 pub fn user_message(&self) -> &'static str {
348 match self {
349 Self::Http(_) => "Network error occurred",
350 Self::Json(_) => "Failed to process response",
351 Self::Bcs(_) => "Failed to process data",
352 Self::Url(_) => "Invalid URL",
353 Self::Hex(_) => "Invalid hex format",
354 Self::InvalidAddress(_) => "Invalid account address",
355 Self::InvalidPublicKey(_) => "Invalid public key",
356 Self::InvalidPrivateKey(_) => "Invalid private key",
357 Self::InvalidSignature(_) => "Invalid signature",
358 Self::SignatureVerificationFailed => "Signature verification failed",
359 Self::InvalidTypeTag(_) => "Invalid type format",
360 Self::Transaction(_) => "Transaction error",
361 Self::SimulationFailed(_) => "Transaction simulation failed",
362 Self::SubmissionFailed(_) => "Transaction submission failed",
363 Self::ExecutionFailed { .. } => "Transaction execution failed",
364 Self::TransactionTimeout { .. } => "Transaction timed out",
365 Self::NotFound(_)
366 | Self::Api {
367 status_code: 404, ..
368 } => "Resource not found",
369 Self::RateLimited { .. }
370 | Self::Api {
371 status_code: 429, ..
372 } => "Rate limit exceeded",
373 Self::Api { status_code, .. } if *status_code >= 500 => "Server error",
374 Self::Api { .. } => "API error",
375 Self::AccountNotFound(_) => "Account not found",
376 Self::InvalidMnemonic(_) => "Invalid recovery phrase",
377 Self::InvalidJwt(_) => "Invalid authentication token",
378 Self::KeyDerivation(_) => "Key derivation failed",
379 Self::InsufficientSignatures { .. } => "Insufficient signatures",
380 Self::FeatureNotEnabled(_) => "Feature not enabled",
381 Self::Config(_) => "Configuration error",
382 Self::Internal(_) => "Internal error",
383 Self::Other(_) => "An error occurred",
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_error_display() {
394 let err = AptosError::InvalidAddress("bad address".to_string());
395 assert_eq!(err.to_string(), "Invalid address: bad address");
396 }
397
398 #[test]
399 fn test_is_not_found() {
400 assert!(AptosError::NotFound("test".to_string()).is_not_found());
401 assert!(AptosError::AccountNotFound("0x1".to_string()).is_not_found());
402 assert!(AptosError::api(404, "not found").is_not_found());
403 assert!(!AptosError::api(500, "server error").is_not_found());
404 }
405
406 #[test]
407 fn test_is_retryable() {
408 assert!(AptosError::api(429, "rate limited").is_retryable());
409 assert!(AptosError::api(503, "unavailable").is_retryable());
410 assert!(AptosError::api(500, "internal error").is_retryable());
411 assert!(AptosError::api(502, "bad gateway").is_retryable());
412 assert!(AptosError::api(504, "timeout").is_retryable());
413 assert!(!AptosError::api(400, "bad request").is_retryable());
414 }
415
416 #[test]
417 fn test_is_timeout() {
418 let err = AptosError::TransactionTimeout {
419 hash: "0x123".to_string(),
420 timeout_secs: 30,
421 };
422 assert!(err.is_timeout());
423 assert!(!AptosError::InvalidAddress("test".to_string()).is_timeout());
424 }
425
426 #[test]
427 fn test_bcs_error() {
428 let err = AptosError::bcs("serialization failed");
429 assert!(matches!(err, AptosError::Bcs(_)));
430 assert!(err.to_string().contains("serialization failed"));
431 }
432
433 #[test]
434 fn test_transaction_error() {
435 let err = AptosError::transaction("invalid payload");
436 assert!(matches!(err, AptosError::Transaction(_)));
437 assert!(err.to_string().contains("invalid payload"));
438 }
439
440 #[test]
441 fn test_api_error() {
442 let err = AptosError::api(400, "bad request");
443 assert!(err.to_string().contains("400"));
444 assert!(err.to_string().contains("bad request"));
445 }
446
447 #[test]
448 fn test_api_error_with_details() {
449 let err = AptosError::api_with_details(
450 400,
451 "invalid argument",
452 Some("INVALID_ARGUMENT".to_string()),
453 Some(42),
454 );
455 if let AptosError::Api {
456 status_code,
457 message,
458 error_code,
459 vm_error_code,
460 } = err
461 {
462 assert_eq!(status_code, 400);
463 assert_eq!(message, "invalid argument");
464 assert_eq!(error_code, Some("INVALID_ARGUMENT".to_string()));
465 assert_eq!(vm_error_code, Some(42));
466 } else {
467 panic!("Expected Api error variant");
468 }
469 }
470
471 #[test]
472 fn test_various_error_displays() {
473 assert!(
474 AptosError::InvalidPublicKey("bad key".to_string())
475 .to_string()
476 .contains("public key")
477 );
478 assert!(
479 AptosError::InvalidPrivateKey("bad key".to_string())
480 .to_string()
481 .contains("private key")
482 );
483 assert!(
484 AptosError::InvalidSignature("bad sig".to_string())
485 .to_string()
486 .contains("signature")
487 );
488 assert!(
489 AptosError::SignatureVerificationFailed
490 .to_string()
491 .contains("verification")
492 );
493 assert!(
494 AptosError::InvalidTypeTag("bad tag".to_string())
495 .to_string()
496 .contains("type tag")
497 );
498 assert!(
499 AptosError::SimulationFailed("error".to_string())
500 .to_string()
501 .contains("Simulation")
502 );
503 assert!(
504 AptosError::SubmissionFailed("error".to_string())
505 .to_string()
506 .contains("Submission")
507 );
508 }
509
510 #[test]
511 fn test_execution_failed() {
512 let err = AptosError::ExecutionFailed {
513 vm_status: "ABORTED".to_string(),
514 };
515 assert!(err.to_string().contains("ABORTED"));
516 }
517
518 #[test]
519 fn test_rate_limited() {
520 let err = AptosError::RateLimited {
521 retry_after_secs: Some(30),
522 };
523 assert!(err.to_string().contains("Rate limited"));
524 }
525
526 #[test]
527 fn test_insufficient_signatures() {
528 let err = AptosError::InsufficientSignatures {
529 required: 3,
530 provided: 1,
531 };
532 assert!(err.to_string().contains('3'));
533 assert!(err.to_string().contains('1'));
534 }
535
536 #[test]
537 fn test_feature_not_enabled() {
538 let err = AptosError::FeatureNotEnabled("ed25519".to_string());
539 assert!(err.to_string().contains("ed25519"));
540 assert!(err.to_string().contains("Cargo.toml"));
541 }
542
543 #[test]
544 fn test_config_error() {
545 let err = AptosError::Config("invalid config".to_string());
546 assert!(err.to_string().contains("Configuration"));
547 }
548
549 #[test]
550 fn test_internal_error() {
551 let err = AptosError::Internal("bug".to_string());
552 assert!(err.to_string().contains("Internal"));
553 }
554
555 #[test]
556 fn test_invalid_mnemonic() {
557 let err = AptosError::InvalidMnemonic("bad phrase".to_string());
558 assert!(err.to_string().contains("mnemonic"));
559 }
560
561 #[test]
562 fn test_invalid_jwt() {
563 let err = AptosError::InvalidJwt("bad token".to_string());
564 assert!(err.to_string().contains("JWT"));
565 }
566
567 #[test]
568 fn test_key_derivation() {
569 let err = AptosError::KeyDerivation("failed".to_string());
570 assert!(err.to_string().contains("derivation"));
571 }
572
573 #[test]
574 fn test_sanitized_message_basic() {
575 let err = AptosError::api(400, "bad request");
576 let sanitized = err.sanitized_message();
577 assert!(sanitized.contains("bad request"));
578 }
579
580 #[test]
581 fn test_sanitized_message_truncates_long_messages() {
582 let long_message = "x".repeat(2000);
583 let err = AptosError::api(500, long_message);
584 let sanitized = err.sanitized_message();
585 assert!(sanitized.len() < 1200); assert!(sanitized.contains("truncated"));
587 }
588
589 #[test]
590 fn test_sanitized_message_removes_control_chars() {
591 let err = AptosError::api(400, "bad\x00request\x1f");
592 let sanitized = err.sanitized_message();
593 assert!(!sanitized.contains('\x00'));
594 assert!(!sanitized.contains('\x1f'));
595 }
596
597 #[test]
598 fn test_sanitized_message_redacts_sensitive_patterns() {
599 let err = AptosError::Internal("private_key: abc123".to_string());
600 let sanitized = err.sanitized_message();
601 assert!(sanitized.contains("REDACTED"));
602 assert!(!sanitized.contains("abc123"));
603
604 let err = AptosError::Internal("mnemonic phrase here".to_string());
605 let sanitized = err.sanitized_message();
606 assert!(sanitized.contains("REDACTED"));
607 }
608
609 #[test]
610 fn test_sanitized_message_redacts_url_with_query() {
611 let err = AptosError::api(
615 500,
616 "request to https://node.example.com/v1?trace=abc failed",
617 );
618 let sanitized = err.sanitized_message();
619 assert!(sanitized.contains("REDACTED"));
620 assert!(sanitized.contains("query parameters"));
621 assert!(!sanitized.contains("trace=abc"));
622 }
623
624 #[test]
625 fn test_sanitized_message_redacts_second_url_with_query() {
626 let msg = "tried https://a.example.com/ok then https://b.example.com/p?ref=xyz";
629 let err = AptosError::api(502, msg);
630 let sanitized = err.sanitized_message();
631 assert!(sanitized.contains("REDACTED"));
632 assert!(sanitized.contains("query parameters"));
633 assert!(!sanitized.contains("ref=xyz"));
634 }
635
636 #[test]
637 fn test_sanitized_message_keeps_url_without_query() {
638 let err = AptosError::api(500, "request to https://node.example.com/v1 failed");
640 let sanitized = err.sanitized_message();
641 assert!(sanitized.contains("node.example.com"));
642 assert!(!sanitized.contains("REDACTED"));
643 }
644
645 #[test]
646 fn test_user_message() {
647 assert_eq!(
648 AptosError::api(404, "not found").user_message(),
649 "Resource not found"
650 );
651 assert_eq!(
652 AptosError::api(429, "rate limited").user_message(),
653 "Rate limit exceeded"
654 );
655 assert_eq!(
656 AptosError::api(500, "internal error").user_message(),
657 "Server error"
658 );
659 assert_eq!(
660 AptosError::InvalidAddress("bad".to_string()).user_message(),
661 "Invalid account address"
662 );
663 }
664
665 #[test]
666 fn test_user_message_all_variants() {
667 assert_eq!(
669 AptosError::InvalidPublicKey("bad".to_string()).user_message(),
670 "Invalid public key"
671 );
672 assert_eq!(
673 AptosError::InvalidPrivateKey("bad".to_string()).user_message(),
674 "Invalid private key"
675 );
676 assert_eq!(
677 AptosError::InvalidSignature("bad".to_string()).user_message(),
678 "Invalid signature"
679 );
680 assert_eq!(
681 AptosError::SignatureVerificationFailed.user_message(),
682 "Signature verification failed"
683 );
684 assert_eq!(
685 AptosError::InvalidTypeTag("bad".to_string()).user_message(),
686 "Invalid type format"
687 );
688 assert_eq!(
689 AptosError::Transaction("bad".to_string()).user_message(),
690 "Transaction error"
691 );
692 assert_eq!(
693 AptosError::SimulationFailed("bad".to_string()).user_message(),
694 "Transaction simulation failed"
695 );
696 assert_eq!(
697 AptosError::SubmissionFailed("bad".to_string()).user_message(),
698 "Transaction submission failed"
699 );
700 assert_eq!(
701 AptosError::ExecutionFailed {
702 vm_status: "ABORTED".to_string()
703 }
704 .user_message(),
705 "Transaction execution failed"
706 );
707 assert_eq!(
708 AptosError::TransactionTimeout {
709 hash: "0x1".to_string(),
710 timeout_secs: 30
711 }
712 .user_message(),
713 "Transaction timed out"
714 );
715 assert_eq!(
716 AptosError::NotFound("x".to_string()).user_message(),
717 "Resource not found"
718 );
719 assert_eq!(
720 AptosError::RateLimited {
721 retry_after_secs: Some(30)
722 }
723 .user_message(),
724 "Rate limit exceeded"
725 );
726 assert_eq!(
727 AptosError::api(503, "unavailable").user_message(),
728 "Server error"
729 );
730 assert_eq!(
731 AptosError::api(400, "bad request").user_message(),
732 "API error"
733 );
734 assert_eq!(
735 AptosError::AccountNotFound("0x1".to_string()).user_message(),
736 "Account not found"
737 );
738 assert_eq!(
739 AptosError::InvalidMnemonic("bad".to_string()).user_message(),
740 "Invalid recovery phrase"
741 );
742 assert_eq!(
743 AptosError::InvalidJwt("bad".to_string()).user_message(),
744 "Invalid authentication token"
745 );
746 assert_eq!(
747 AptosError::KeyDerivation("bad".to_string()).user_message(),
748 "Key derivation failed"
749 );
750 assert_eq!(
751 AptosError::InsufficientSignatures {
752 required: 3,
753 provided: 1
754 }
755 .user_message(),
756 "Insufficient signatures"
757 );
758 assert_eq!(
759 AptosError::FeatureNotEnabled("ed25519".to_string()).user_message(),
760 "Feature not enabled"
761 );
762 assert_eq!(
763 AptosError::Config("bad".to_string()).user_message(),
764 "Configuration error"
765 );
766 assert_eq!(
767 AptosError::Internal("bug".to_string()).user_message(),
768 "Internal error"
769 );
770 assert_eq!(
771 AptosError::Other(anyhow::anyhow!("misc")).user_message(),
772 "An error occurred"
773 );
774 }
775
776 #[test]
777 fn test_is_retryable_http_errors() {
778 assert!(!AptosError::InvalidAddress("x".to_string()).is_retryable());
780 assert!(!AptosError::Transaction("x".to_string()).is_retryable());
781 assert!(!AptosError::NotFound("x".to_string()).is_retryable());
782 }
783}