1use std::fmt;
7use thiserror::Error;
8
9pub type AptosResult<T> = Result<T, AptosError>;
11
12#[derive(Error, Debug)]
17pub enum AptosError {
18 #[error("HTTP error: {0}")]
20 Http(#[from] reqwest::Error),
21
22 #[error("JSON error: {0}")]
24 Json(#[from] serde_json::Error),
25
26 #[error("BCS error: {0}")]
28 Bcs(String),
29
30 #[error("URL error: {0}")]
32 Url(#[from] url::ParseError),
33
34 #[error("Hex error: {0}")]
36 Hex(#[from] hex::FromHexError),
37
38 #[error("Invalid address: {0}")]
40 InvalidAddress(String),
41
42 #[error("Invalid public key: {0}")]
44 InvalidPublicKey(String),
45
46 #[error("Invalid private key: {0}")]
48 InvalidPrivateKey(String),
49
50 #[error("Invalid signature: {0}")]
52 InvalidSignature(String),
53
54 #[error("Signature verification failed")]
56 SignatureVerificationFailed,
57
58 #[error("Invalid type tag: {0}")]
60 InvalidTypeTag(String),
61
62 #[error("Transaction error: {0}")]
64 Transaction(String),
65
66 #[error("Simulation failed: {0}")]
68 SimulationFailed(String),
69
70 #[error("Submission failed: {0}")]
72 SubmissionFailed(String),
73
74 #[error("Execution failed: {vm_status}")]
76 ExecutionFailed {
77 vm_status: String,
79 },
80
81 #[error("Transaction timed out after {timeout_secs} seconds")]
83 TransactionTimeout {
84 hash: String,
86 timeout_secs: u64,
88 },
89
90 #[error("API error ({status_code}): {message}")]
92 Api {
93 status_code: u16,
95 message: String,
97 error_code: Option<String>,
99 vm_error_code: Option<u64>,
101 },
102
103 #[error("Rate limited: retry after {retry_after_secs:?} seconds")]
105 RateLimited {
106 retry_after_secs: Option<u64>,
108 },
109
110 #[error("Resource not found: {0}")]
112 NotFound(String),
113
114 #[error("Account not found: {0}")]
116 AccountNotFound(String),
117
118 #[error("Invalid mnemonic: {0}")]
120 InvalidMnemonic(String),
121
122 #[error("Invalid JWT: {0}")]
124 InvalidJwt(String),
125
126 #[error("Key derivation error: {0}")]
128 KeyDerivation(String),
129
130 #[error("Insufficient signatures: need {required}, got {provided}")]
132 InsufficientSignatures {
133 required: usize,
135 provided: usize,
137 },
138
139 #[error("Feature not enabled: {0}. Enable the '{0}' feature in Cargo.toml")]
141 FeatureNotEnabled(String),
142
143 #[error("Configuration error: {0}")]
145 Config(String),
146
147 #[error("Internal error: {0}")]
149 Internal(String),
150
151 #[error("{0}")]
153 Other(#[from] anyhow::Error),
154}
155
156const MAX_ERROR_MESSAGE_LENGTH: usize = 1000;
158
159const SENSITIVE_PATTERNS: &[&str] = &[
161 "private_key",
162 "secret",
163 "password",
164 "mnemonic",
165 "seed",
166 "bearer",
167 "authorization",
168];
169
170impl AptosError {
171 pub fn bcs<E: fmt::Display>(err: E) -> Self {
173 Self::Bcs(err.to_string())
174 }
175
176 pub fn transaction<S: Into<String>>(msg: S) -> Self {
178 Self::Transaction(msg.into())
179 }
180
181 pub fn api(status_code: u16, message: impl Into<String>) -> Self {
183 Self::Api {
184 status_code,
185 message: message.into(),
186 error_code: None,
187 vm_error_code: None,
188 }
189 }
190
191 pub fn api_with_details(
193 status_code: u16,
194 message: impl Into<String>,
195 error_code: Option<String>,
196 vm_error_code: Option<u64>,
197 ) -> Self {
198 Self::Api {
199 status_code,
200 message: message.into(),
201 error_code,
202 vm_error_code,
203 }
204 }
205
206 pub fn is_not_found(&self) -> bool {
208 matches!(
209 self,
210 Self::NotFound(_)
211 | Self::AccountNotFound(_)
212 | Self::Api {
213 status_code: 404,
214 ..
215 }
216 )
217 }
218
219 pub fn is_timeout(&self) -> bool {
221 matches!(self, Self::TransactionTimeout { .. })
222 }
223
224 pub fn is_retryable(&self) -> bool {
226 match self {
227 Self::Http(e) => e.is_timeout() || e.is_connect(),
228 Self::Api { status_code, .. } => {
229 matches!(status_code, 429 | 500 | 502 | 503 | 504)
230 }
231 Self::RateLimited { .. } => true,
232 _ => false,
233 }
234 }
235
236 pub fn sanitized_message(&self) -> String {
253 let raw_message = self.to_string();
254 Self::sanitize_string(&raw_message)
255 }
256
257 fn sanitize_string(s: &str) -> String {
259 let cleaned: String = s
261 .chars()
262 .filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
263 .collect();
264
265 let lower = cleaned.to_lowercase();
267 for pattern in SENSITIVE_PATTERNS {
268 if lower.contains(pattern) {
269 return format!("[REDACTED: message contained sensitive pattern '{pattern}']");
270 }
271 }
272
273 if cleaned.len() > MAX_ERROR_MESSAGE_LENGTH {
275 format!(
276 "{}... [truncated, total length: {}]",
277 &cleaned[..MAX_ERROR_MESSAGE_LENGTH],
278 cleaned.len()
279 )
280 } else {
281 cleaned
282 }
283 }
284
285 pub fn user_message(&self) -> &'static str {
290 match self {
291 Self::Http(_) => "Network error occurred",
292 Self::Json(_) => "Failed to process response",
293 Self::Bcs(_) => "Failed to process data",
294 Self::Url(_) => "Invalid URL",
295 Self::Hex(_) => "Invalid hex format",
296 Self::InvalidAddress(_) => "Invalid account address",
297 Self::InvalidPublicKey(_) => "Invalid public key",
298 Self::InvalidPrivateKey(_) => "Invalid private key",
299 Self::InvalidSignature(_) => "Invalid signature",
300 Self::SignatureVerificationFailed => "Signature verification failed",
301 Self::InvalidTypeTag(_) => "Invalid type format",
302 Self::Transaction(_) => "Transaction error",
303 Self::SimulationFailed(_) => "Transaction simulation failed",
304 Self::SubmissionFailed(_) => "Transaction submission failed",
305 Self::ExecutionFailed { .. } => "Transaction execution failed",
306 Self::TransactionTimeout { .. } => "Transaction timed out",
307 Self::NotFound(_)
308 | Self::Api {
309 status_code: 404, ..
310 } => "Resource not found",
311 Self::RateLimited { .. }
312 | Self::Api {
313 status_code: 429, ..
314 } => "Rate limit exceeded",
315 Self::Api { status_code, .. } if *status_code >= 500 => "Server error",
316 Self::Api { .. } => "API error",
317 Self::AccountNotFound(_) => "Account not found",
318 Self::InvalidMnemonic(_) => "Invalid recovery phrase",
319 Self::InvalidJwt(_) => "Invalid authentication token",
320 Self::KeyDerivation(_) => "Key derivation failed",
321 Self::InsufficientSignatures { .. } => "Insufficient signatures",
322 Self::FeatureNotEnabled(_) => "Feature not enabled",
323 Self::Config(_) => "Configuration error",
324 Self::Internal(_) => "Internal error",
325 Self::Other(_) => "An error occurred",
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_error_display() {
336 let err = AptosError::InvalidAddress("bad address".to_string());
337 assert_eq!(err.to_string(), "Invalid address: bad address");
338 }
339
340 #[test]
341 fn test_is_not_found() {
342 assert!(AptosError::NotFound("test".to_string()).is_not_found());
343 assert!(AptosError::AccountNotFound("0x1".to_string()).is_not_found());
344 assert!(AptosError::api(404, "not found").is_not_found());
345 assert!(!AptosError::api(500, "server error").is_not_found());
346 }
347
348 #[test]
349 fn test_is_retryable() {
350 assert!(AptosError::api(429, "rate limited").is_retryable());
351 assert!(AptosError::api(503, "unavailable").is_retryable());
352 assert!(AptosError::api(500, "internal error").is_retryable());
353 assert!(AptosError::api(502, "bad gateway").is_retryable());
354 assert!(AptosError::api(504, "timeout").is_retryable());
355 assert!(!AptosError::api(400, "bad request").is_retryable());
356 }
357
358 #[test]
359 fn test_is_timeout() {
360 let err = AptosError::TransactionTimeout {
361 hash: "0x123".to_string(),
362 timeout_secs: 30,
363 };
364 assert!(err.is_timeout());
365 assert!(!AptosError::InvalidAddress("test".to_string()).is_timeout());
366 }
367
368 #[test]
369 fn test_bcs_error() {
370 let err = AptosError::bcs("serialization failed");
371 assert!(matches!(err, AptosError::Bcs(_)));
372 assert!(err.to_string().contains("serialization failed"));
373 }
374
375 #[test]
376 fn test_transaction_error() {
377 let err = AptosError::transaction("invalid payload");
378 assert!(matches!(err, AptosError::Transaction(_)));
379 assert!(err.to_string().contains("invalid payload"));
380 }
381
382 #[test]
383 fn test_api_error() {
384 let err = AptosError::api(400, "bad request");
385 assert!(err.to_string().contains("400"));
386 assert!(err.to_string().contains("bad request"));
387 }
388
389 #[test]
390 fn test_api_error_with_details() {
391 let err = AptosError::api_with_details(
392 400,
393 "invalid argument",
394 Some("INVALID_ARGUMENT".to_string()),
395 Some(42),
396 );
397 if let AptosError::Api {
398 status_code,
399 message,
400 error_code,
401 vm_error_code,
402 } = err
403 {
404 assert_eq!(status_code, 400);
405 assert_eq!(message, "invalid argument");
406 assert_eq!(error_code, Some("INVALID_ARGUMENT".to_string()));
407 assert_eq!(vm_error_code, Some(42));
408 } else {
409 panic!("Expected Api error variant");
410 }
411 }
412
413 #[test]
414 fn test_various_error_displays() {
415 assert!(
416 AptosError::InvalidPublicKey("bad key".to_string())
417 .to_string()
418 .contains("public key")
419 );
420 assert!(
421 AptosError::InvalidPrivateKey("bad key".to_string())
422 .to_string()
423 .contains("private key")
424 );
425 assert!(
426 AptosError::InvalidSignature("bad sig".to_string())
427 .to_string()
428 .contains("signature")
429 );
430 assert!(
431 AptosError::SignatureVerificationFailed
432 .to_string()
433 .contains("verification")
434 );
435 assert!(
436 AptosError::InvalidTypeTag("bad tag".to_string())
437 .to_string()
438 .contains("type tag")
439 );
440 assert!(
441 AptosError::SimulationFailed("error".to_string())
442 .to_string()
443 .contains("Simulation")
444 );
445 assert!(
446 AptosError::SubmissionFailed("error".to_string())
447 .to_string()
448 .contains("Submission")
449 );
450 }
451
452 #[test]
453 fn test_execution_failed() {
454 let err = AptosError::ExecutionFailed {
455 vm_status: "ABORTED".to_string(),
456 };
457 assert!(err.to_string().contains("ABORTED"));
458 }
459
460 #[test]
461 fn test_rate_limited() {
462 let err = AptosError::RateLimited {
463 retry_after_secs: Some(30),
464 };
465 assert!(err.to_string().contains("Rate limited"));
466 }
467
468 #[test]
469 fn test_insufficient_signatures() {
470 let err = AptosError::InsufficientSignatures {
471 required: 3,
472 provided: 1,
473 };
474 assert!(err.to_string().contains('3'));
475 assert!(err.to_string().contains('1'));
476 }
477
478 #[test]
479 fn test_feature_not_enabled() {
480 let err = AptosError::FeatureNotEnabled("ed25519".to_string());
481 assert!(err.to_string().contains("ed25519"));
482 assert!(err.to_string().contains("Cargo.toml"));
483 }
484
485 #[test]
486 fn test_config_error() {
487 let err = AptosError::Config("invalid config".to_string());
488 assert!(err.to_string().contains("Configuration"));
489 }
490
491 #[test]
492 fn test_internal_error() {
493 let err = AptosError::Internal("bug".to_string());
494 assert!(err.to_string().contains("Internal"));
495 }
496
497 #[test]
498 fn test_invalid_mnemonic() {
499 let err = AptosError::InvalidMnemonic("bad phrase".to_string());
500 assert!(err.to_string().contains("mnemonic"));
501 }
502
503 #[test]
504 fn test_invalid_jwt() {
505 let err = AptosError::InvalidJwt("bad token".to_string());
506 assert!(err.to_string().contains("JWT"));
507 }
508
509 #[test]
510 fn test_key_derivation() {
511 let err = AptosError::KeyDerivation("failed".to_string());
512 assert!(err.to_string().contains("derivation"));
513 }
514
515 #[test]
516 fn test_sanitized_message_basic() {
517 let err = AptosError::api(400, "bad request");
518 let sanitized = err.sanitized_message();
519 assert!(sanitized.contains("bad request"));
520 }
521
522 #[test]
523 fn test_sanitized_message_truncates_long_messages() {
524 let long_message = "x".repeat(2000);
525 let err = AptosError::api(500, long_message);
526 let sanitized = err.sanitized_message();
527 assert!(sanitized.len() < 1200); assert!(sanitized.contains("truncated"));
529 }
530
531 #[test]
532 fn test_sanitized_message_removes_control_chars() {
533 let err = AptosError::api(400, "bad\x00request\x1f");
534 let sanitized = err.sanitized_message();
535 assert!(!sanitized.contains('\x00'));
536 assert!(!sanitized.contains('\x1f'));
537 }
538
539 #[test]
540 fn test_sanitized_message_redacts_sensitive_patterns() {
541 let err = AptosError::Internal("private_key: abc123".to_string());
542 let sanitized = err.sanitized_message();
543 assert!(sanitized.contains("REDACTED"));
544 assert!(!sanitized.contains("abc123"));
545
546 let err = AptosError::Internal("mnemonic phrase here".to_string());
547 let sanitized = err.sanitized_message();
548 assert!(sanitized.contains("REDACTED"));
549 }
550
551 #[test]
552 fn test_user_message() {
553 assert_eq!(
554 AptosError::api(404, "not found").user_message(),
555 "Resource not found"
556 );
557 assert_eq!(
558 AptosError::api(429, "rate limited").user_message(),
559 "Rate limit exceeded"
560 );
561 assert_eq!(
562 AptosError::api(500, "internal error").user_message(),
563 "Server error"
564 );
565 assert_eq!(
566 AptosError::InvalidAddress("bad".to_string()).user_message(),
567 "Invalid account address"
568 );
569 }
570
571 #[test]
572 fn test_user_message_all_variants() {
573 assert_eq!(
575 AptosError::InvalidPublicKey("bad".to_string()).user_message(),
576 "Invalid public key"
577 );
578 assert_eq!(
579 AptosError::InvalidPrivateKey("bad".to_string()).user_message(),
580 "Invalid private key"
581 );
582 assert_eq!(
583 AptosError::InvalidSignature("bad".to_string()).user_message(),
584 "Invalid signature"
585 );
586 assert_eq!(
587 AptosError::SignatureVerificationFailed.user_message(),
588 "Signature verification failed"
589 );
590 assert_eq!(
591 AptosError::InvalidTypeTag("bad".to_string()).user_message(),
592 "Invalid type format"
593 );
594 assert_eq!(
595 AptosError::Transaction("bad".to_string()).user_message(),
596 "Transaction error"
597 );
598 assert_eq!(
599 AptosError::SimulationFailed("bad".to_string()).user_message(),
600 "Transaction simulation failed"
601 );
602 assert_eq!(
603 AptosError::SubmissionFailed("bad".to_string()).user_message(),
604 "Transaction submission failed"
605 );
606 assert_eq!(
607 AptosError::ExecutionFailed {
608 vm_status: "ABORTED".to_string()
609 }
610 .user_message(),
611 "Transaction execution failed"
612 );
613 assert_eq!(
614 AptosError::TransactionTimeout {
615 hash: "0x1".to_string(),
616 timeout_secs: 30
617 }
618 .user_message(),
619 "Transaction timed out"
620 );
621 assert_eq!(
622 AptosError::NotFound("x".to_string()).user_message(),
623 "Resource not found"
624 );
625 assert_eq!(
626 AptosError::RateLimited {
627 retry_after_secs: Some(30)
628 }
629 .user_message(),
630 "Rate limit exceeded"
631 );
632 assert_eq!(
633 AptosError::api(503, "unavailable").user_message(),
634 "Server error"
635 );
636 assert_eq!(
637 AptosError::api(400, "bad request").user_message(),
638 "API error"
639 );
640 assert_eq!(
641 AptosError::AccountNotFound("0x1".to_string()).user_message(),
642 "Account not found"
643 );
644 assert_eq!(
645 AptosError::InvalidMnemonic("bad".to_string()).user_message(),
646 "Invalid recovery phrase"
647 );
648 assert_eq!(
649 AptosError::InvalidJwt("bad".to_string()).user_message(),
650 "Invalid authentication token"
651 );
652 assert_eq!(
653 AptosError::KeyDerivation("bad".to_string()).user_message(),
654 "Key derivation failed"
655 );
656 assert_eq!(
657 AptosError::InsufficientSignatures {
658 required: 3,
659 provided: 1
660 }
661 .user_message(),
662 "Insufficient signatures"
663 );
664 assert_eq!(
665 AptosError::FeatureNotEnabled("ed25519".to_string()).user_message(),
666 "Feature not enabled"
667 );
668 assert_eq!(
669 AptosError::Config("bad".to_string()).user_message(),
670 "Configuration error"
671 );
672 assert_eq!(
673 AptosError::Internal("bug".to_string()).user_message(),
674 "Internal error"
675 );
676 assert_eq!(
677 AptosError::Other(anyhow::anyhow!("misc")).user_message(),
678 "An error occurred"
679 );
680 }
681
682 #[test]
683 fn test_is_retryable_http_errors() {
684 assert!(!AptosError::InvalidAddress("x".to_string()).is_retryable());
686 assert!(!AptosError::Transaction("x".to_string()).is_retryable());
687 assert!(!AptosError::NotFound("x".to_string()).is_retryable());
688 }
689}