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