Skip to main content

aptos_sdk/
error.rs

1//! Error types for the Aptos SDK.
2//!
3//! This module provides a unified error type [`AptosError`] that encompasses
4//! all possible errors that can occur when using the SDK.
5
6use std::fmt;
7use thiserror::Error;
8
9/// A specialized Result type for Aptos SDK operations.
10pub type AptosResult<T> = Result<T, AptosError>;
11
12/// The main error type for the Aptos SDK.
13///
14/// This enum covers all possible error conditions that can occur when
15/// interacting with the Aptos blockchain through this SDK.
16#[derive(Error, Debug)]
17pub enum AptosError {
18    /// Error occurred during HTTP communication
19    #[error("HTTP error: {0}")]
20    Http(#[from] reqwest::Error),
21
22    /// Error occurred during JSON serialization/deserialization
23    #[error("JSON error: {0}")]
24    Json(#[from] serde_json::Error),
25
26    /// Error occurred during BCS serialization/deserialization
27    #[error("BCS error: {0}")]
28    Bcs(String),
29
30    /// Error occurred during URL parsing
31    #[error("URL error: {0}")]
32    Url(#[from] url::ParseError),
33
34    /// Error occurred during hex encoding/decoding
35    #[error("Hex error: {0}")]
36    Hex(#[from] hex::FromHexError),
37
38    /// Invalid account address
39    #[error("Invalid address: {0}")]
40    InvalidAddress(String),
41
42    /// Invalid public key
43    #[error("Invalid public key: {0}")]
44    InvalidPublicKey(String),
45
46    /// Invalid private key
47    #[error("Invalid private key: {0}")]
48    InvalidPrivateKey(String),
49
50    /// Invalid signature
51    #[error("Invalid signature: {0}")]
52    InvalidSignature(String),
53
54    /// Signature verification failed
55    #[error("Signature verification failed")]
56    SignatureVerificationFailed,
57
58    /// Invalid type tag format
59    #[error("Invalid type tag: {0}")]
60    InvalidTypeTag(String),
61
62    /// Transaction building error
63    #[error("Transaction error: {0}")]
64    Transaction(String),
65
66    /// Transaction simulation failed
67    #[error("Simulation failed: {0}")]
68    SimulationFailed(String),
69
70    /// Transaction submission failed
71    #[error("Submission failed: {0}")]
72    SubmissionFailed(String),
73
74    /// Transaction execution failed on chain
75    #[error("Execution failed: {vm_status}")]
76    ExecutionFailed {
77        /// The VM status message explaining the failure
78        vm_status: String,
79    },
80
81    /// Transaction timed out waiting for confirmation
82    #[error("Transaction timed out after {timeout_secs} seconds")]
83    TransactionTimeout {
84        /// The hash of the transaction that timed out
85        hash: String,
86        /// How long we waited before timing out
87        timeout_secs: u64,
88    },
89
90    /// API returned an error response
91    #[error("API error ({status_code}): {message}")]
92    Api {
93        /// HTTP status code
94        status_code: u16,
95        /// Error message from the API
96        message: String,
97        /// Optional error code from the API
98        error_code: Option<String>,
99        /// Optional VM error code
100        vm_error_code: Option<u64>,
101    },
102
103    /// Rate limited by the API
104    #[error("Rate limited: retry after {retry_after_secs:?} seconds")]
105    RateLimited {
106        /// How long to wait before retrying (if provided)
107        retry_after_secs: Option<u64>,
108    },
109
110    /// Resource not found
111    #[error("Resource not found: {0}")]
112    NotFound(String),
113
114    /// Account not found
115    #[error("Account not found: {0}")]
116    AccountNotFound(String),
117
118    /// Invalid mnemonic phrase
119    #[error("Invalid mnemonic: {0}")]
120    InvalidMnemonic(String),
121
122    /// Invalid JWT
123    #[error("Invalid JWT: {0}")]
124    InvalidJwt(String),
125
126    /// Key derivation error
127    #[error("Key derivation error: {0}")]
128    KeyDerivation(String),
129
130    /// Insufficient signatures for multi-signature operation
131    #[error("Insufficient signatures: need {required}, got {provided}")]
132    InsufficientSignatures {
133        /// Number of signatures required
134        required: usize,
135        /// Number of signatures provided
136        provided: usize,
137    },
138
139    /// Feature not enabled
140    #[error("Feature not enabled: {0}. Enable the '{0}' feature in Cargo.toml")]
141    FeatureNotEnabled(String),
142
143    /// Configuration error
144    #[error("Configuration error: {0}")]
145    Config(String),
146
147    /// Internal SDK error (should not happen)
148    #[error("Internal error: {0}")]
149    Internal(String),
150
151    /// Any other error
152    #[error("{0}")]
153    Other(#[from] anyhow::Error),
154}
155
156/// Maximum length for error messages to prevent excessive memory usage in logs.
157const MAX_ERROR_MESSAGE_LENGTH: usize = 1000;
158
159/// Patterns that might indicate sensitive information in error messages.
160const SENSITIVE_PATTERNS: &[&str] = &[
161    "private_key",
162    "secret",
163    "password",
164    "mnemonic",
165    "seed",
166    "bearer",
167    "authorization",
168];
169
170impl AptosError {
171    /// Creates a new BCS error
172    pub fn bcs<E: fmt::Display>(err: E) -> Self {
173        Self::Bcs(err.to_string())
174    }
175
176    /// Creates a new transaction error
177    pub fn transaction<S: Into<String>>(msg: S) -> Self {
178        Self::Transaction(msg.into())
179    }
180
181    /// Creates a new API error from response details
182    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    /// Creates a new API error with additional details
192    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    /// Returns true if this is a "not found" error
207    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    /// Returns true if this is a timeout error
220    pub fn is_timeout(&self) -> bool {
221        matches!(self, Self::TransactionTimeout { .. })
222    }
223
224    /// Returns true if this is a transient error that might succeed on retry
225    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    /// Returns a sanitized version of the error message safe for logging.
237    ///
238    /// This method:
239    /// - Removes control characters that could corrupt logs
240    /// - Truncates very long messages to prevent log flooding
241    /// - Redacts patterns that might indicate sensitive information
242    ///
243    /// # Example
244    ///
245    /// ```rust
246    /// use aptos_sdk::AptosError;
247    ///
248    /// let err = AptosError::api(500, "Internal server error with details...");
249    /// let safe_msg = err.sanitized_message();
250    /// // safe_msg is guaranteed to be safe for logging
251    /// ```
252    pub fn sanitized_message(&self) -> String {
253        let raw_message = self.to_string();
254        Self::sanitize_string(&raw_message)
255    }
256
257    /// Sanitizes a string for safe logging.
258    fn sanitize_string(s: &str) -> String {
259        // Remove control characters (except newline and tab for readability)
260        let cleaned: String = s
261            .chars()
262            .filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
263            .collect();
264
265        // Check for sensitive patterns (case-insensitive)
266        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        // Truncate if too long
274        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    /// Returns the error message suitable for display to end users.
286    ///
287    /// This is a more conservative sanitization that provides less detail
288    /// but is safer for user-facing error messages.
289    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); // Should be truncated
528        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        // Test all user_message variants for coverage
574        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        // We can't easily test reqwest errors, so just ensure non-http errors return false
685        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}