use std::{array::TryFromSliceError, result::Result as StdResult};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::crypto::CryptoError;
pub type Result<T> = StdResult<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("JSON parsing failed: {0}")]
Json(#[from] serde_json::Error),
#[error("API error {status_code}: {error_code} - {message}")]
Api {
status_code: u16,
error_code: String,
message: String,
},
#[error("HTTP transport error: {message}")]
HttpTransport { message: String, status_code: Option<u16> },
#[error("Request timeout after {timeout_ms}ms to {endpoint}")]
RequestTimeout { endpoint: String, timeout_ms: u64 },
#[error("Connection failed: {0}")]
Connection(String),
#[error("DNS resolution failed: {0}")]
DnsResolution(String),
#[error("Failed to deserialize {format} response: {error} - Response: {response}")]
ResponseDeserialization {
format: String,
error: String,
response: String,
},
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("Authorization failed: {0}")]
Authorization(String),
#[error("Rate limit exceeded")]
RateLimitExceeded { retry_after_seconds: Option<u64> },
#[error("Invalid parameter '{parameter}': {message}")]
InvalidParameter { parameter: String, message: String },
#[error("Resource not found: {resource_type} with {identifier}")]
ResourceNotFound { resource_type: String, identifier: String },
#[error("Business logic error: {operation} failed - {reason}")]
BusinessLogic { operation: String, reason: String },
#[error("Cryptographic operation failed: {0}")]
Crypto(#[from] CryptoError),
#[error("Signature verification failed: {0}")]
VerificationError(String),
#[error("Client configuration error: {0}")]
Config(#[from] ConfigError),
#[error("Invalid URL: {0}")]
Url(#[from] url::ParseError),
#[error("Hex decoding failed: {0}")]
Hex(#[from] hex::FromHexError),
#[error("Invalid address format: {0}")]
Address(String),
#[error("Array conversion failed: expected length {expected}, got {actual}")]
ArrayConversion { expected: usize, actual: usize },
#[error("Validation failed: {field} - {message}")]
Validation { field: String, message: String },
#[error("{0}")]
Custom(String),
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Invalid timeout: {0}")]
InvalidTimeout(String),
#[error("Invalid network configuration: {0}")]
InvalidNetwork(String),
#[error("Missing required configuration: {0}")]
MissingConfig(String),
#[error("Failed to build HTTP client: {0}")]
ClientBuilder(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error_code: String,
pub message: String,
}
impl Error {
pub fn api(status_code: u16, error_code: String, message: String) -> Self {
Self::Api {
status_code,
error_code,
message,
}
}
pub fn address<T: Into<String>>(msg: T) -> Self {
Self::Address(msg.into())
}
pub fn array_conversion(expected: usize, actual: usize) -> Self {
Self::ArrayConversion { expected, actual }
}
pub fn validation<T: Into<String>, U: Into<String>>(field: T, message: U) -> Self {
Self::Validation {
field: field.into(),
message: message.into(),
}
}
pub fn custom<T: Into<String>>(msg: T) -> Self {
Self::Custom(msg.into())
}
pub fn verification_error<T: Into<String>>(msg: T) -> Self {
Self::VerificationError(msg.into())
}
pub fn is_api_error(&self) -> bool {
matches!(self, Self::Api { .. })
}
pub fn is_config_error(&self) -> bool {
matches!(self, Self::Config(_))
}
pub fn is_crypto_error(&self) -> bool {
matches!(self, Self::Crypto(_))
}
pub fn status_code(&self) -> Option<u16> {
match self {
Self::Api { status_code, .. } => Some(*status_code),
_ => None,
}
}
pub fn error_code(&self) -> Option<&str> {
match self {
Self::Api { error_code, .. } => Some(error_code),
_ => None,
}
}
pub fn http_transport<T: Into<String>>(message: T, status_code: Option<u16>) -> Self {
Self::HttpTransport {
message: message.into(),
status_code,
}
}
pub fn request_timeout<T: Into<String>>(endpoint: T, timeout_ms: u64) -> Self {
Self::RequestTimeout {
endpoint: endpoint.into(),
timeout_ms,
}
}
pub fn connection<T: Into<String>>(message: T) -> Self {
Self::Connection(message.into())
}
pub fn dns_resolution<T: Into<String>>(message: T) -> Self {
Self::DnsResolution(message.into())
}
pub fn response_deserialization<A: Into<String>, B: Into<String>, C: Into<String>>(
format: A,
error: B,
response: C,
) -> Self {
Self::ResponseDeserialization {
format: format.into(),
error: error.into(),
response: response.into(),
}
}
pub fn authentication<T: Into<String>>(message: T) -> Self {
Self::Authentication(message.into())
}
pub fn authorization<T: Into<String>>(message: T) -> Self {
Self::Authorization(message.into())
}
pub fn rate_limit_exceeded(retry_after_seconds: Option<u64>) -> Self {
Self::RateLimitExceeded { retry_after_seconds }
}
pub fn invalid_parameter<A: Into<String>, B: Into<String>>(parameter: A, message: B) -> Self {
Self::InvalidParameter {
parameter: parameter.into(),
message: message.into(),
}
}
pub fn resource_not_found<A: Into<String>, B: Into<String>>(resource_type: A, identifier: B) -> Self {
Self::ResourceNotFound {
resource_type: resource_type.into(),
identifier: identifier.into(),
}
}
pub fn business_logic<A: Into<String>, B: Into<String>>(operation: A, reason: B) -> Self {
Self::BusinessLogic {
operation: operation.into(),
reason: reason.into(),
}
}
}
impl From<TryFromSliceError> for Error {
fn from(_err: TryFromSliceError) -> Self {
Self::ArrayConversion {
expected: 32, actual: 0, }
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
Error::request_timeout(
err.url()
.map(|u| u.to_string())
.unwrap_or_else(|| "unknown".to_string()),
30000, )
} else if err.is_connect() {
Error::connection(format!("Connection failed: {}", err))
} else if err.is_request() {
Error::invalid_parameter("request", format!("Request error: {}", err))
} else if err.is_decode() {
Error::response_deserialization("JSON", err.to_string(), "Failed to decode response body")
} else {
if let Some(status) = err.status() {
Error::http_transport(err.to_string(), Some(status.as_u16()))
} else {
Error::http_transport(err.to_string(), None)
}
}
}
}
impl ConfigError {
pub fn invalid_timeout<T: Into<String>>(msg: T) -> Self {
Self::InvalidTimeout(msg.into())
}
pub fn invalid_network<T: Into<String>>(msg: T) -> Self {
Self::InvalidNetwork(msg.into())
}
pub fn missing_config<T: Into<String>>(msg: T) -> Self {
Self::MissingConfig(msg.into())
}
pub fn client_builder<T: Into<String>>(msg: T) -> Self {
Self::ClientBuilder(msg.into())
}
}
#[cfg(test)]
mod tests {
use std::error::Error as StdError;
use super::*;
#[test]
fn test_error_creation_methods() {
let api_error = Error::api(
404,
"resource_not_found".to_string(),
"Transaction not found".to_string(),
);
assert!(matches!(api_error, Error::Api { status_code: 404, .. }));
assert_eq!(api_error.status_code(), Some(404));
assert_eq!(api_error.error_code(), Some("resource_not_found"));
let addr_error = Error::address("Invalid address format");
assert!(matches!(addr_error, Error::Address(_)));
let array_error = Error::array_conversion(32, 16);
assert!(matches!(
array_error,
Error::ArrayConversion {
expected: 32,
actual: 16
}
));
let validation_error = Error::validation("email", "Invalid email format");
assert!(matches!(validation_error, Error::Validation { .. }));
let custom_error = Error::custom("Custom error message");
assert!(matches!(custom_error, Error::Custom(_)));
}
#[test]
fn test_http_transport_error_creation() {
let error_with_status = Error::http_transport("Connection failed", Some(500));
assert!(matches!(
error_with_status,
Error::HttpTransport {
status_code: Some(500),
..
}
));
let error_without_status = Error::http_transport("Connection failed", None);
assert!(matches!(
error_without_status,
Error::HttpTransport { status_code: None, .. }
));
}
#[test]
fn test_request_timeout_error_creation() {
let timeout_error = Error::request_timeout("/api/transactions", 30000);
assert!(matches!(timeout_error, Error::RequestTimeout { timeout_ms: 30000, .. }));
}
#[test]
fn test_authentication_and_authorization_errors() {
let auth_error = Error::authentication("Invalid signature");
assert!(matches!(auth_error, Error::Authentication(_)));
let authz_error = Error::authorization("Insufficient permissions");
assert!(matches!(authz_error, Error::Authorization(_)));
}
#[test]
fn test_rate_limit_error_creation() {
let rate_limit_with_retry = Error::rate_limit_exceeded(Some(60));
assert!(matches!(
rate_limit_with_retry,
Error::RateLimitExceeded {
retry_after_seconds: Some(60)
}
));
let rate_limit_without_retry = Error::rate_limit_exceeded(None);
assert!(matches!(
rate_limit_without_retry,
Error::RateLimitExceeded {
retry_after_seconds: None
}
));
}
#[test]
fn test_parameter_and_resource_errors() {
let param_error = Error::invalid_parameter("amount", "Amount must be positive");
assert!(matches!(param_error, Error::InvalidParameter { .. }));
let resource_error = Error::resource_not_found("transaction", "0x123abc");
assert!(matches!(resource_error, Error::ResourceNotFound { .. }));
}
#[test]
fn test_business_logic_error_creation() {
let business_error = Error::business_logic("transfer", "Insufficient balance");
assert!(matches!(business_error, Error::BusinessLogic { .. }));
}
#[test]
fn test_connection_and_dns_errors() {
let conn_error = Error::connection("Failed to connect to server");
assert!(matches!(conn_error, Error::Connection(_)));
let dns_error = Error::dns_resolution("Could not resolve hostname");
assert!(matches!(dns_error, Error::DnsResolution(_)));
}
#[test]
fn test_response_deserialization_error() {
let deser_error = Error::response_deserialization("JSON", "unexpected end of input", "{\"invalid\":");
assert!(matches!(deser_error, Error::ResponseDeserialization { .. }));
}
#[test]
fn test_error_type_checking_methods() {
let api_error = Error::api(500, "server_error".to_string(), "Internal server error".to_string());
assert!(api_error.is_api_error());
assert!(!api_error.is_config_error());
assert!(!api_error.is_crypto_error());
let config_error = Error::Config(ConfigError::InvalidTimeout("Timeout too large".to_string()));
assert!(!config_error.is_api_error());
assert!(config_error.is_config_error());
assert!(!config_error.is_crypto_error());
let crypto_error = Error::Crypto(CryptoError::InvalidPrivateKey("Invalid key format".to_string()));
assert!(!crypto_error.is_api_error());
assert!(!crypto_error.is_config_error());
assert!(crypto_error.is_crypto_error());
}
#[test]
fn test_status_code_and_error_code_extraction() {
let api_error = Error::api(422, "business_logic_error".to_string(), "Invalid operation".to_string());
assert_eq!(api_error.status_code(), Some(422));
assert_eq!(api_error.error_code(), Some("business_logic_error"));
let non_api_error = Error::custom("Not an API error");
assert_eq!(non_api_error.status_code(), None);
assert_eq!(non_api_error.error_code(), None);
}
#[test]
fn test_crypto_error_creation() {
let invalid_private_key = CryptoError::invalid_private_key("Key too short");
assert!(matches!(invalid_private_key, CryptoError::InvalidPrivateKey(_)));
let invalid_public_key = CryptoError::invalid_public_key("Invalid format");
assert!(matches!(invalid_public_key, CryptoError::InvalidPublicKey(_)));
let signature_failed = CryptoError::signature_failed("Could not create signature");
assert!(matches!(signature_failed, CryptoError::SignatureFailed(_)));
let verification_failed = CryptoError::verification_failed("Signature mismatch");
assert!(matches!(verification_failed, CryptoError::VerificationFailed(_)));
let key_derivation = CryptoError::key_derivation("Derivation failed");
assert!(matches!(key_derivation, CryptoError::KeyDerivation(_)));
}
#[test]
fn test_config_error_creation() {
let invalid_timeout = ConfigError::invalid_timeout("Timeout cannot be zero");
assert!(matches!(invalid_timeout, ConfigError::InvalidTimeout(_)));
let invalid_network = ConfigError::invalid_network("Unknown network");
assert!(matches!(invalid_network, ConfigError::InvalidNetwork(_)));
let missing_config = ConfigError::missing_config("API key required");
assert!(matches!(missing_config, ConfigError::MissingConfig(_)));
let client_builder = ConfigError::client_builder("Failed to build HTTP client");
assert!(matches!(client_builder, ConfigError::ClientBuilder(_)));
}
#[test]
fn test_error_display_formatting() {
let api_error = Error::api(404, "not_found".to_string(), "Resource not found".to_string());
let display_str = format!("{}", api_error);
assert!(display_str.contains("API error 404"));
assert!(display_str.contains("not_found"));
assert!(display_str.contains("Resource not found"));
let timeout_error = Error::request_timeout("/api/test", 5000);
let timeout_str = format!("{}", timeout_error);
assert!(timeout_str.contains("Request timeout after 5000ms"));
assert!(timeout_str.contains("/api/test"));
let param_error = Error::invalid_parameter("amount", "Must be positive");
let param_str = format!("{}", param_error);
assert!(param_str.contains("Invalid parameter 'amount'"));
assert!(param_str.contains("Must be positive"));
}
#[test]
fn test_error_from_conversions() {
let crypto_error = CryptoError::invalid_private_key("Invalid key");
let error: Error = crypto_error.into();
assert!(matches!(error, Error::Crypto(_)));
let config_error = ConfigError::invalid_timeout("Invalid timeout");
let error: Error = config_error.into();
assert!(matches!(error, Error::Config(_)));
let result: StdResult<[u8; 4], TryFromSliceError> = [0u8; 2].as_slice().try_into();
let slice_error = result.unwrap_err();
let error: Error = slice_error.into();
assert!(matches!(
error,
Error::ArrayConversion {
expected: 32,
actual: 0
}
));
}
#[test]
fn test_error_response_structure() {
let error_response = ErrorResponse {
error_code: "validation_error".to_string(),
message: "Invalid input parameters".to_string(),
};
let json = serde_json::to_string(&error_response).expect("Should serialize");
assert!(json.contains("validation_error"));
assert!(json.contains("Invalid input parameters"));
let deserialized: ErrorResponse = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(deserialized.error_code, "validation_error");
assert_eq!(deserialized.message, "Invalid input parameters");
}
#[test]
fn test_reqwest_error_conversion() {
}
#[test]
fn test_error_debug_formatting() {
let error = Error::api(500, "server_error".to_string(), "Internal error".to_string());
let debug_str = format!("{:?}", error);
assert!(debug_str.contains("Api"));
assert!(debug_str.contains("status_code: 500"));
let crypto_error = CryptoError::invalid_private_key("Invalid format");
let crypto_debug = format!("{:?}", crypto_error);
assert!(crypto_debug.contains("InvalidPrivateKey"));
let config_error = ConfigError::invalid_network("Unknown network");
let config_debug = format!("{:?}", config_error);
assert!(config_debug.contains("InvalidNetwork"));
}
#[test]
fn test_result_type_alias() {
let success_result: Result<String> = Ok("success".to_string());
assert!(success_result.is_ok());
if let Ok(value) = success_result {
assert_eq!(value, "success");
}
let error_result: Result<String> = Err(Error::custom("test error"));
assert!(error_result.is_err());
if let Err(error) = error_result {
assert!(matches!(error, Error::Custom(_)));
}
}
#[test]
fn test_error_source_chain() {
let crypto_error = CryptoError::invalid_private_key("Base crypto error");
let main_error = Error::Crypto(crypto_error);
assert!(main_error.source().is_some());
let config_error = ConfigError::invalid_timeout("Base config error");
let main_error = Error::Config(config_error);
assert!(main_error.source().is_some());
}
}