use std::fmt;
use thiserror::Error;
pub type AptosResult<T> = Result<T, AptosError>;
#[derive(Error, Debug)]
pub enum AptosError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("BCS error: {0}")]
Bcs(String),
#[error("URL error: {0}")]
Url(#[from] url::ParseError),
#[error("Hex error: {0}")]
Hex(#[from] const_hex::FromHexError),
#[error("Invalid address: {0}")]
InvalidAddress(String),
#[error("Invalid public key: {0}")]
InvalidPublicKey(String),
#[error("Invalid private key: {0}")]
InvalidPrivateKey(String),
#[error("Invalid signature: {0}")]
InvalidSignature(String),
#[error("Signature verification failed")]
SignatureVerificationFailed,
#[error("Invalid type tag: {0}")]
InvalidTypeTag(String),
#[error("Transaction error: {0}")]
Transaction(String),
#[error("Simulation failed: {0}")]
SimulationFailed(String),
#[error("Submission failed: {0}")]
SubmissionFailed(String),
#[error("Execution failed: {vm_status}")]
ExecutionFailed {
vm_status: String,
},
#[error("Transaction timed out after {timeout_secs} seconds")]
TransactionTimeout {
hash: String,
timeout_secs: u64,
},
#[error("API error ({status_code}): {message}")]
Api {
status_code: u16,
message: String,
error_code: Option<String>,
vm_error_code: Option<u64>,
},
#[error("Rate limited: retry after {retry_after_secs:?} seconds")]
RateLimited {
retry_after_secs: Option<u64>,
},
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Account not found: {0}")]
AccountNotFound(String),
#[error("Invalid mnemonic: {0}")]
InvalidMnemonic(String),
#[error("Invalid JWT: {0}")]
InvalidJwt(String),
#[error("Key derivation error: {0}")]
KeyDerivation(String),
#[error("Insufficient signatures: need {required}, got {provided}")]
InsufficientSignatures {
required: usize,
provided: usize,
},
#[error("Feature not enabled: {0}. Enable the '{0}' feature in Cargo.toml")]
FeatureNotEnabled(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
const MAX_ERROR_MESSAGE_LENGTH: usize = 1000;
const SENSITIVE_PATTERNS: &[&str] = &[
"private_key",
"secret",
"password",
"mnemonic",
"seed",
"bearer",
"authorization",
"token",
"jwt",
"credential",
"api_key",
"apikey",
"access_token",
"refresh_token",
"pepper",
];
impl AptosError {
pub fn bcs<E: fmt::Display>(err: E) -> Self {
Self::Bcs(err.to_string())
}
pub fn transaction<S: Into<String>>(msg: S) -> Self {
Self::Transaction(msg.into())
}
pub fn api(status_code: u16, message: impl Into<String>) -> Self {
Self::Api {
status_code,
message: message.into(),
error_code: None,
vm_error_code: None,
}
}
pub fn api_with_details(
status_code: u16,
message: impl Into<String>,
error_code: Option<String>,
vm_error_code: Option<u64>,
) -> Self {
Self::Api {
status_code,
message: message.into(),
error_code,
vm_error_code,
}
}
pub fn is_not_found(&self) -> bool {
matches!(
self,
Self::NotFound(_)
| Self::AccountNotFound(_)
| Self::Api {
status_code: 404,
..
}
)
}
pub fn is_timeout(&self) -> bool {
matches!(self, Self::TransactionTimeout { .. })
}
pub fn is_retryable(&self) -> bool {
match self {
Self::Http(e) => e.is_timeout() || e.is_connect(),
Self::Api { status_code, .. } => {
matches!(status_code, 429 | 500 | 502 | 503 | 504)
}
Self::RateLimited { .. } => true,
_ => false,
}
}
pub fn sanitized_message(&self) -> String {
let raw_message = self.to_string();
Self::sanitize_string(&raw_message)
}
fn sanitize_string(s: &str) -> String {
let cleaned: String = s
.chars()
.filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
.collect();
let lower = cleaned.to_lowercase();
for pattern in SENSITIVE_PATTERNS {
if lower.contains(pattern) {
return format!("[REDACTED: message contained sensitive pattern '{pattern}']");
}
}
for scheme in ["http://", "https://"] {
if let Some(scheme_pos) = lower.find(scheme) {
let url_start = scheme_pos;
let url_rest = &lower[url_start..];
let url_end = url_rest
.find(|c: char| c.is_whitespace() || c == '>' || c == '"' || c == '\'')
.unwrap_or(url_rest.len());
let url_token = &url_rest[..url_end];
if url_token.contains('?') {
return "[REDACTED: message contained URL with query parameters]".into();
}
}
}
if cleaned.len() > MAX_ERROR_MESSAGE_LENGTH {
let mut end = MAX_ERROR_MESSAGE_LENGTH;
while end > 0 && !cleaned.is_char_boundary(end) {
end -= 1;
}
format!(
"{}... [truncated, total length: {}]",
&cleaned[..end],
cleaned.len()
)
} else {
cleaned
}
}
pub fn user_message(&self) -> &'static str {
match self {
Self::Http(_) => "Network error occurred",
Self::Json(_) => "Failed to process response",
Self::Bcs(_) => "Failed to process data",
Self::Url(_) => "Invalid URL",
Self::Hex(_) => "Invalid hex format",
Self::InvalidAddress(_) => "Invalid account address",
Self::InvalidPublicKey(_) => "Invalid public key",
Self::InvalidPrivateKey(_) => "Invalid private key",
Self::InvalidSignature(_) => "Invalid signature",
Self::SignatureVerificationFailed => "Signature verification failed",
Self::InvalidTypeTag(_) => "Invalid type format",
Self::Transaction(_) => "Transaction error",
Self::SimulationFailed(_) => "Transaction simulation failed",
Self::SubmissionFailed(_) => "Transaction submission failed",
Self::ExecutionFailed { .. } => "Transaction execution failed",
Self::TransactionTimeout { .. } => "Transaction timed out",
Self::NotFound(_)
| Self::Api {
status_code: 404, ..
} => "Resource not found",
Self::RateLimited { .. }
| Self::Api {
status_code: 429, ..
} => "Rate limit exceeded",
Self::Api { status_code, .. } if *status_code >= 500 => "Server error",
Self::Api { .. } => "API error",
Self::AccountNotFound(_) => "Account not found",
Self::InvalidMnemonic(_) => "Invalid recovery phrase",
Self::InvalidJwt(_) => "Invalid authentication token",
Self::KeyDerivation(_) => "Key derivation failed",
Self::InsufficientSignatures { .. } => "Insufficient signatures",
Self::FeatureNotEnabled(_) => "Feature not enabled",
Self::Config(_) => "Configuration error",
Self::Internal(_) => "Internal error",
Self::Other(_) => "An error occurred",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = AptosError::InvalidAddress("bad address".to_string());
assert_eq!(err.to_string(), "Invalid address: bad address");
}
#[test]
fn test_is_not_found() {
assert!(AptosError::NotFound("test".to_string()).is_not_found());
assert!(AptosError::AccountNotFound("0x1".to_string()).is_not_found());
assert!(AptosError::api(404, "not found").is_not_found());
assert!(!AptosError::api(500, "server error").is_not_found());
}
#[test]
fn test_is_retryable() {
assert!(AptosError::api(429, "rate limited").is_retryable());
assert!(AptosError::api(503, "unavailable").is_retryable());
assert!(AptosError::api(500, "internal error").is_retryable());
assert!(AptosError::api(502, "bad gateway").is_retryable());
assert!(AptosError::api(504, "timeout").is_retryable());
assert!(!AptosError::api(400, "bad request").is_retryable());
}
#[test]
fn test_is_timeout() {
let err = AptosError::TransactionTimeout {
hash: "0x123".to_string(),
timeout_secs: 30,
};
assert!(err.is_timeout());
assert!(!AptosError::InvalidAddress("test".to_string()).is_timeout());
}
#[test]
fn test_bcs_error() {
let err = AptosError::bcs("serialization failed");
assert!(matches!(err, AptosError::Bcs(_)));
assert!(err.to_string().contains("serialization failed"));
}
#[test]
fn test_transaction_error() {
let err = AptosError::transaction("invalid payload");
assert!(matches!(err, AptosError::Transaction(_)));
assert!(err.to_string().contains("invalid payload"));
}
#[test]
fn test_api_error() {
let err = AptosError::api(400, "bad request");
assert!(err.to_string().contains("400"));
assert!(err.to_string().contains("bad request"));
}
#[test]
fn test_api_error_with_details() {
let err = AptosError::api_with_details(
400,
"invalid argument",
Some("INVALID_ARGUMENT".to_string()),
Some(42),
);
if let AptosError::Api {
status_code,
message,
error_code,
vm_error_code,
} = err
{
assert_eq!(status_code, 400);
assert_eq!(message, "invalid argument");
assert_eq!(error_code, Some("INVALID_ARGUMENT".to_string()));
assert_eq!(vm_error_code, Some(42));
} else {
panic!("Expected Api error variant");
}
}
#[test]
fn test_various_error_displays() {
assert!(
AptosError::InvalidPublicKey("bad key".to_string())
.to_string()
.contains("public key")
);
assert!(
AptosError::InvalidPrivateKey("bad key".to_string())
.to_string()
.contains("private key")
);
assert!(
AptosError::InvalidSignature("bad sig".to_string())
.to_string()
.contains("signature")
);
assert!(
AptosError::SignatureVerificationFailed
.to_string()
.contains("verification")
);
assert!(
AptosError::InvalidTypeTag("bad tag".to_string())
.to_string()
.contains("type tag")
);
assert!(
AptosError::SimulationFailed("error".to_string())
.to_string()
.contains("Simulation")
);
assert!(
AptosError::SubmissionFailed("error".to_string())
.to_string()
.contains("Submission")
);
}
#[test]
fn test_execution_failed() {
let err = AptosError::ExecutionFailed {
vm_status: "ABORTED".to_string(),
};
assert!(err.to_string().contains("ABORTED"));
}
#[test]
fn test_rate_limited() {
let err = AptosError::RateLimited {
retry_after_secs: Some(30),
};
assert!(err.to_string().contains("Rate limited"));
}
#[test]
fn test_insufficient_signatures() {
let err = AptosError::InsufficientSignatures {
required: 3,
provided: 1,
};
assert!(err.to_string().contains('3'));
assert!(err.to_string().contains('1'));
}
#[test]
fn test_feature_not_enabled() {
let err = AptosError::FeatureNotEnabled("ed25519".to_string());
assert!(err.to_string().contains("ed25519"));
assert!(err.to_string().contains("Cargo.toml"));
}
#[test]
fn test_config_error() {
let err = AptosError::Config("invalid config".to_string());
assert!(err.to_string().contains("Configuration"));
}
#[test]
fn test_internal_error() {
let err = AptosError::Internal("bug".to_string());
assert!(err.to_string().contains("Internal"));
}
#[test]
fn test_invalid_mnemonic() {
let err = AptosError::InvalidMnemonic("bad phrase".to_string());
assert!(err.to_string().contains("mnemonic"));
}
#[test]
fn test_invalid_jwt() {
let err = AptosError::InvalidJwt("bad token".to_string());
assert!(err.to_string().contains("JWT"));
}
#[test]
fn test_key_derivation() {
let err = AptosError::KeyDerivation("failed".to_string());
assert!(err.to_string().contains("derivation"));
}
#[test]
fn test_sanitized_message_basic() {
let err = AptosError::api(400, "bad request");
let sanitized = err.sanitized_message();
assert!(sanitized.contains("bad request"));
}
#[test]
fn test_sanitized_message_truncates_long_messages() {
let long_message = "x".repeat(2000);
let err = AptosError::api(500, long_message);
let sanitized = err.sanitized_message();
assert!(sanitized.len() < 1200); assert!(sanitized.contains("truncated"));
}
#[test]
fn test_sanitized_message_removes_control_chars() {
let err = AptosError::api(400, "bad\x00request\x1f");
let sanitized = err.sanitized_message();
assert!(!sanitized.contains('\x00'));
assert!(!sanitized.contains('\x1f'));
}
#[test]
fn test_sanitized_message_redacts_sensitive_patterns() {
let err = AptosError::Internal("private_key: abc123".to_string());
let sanitized = err.sanitized_message();
assert!(sanitized.contains("REDACTED"));
assert!(!sanitized.contains("abc123"));
let err = AptosError::Internal("mnemonic phrase here".to_string());
let sanitized = err.sanitized_message();
assert!(sanitized.contains("REDACTED"));
}
#[test]
fn test_user_message() {
assert_eq!(
AptosError::api(404, "not found").user_message(),
"Resource not found"
);
assert_eq!(
AptosError::api(429, "rate limited").user_message(),
"Rate limit exceeded"
);
assert_eq!(
AptosError::api(500, "internal error").user_message(),
"Server error"
);
assert_eq!(
AptosError::InvalidAddress("bad".to_string()).user_message(),
"Invalid account address"
);
}
#[test]
fn test_user_message_all_variants() {
assert_eq!(
AptosError::InvalidPublicKey("bad".to_string()).user_message(),
"Invalid public key"
);
assert_eq!(
AptosError::InvalidPrivateKey("bad".to_string()).user_message(),
"Invalid private key"
);
assert_eq!(
AptosError::InvalidSignature("bad".to_string()).user_message(),
"Invalid signature"
);
assert_eq!(
AptosError::SignatureVerificationFailed.user_message(),
"Signature verification failed"
);
assert_eq!(
AptosError::InvalidTypeTag("bad".to_string()).user_message(),
"Invalid type format"
);
assert_eq!(
AptosError::Transaction("bad".to_string()).user_message(),
"Transaction error"
);
assert_eq!(
AptosError::SimulationFailed("bad".to_string()).user_message(),
"Transaction simulation failed"
);
assert_eq!(
AptosError::SubmissionFailed("bad".to_string()).user_message(),
"Transaction submission failed"
);
assert_eq!(
AptosError::ExecutionFailed {
vm_status: "ABORTED".to_string()
}
.user_message(),
"Transaction execution failed"
);
assert_eq!(
AptosError::TransactionTimeout {
hash: "0x1".to_string(),
timeout_secs: 30
}
.user_message(),
"Transaction timed out"
);
assert_eq!(
AptosError::NotFound("x".to_string()).user_message(),
"Resource not found"
);
assert_eq!(
AptosError::RateLimited {
retry_after_secs: Some(30)
}
.user_message(),
"Rate limit exceeded"
);
assert_eq!(
AptosError::api(503, "unavailable").user_message(),
"Server error"
);
assert_eq!(
AptosError::api(400, "bad request").user_message(),
"API error"
);
assert_eq!(
AptosError::AccountNotFound("0x1".to_string()).user_message(),
"Account not found"
);
assert_eq!(
AptosError::InvalidMnemonic("bad".to_string()).user_message(),
"Invalid recovery phrase"
);
assert_eq!(
AptosError::InvalidJwt("bad".to_string()).user_message(),
"Invalid authentication token"
);
assert_eq!(
AptosError::KeyDerivation("bad".to_string()).user_message(),
"Key derivation failed"
);
assert_eq!(
AptosError::InsufficientSignatures {
required: 3,
provided: 1
}
.user_message(),
"Insufficient signatures"
);
assert_eq!(
AptosError::FeatureNotEnabled("ed25519".to_string()).user_message(),
"Feature not enabled"
);
assert_eq!(
AptosError::Config("bad".to_string()).user_message(),
"Configuration error"
);
assert_eq!(
AptosError::Internal("bug".to_string()).user_message(),
"Internal error"
);
assert_eq!(
AptosError::Other(anyhow::anyhow!("misc")).user_message(),
"An error occurred"
);
}
#[test]
fn test_is_retryable_http_errors() {
assert!(!AptosError::InvalidAddress("x".to_string()).is_retryable());
assert!(!AptosError::Transaction("x".to_string()).is_retryable());
assert!(!AptosError::NotFound("x".to_string()).is_retryable());
}
}