onemoney_protocol/
error.rs

1//! Error types for the OneMoney SDK.
2
3use serde::{Deserialize, Serialize};
4use std::array::TryFromSliceError;
5use std::result::Result as StdResult;
6use thiserror::Error;
7
8/// Result type alias for OneMoney SDK operations.
9pub type Result<T> = StdResult<T, Error>;
10
11/// Main error type for the OneMoney SDK.
12#[derive(Error, Debug)]
13pub enum Error {
14    /// JSON serialization/deserialization error.
15    #[error("JSON parsing failed: {0}")]
16    Json(#[from] serde_json::Error),
17
18    /// API error returned by the server.
19    #[error("API error {status_code}: {error_code} - {message}")]
20    Api {
21        status_code: u16,
22        error_code: String,
23        message: String,
24    },
25
26    /// HTTP transport error with optional status code.
27    #[error("HTTP transport error: {message}")]
28    HttpTransport {
29        message: String,
30        status_code: Option<u16>,
31    },
32
33    /// Request timeout error.
34    #[error("Request timeout after {timeout_ms}ms to {endpoint}")]
35    RequestTimeout { endpoint: String, timeout_ms: u64 },
36
37    /// Connection error.
38    #[error("Connection failed: {0}")]
39    Connection(String),
40
41    /// DNS resolution error.
42    #[error("DNS resolution failed: {0}")]
43    DnsResolution(String),
44
45    /// Response deserialization error.
46    #[error("Failed to deserialize {format} response: {error} - Response: {response}")]
47    ResponseDeserialization {
48        format: String,
49        error: String,
50        response: String,
51    },
52
53    /// Authentication error.
54    #[error("Authentication failed: {0}")]
55    Authentication(String),
56
57    /// Authorization error.
58    #[error("Authorization failed: {0}")]
59    Authorization(String),
60
61    /// Rate limit exceeded.
62    #[error("Rate limit exceeded")]
63    RateLimitExceeded { retry_after_seconds: Option<u64> },
64
65    /// Invalid request parameter.
66    #[error("Invalid parameter '{parameter}': {message}")]
67    InvalidParameter { parameter: String, message: String },
68
69    /// Resource not found.
70    #[error("Resource not found: {resource_type} with {identifier}")]
71    ResourceNotFound {
72        resource_type: String,
73        identifier: String,
74    },
75
76    /// Business logic error.
77    #[error("Business logic error: {operation} failed - {reason}")]
78    BusinessLogic { operation: String, reason: String },
79
80    /// Cryptographic operation errors.
81    #[error("Cryptographic operation failed: {0}")]
82    Crypto(#[from] CryptoError),
83
84    /// Client configuration errors.
85    #[error("Client configuration error: {0}")]
86    Config(#[from] ConfigError),
87
88    /// URL parsing error.
89    #[error("Invalid URL: {0}")]
90    Url(#[from] url::ParseError),
91
92    /// Hex decoding error.
93    #[error("Hex decoding failed: {0}")]
94    Hex(#[from] hex::FromHexError),
95
96    /// RLP encoding/decoding error.
97    #[error("RLP encoding/decoding failed: {0}")]
98    Rlp(#[from] rlp::DecoderError),
99
100    /// Address parsing error.
101    #[error("Invalid address format: {0}")]
102    Address(String),
103
104    /// Array conversion error.
105    #[error("Array conversion failed: expected length {expected}, got {actual}")]
106    ArrayConversion { expected: usize, actual: usize },
107
108    /// Validation error for input parameters.
109    #[error("Validation failed: {field} - {message}")]
110    Validation { field: String, message: String },
111
112    /// Generic error with custom message.
113    #[error("{0}")]
114    Custom(String),
115}
116
117/// Cryptographic operation errors.
118#[derive(Error, Debug)]
119pub enum CryptoError {
120    /// Invalid private key format or content.
121    #[error("Invalid private key: {0}")]
122    InvalidPrivateKey(String),
123
124    /// Invalid public key format or content.
125    #[error("Invalid public key: {0}")]
126    InvalidPublicKey(String),
127
128    /// Signature creation failed.
129    #[error("Failed to create signature: {0}")]
130    SignatureFailed(String),
131
132    /// Signature verification failed.
133    #[error("Signature verification failed: {0}")]
134    VerificationFailed(String),
135
136    /// Key derivation error.
137    #[error("Key derivation failed: {0}")]
138    KeyDerivation(String),
139}
140
141/// Client configuration errors.
142#[derive(Error, Debug)]
143pub enum ConfigError {
144    /// Invalid timeout value.
145    #[error("Invalid timeout: {0}")]
146    InvalidTimeout(String),
147
148    /// Invalid network configuration.
149    #[error("Invalid network configuration: {0}")]
150    InvalidNetwork(String),
151
152    /// Missing required configuration.
153    #[error("Missing required configuration: {0}")]
154    MissingConfig(String),
155
156    /// HTTP client builder failed.
157    #[error("Failed to build HTTP client: {0}")]
158    ClientBuilder(String),
159}
160
161/// API error response structure.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ErrorResponse {
164    pub error_code: String,
165    pub message: String,
166}
167
168impl Error {
169    /// Create a new API error.
170    pub fn api(status_code: u16, error_code: String, message: String) -> Self {
171        Self::Api {
172            status_code,
173            error_code,
174            message,
175        }
176    }
177
178    /// Create an address parsing error.
179    pub fn address<T: Into<String>>(msg: T) -> Self {
180        Self::Address(msg.into())
181    }
182
183    /// Create an array conversion error.
184    pub fn array_conversion(expected: usize, actual: usize) -> Self {
185        Self::ArrayConversion { expected, actual }
186    }
187
188    /// Create a validation error.
189    pub fn validation<T: Into<String>, U: Into<String>>(field: T, message: U) -> Self {
190        Self::Validation {
191            field: field.into(),
192            message: message.into(),
193        }
194    }
195
196    /// Create a custom error.
197    pub fn custom<T: Into<String>>(msg: T) -> Self {
198        Self::Custom(msg.into())
199    }
200
201    /// Check if this is an API error.
202    pub fn is_api_error(&self) -> bool {
203        matches!(self, Self::Api { .. })
204    }
205
206    /// Check if this is a configuration error.
207    pub fn is_config_error(&self) -> bool {
208        matches!(self, Self::Config(_))
209    }
210
211    /// Check if this is a cryptographic error.
212    pub fn is_crypto_error(&self) -> bool {
213        matches!(self, Self::Crypto(_))
214    }
215
216    /// Get the status code if this is an API error.
217    pub fn status_code(&self) -> Option<u16> {
218        match self {
219            Self::Api { status_code, .. } => Some(*status_code),
220            _ => None,
221        }
222    }
223
224    /// Get the error code if this is an API error.
225    pub fn error_code(&self) -> Option<&str> {
226        match self {
227            Self::Api { error_code, .. } => Some(error_code),
228            _ => None,
229        }
230    }
231
232    /// Create an HTTP transport error.
233    pub fn http_transport<T: Into<String>>(message: T, status_code: Option<u16>) -> Self {
234        Self::HttpTransport {
235            message: message.into(),
236            status_code,
237        }
238    }
239
240    /// Create a request timeout error.
241    pub fn request_timeout<T: Into<String>>(endpoint: T, timeout_ms: u64) -> Self {
242        Self::RequestTimeout {
243            endpoint: endpoint.into(),
244            timeout_ms,
245        }
246    }
247
248    /// Create a connection error.
249    pub fn connection<T: Into<String>>(message: T) -> Self {
250        Self::Connection(message.into())
251    }
252
253    /// Create a DNS resolution error.
254    pub fn dns_resolution<T: Into<String>>(message: T) -> Self {
255        Self::DnsResolution(message.into())
256    }
257
258    /// Create a response deserialization error.
259    pub fn response_deserialization<A: Into<String>, B: Into<String>, C: Into<String>>(
260        format: A,
261        error: B,
262        response: C,
263    ) -> Self {
264        Self::ResponseDeserialization {
265            format: format.into(),
266            error: error.into(),
267            response: response.into(),
268        }
269    }
270
271    /// Create an authentication error.
272    pub fn authentication<T: Into<String>>(message: T) -> Self {
273        Self::Authentication(message.into())
274    }
275
276    /// Create an authorization error.
277    pub fn authorization<T: Into<String>>(message: T) -> Self {
278        Self::Authorization(message.into())
279    }
280
281    /// Create a rate limit exceeded error.
282    pub fn rate_limit_exceeded(retry_after_seconds: Option<u64>) -> Self {
283        Self::RateLimitExceeded {
284            retry_after_seconds,
285        }
286    }
287
288    /// Create an invalid parameter error.
289    pub fn invalid_parameter<A: Into<String>, B: Into<String>>(parameter: A, message: B) -> Self {
290        Self::InvalidParameter {
291            parameter: parameter.into(),
292            message: message.into(),
293        }
294    }
295
296    /// Create a resource not found error.
297    pub fn resource_not_found<A: Into<String>, B: Into<String>>(
298        resource_type: A,
299        identifier: B,
300    ) -> Self {
301        Self::ResourceNotFound {
302            resource_type: resource_type.into(),
303            identifier: identifier.into(),
304        }
305    }
306
307    /// Create a business logic error.
308    pub fn business_logic<A: Into<String>, B: Into<String>>(operation: A, reason: B) -> Self {
309        Self::BusinessLogic {
310            operation: operation.into(),
311            reason: reason.into(),
312        }
313    }
314}
315
316impl From<TryFromSliceError> for Error {
317    fn from(_err: TryFromSliceError) -> Self {
318        Self::ArrayConversion {
319            expected: 32, // Most common case for crypto operations
320            actual: 0,    // We don't have the actual length in TryFromSliceError
321        }
322    }
323}
324
325/// Enhanced reqwest error mapping with L1 compatibility.
326impl From<reqwest::Error> for Error {
327    fn from(err: reqwest::Error) -> Self {
328        if err.is_timeout() {
329            Error::request_timeout(
330                err.url()
331                    .map(|u| u.to_string())
332                    .unwrap_or_else(|| "unknown".to_string()),
333                30000, // Default timeout assumption
334            )
335        } else if err.is_connect() {
336            Error::connection(format!("Connection failed: {}", err))
337        } else if err.is_request() {
338            Error::invalid_parameter("request", format!("Request error: {}", err))
339        } else if err.is_decode() {
340            Error::response_deserialization(
341                "JSON",
342                err.to_string(),
343                "Failed to decode response body",
344            )
345        } else {
346            // Check for specific HTTP status codes if available
347            if let Some(status) = err.status() {
348                Error::http_transport(err.to_string(), Some(status.as_u16()))
349            } else {
350                Error::http_transport(err.to_string(), None)
351            }
352        }
353    }
354}
355
356impl CryptoError {
357    /// Create an invalid private key error.
358    pub fn invalid_private_key<T: Into<String>>(msg: T) -> Self {
359        Self::InvalidPrivateKey(msg.into())
360    }
361
362    /// Create an invalid public key error.
363    pub fn invalid_public_key<T: Into<String>>(msg: T) -> Self {
364        Self::InvalidPublicKey(msg.into())
365    }
366
367    /// Create a signature failed error.
368    pub fn signature_failed<T: Into<String>>(msg: T) -> Self {
369        Self::SignatureFailed(msg.into())
370    }
371
372    /// Create a verification failed error.
373    pub fn verification_failed<T: Into<String>>(msg: T) -> Self {
374        Self::VerificationFailed(msg.into())
375    }
376
377    /// Create a key derivation error.
378    pub fn key_derivation<T: Into<String>>(msg: T) -> Self {
379        Self::KeyDerivation(msg.into())
380    }
381}
382
383impl ConfigError {
384    /// Create an invalid timeout error.
385    pub fn invalid_timeout<T: Into<String>>(msg: T) -> Self {
386        Self::InvalidTimeout(msg.into())
387    }
388
389    /// Create an invalid network error.
390    pub fn invalid_network<T: Into<String>>(msg: T) -> Self {
391        Self::InvalidNetwork(msg.into())
392    }
393
394    /// Create a missing config error.
395    pub fn missing_config<T: Into<String>>(msg: T) -> Self {
396        Self::MissingConfig(msg.into())
397    }
398
399    /// Create a client builder error.
400    pub fn client_builder<T: Into<String>>(msg: T) -> Self {
401        Self::ClientBuilder(msg.into())
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use std::error::Error as StdError;
409
410    #[test]
411    fn test_error_creation_methods() {
412        // Test API error creation
413        let api_error = Error::api(
414            404,
415            "resource_not_found".to_string(),
416            "Transaction not found".to_string(),
417        );
418        assert!(matches!(
419            api_error,
420            Error::Api {
421                status_code: 404,
422                ..
423            }
424        ));
425        assert_eq!(api_error.status_code(), Some(404));
426        assert_eq!(api_error.error_code(), Some("resource_not_found"));
427
428        // Test address error creation
429        let addr_error = Error::address("Invalid address format");
430        assert!(matches!(addr_error, Error::Address(_)));
431
432        // Test array conversion error creation
433        let array_error = Error::array_conversion(32, 16);
434        assert!(matches!(
435            array_error,
436            Error::ArrayConversion {
437                expected: 32,
438                actual: 16
439            }
440        ));
441
442        // Test validation error creation
443        let validation_error = Error::validation("email", "Invalid email format");
444        assert!(matches!(validation_error, Error::Validation { .. }));
445
446        // Test custom error creation
447        let custom_error = Error::custom("Custom error message");
448        assert!(matches!(custom_error, Error::Custom(_)));
449    }
450
451    #[test]
452    fn test_http_transport_error_creation() {
453        let error_with_status = Error::http_transport("Connection failed", Some(500));
454        assert!(matches!(
455            error_with_status,
456            Error::HttpTransport {
457                status_code: Some(500),
458                ..
459            }
460        ));
461
462        let error_without_status = Error::http_transport("Connection failed", None);
463        assert!(matches!(
464            error_without_status,
465            Error::HttpTransport {
466                status_code: None,
467                ..
468            }
469        ));
470    }
471
472    #[test]
473    fn test_request_timeout_error_creation() {
474        let timeout_error = Error::request_timeout("/api/transactions", 30000);
475        assert!(matches!(
476            timeout_error,
477            Error::RequestTimeout {
478                timeout_ms: 30000,
479                ..
480            }
481        ));
482    }
483
484    #[test]
485    fn test_authentication_and_authorization_errors() {
486        let auth_error = Error::authentication("Invalid signature");
487        assert!(matches!(auth_error, Error::Authentication(_)));
488
489        let authz_error = Error::authorization("Insufficient permissions");
490        assert!(matches!(authz_error, Error::Authorization(_)));
491    }
492
493    #[test]
494    fn test_rate_limit_error_creation() {
495        let rate_limit_with_retry = Error::rate_limit_exceeded(Some(60));
496        assert!(matches!(
497            rate_limit_with_retry,
498            Error::RateLimitExceeded {
499                retry_after_seconds: Some(60)
500            }
501        ));
502
503        let rate_limit_without_retry = Error::rate_limit_exceeded(None);
504        assert!(matches!(
505            rate_limit_without_retry,
506            Error::RateLimitExceeded {
507                retry_after_seconds: None
508            }
509        ));
510    }
511
512    #[test]
513    fn test_parameter_and_resource_errors() {
514        let param_error = Error::invalid_parameter("amount", "Amount must be positive");
515        assert!(matches!(param_error, Error::InvalidParameter { .. }));
516
517        let resource_error = Error::resource_not_found("transaction", "0x123abc");
518        assert!(matches!(resource_error, Error::ResourceNotFound { .. }));
519    }
520
521    #[test]
522    fn test_business_logic_error_creation() {
523        let business_error = Error::business_logic("transfer", "Insufficient balance");
524        assert!(matches!(business_error, Error::BusinessLogic { .. }));
525    }
526
527    #[test]
528    fn test_connection_and_dns_errors() {
529        let conn_error = Error::connection("Failed to connect to server");
530        assert!(matches!(conn_error, Error::Connection(_)));
531
532        let dns_error = Error::dns_resolution("Could not resolve hostname");
533        assert!(matches!(dns_error, Error::DnsResolution(_)));
534    }
535
536    #[test]
537    fn test_response_deserialization_error() {
538        let deser_error =
539            Error::response_deserialization("JSON", "unexpected end of input", "{\"invalid\":");
540        assert!(matches!(deser_error, Error::ResponseDeserialization { .. }));
541    }
542
543    #[test]
544    fn test_error_type_checking_methods() {
545        let api_error = Error::api(
546            500,
547            "server_error".to_string(),
548            "Internal server error".to_string(),
549        );
550        assert!(api_error.is_api_error());
551        assert!(!api_error.is_config_error());
552        assert!(!api_error.is_crypto_error());
553
554        let config_error =
555            Error::Config(ConfigError::InvalidTimeout("Timeout too large".to_string()));
556        assert!(!config_error.is_api_error());
557        assert!(config_error.is_config_error());
558        assert!(!config_error.is_crypto_error());
559
560        let crypto_error = Error::Crypto(CryptoError::InvalidPrivateKey(
561            "Invalid key format".to_string(),
562        ));
563        assert!(!crypto_error.is_api_error());
564        assert!(!crypto_error.is_config_error());
565        assert!(crypto_error.is_crypto_error());
566    }
567
568    #[test]
569    fn test_status_code_and_error_code_extraction() {
570        let api_error = Error::api(
571            422,
572            "business_logic_error".to_string(),
573            "Invalid operation".to_string(),
574        );
575        assert_eq!(api_error.status_code(), Some(422));
576        assert_eq!(api_error.error_code(), Some("business_logic_error"));
577
578        let non_api_error = Error::custom("Not an API error");
579        assert_eq!(non_api_error.status_code(), None);
580        assert_eq!(non_api_error.error_code(), None);
581    }
582
583    #[test]
584    fn test_crypto_error_creation() {
585        let invalid_private_key = CryptoError::invalid_private_key("Key too short");
586        assert!(matches!(
587            invalid_private_key,
588            CryptoError::InvalidPrivateKey(_)
589        ));
590
591        let invalid_public_key = CryptoError::invalid_public_key("Invalid format");
592        assert!(matches!(
593            invalid_public_key,
594            CryptoError::InvalidPublicKey(_)
595        ));
596
597        let signature_failed = CryptoError::signature_failed("Could not create signature");
598        assert!(matches!(signature_failed, CryptoError::SignatureFailed(_)));
599
600        let verification_failed = CryptoError::verification_failed("Signature mismatch");
601        assert!(matches!(
602            verification_failed,
603            CryptoError::VerificationFailed(_)
604        ));
605
606        let key_derivation = CryptoError::key_derivation("Derivation failed");
607        assert!(matches!(key_derivation, CryptoError::KeyDerivation(_)));
608    }
609
610    #[test]
611    fn test_config_error_creation() {
612        let invalid_timeout = ConfigError::invalid_timeout("Timeout cannot be zero");
613        assert!(matches!(invalid_timeout, ConfigError::InvalidTimeout(_)));
614
615        let invalid_network = ConfigError::invalid_network("Unknown network");
616        assert!(matches!(invalid_network, ConfigError::InvalidNetwork(_)));
617
618        let missing_config = ConfigError::missing_config("API key required");
619        assert!(matches!(missing_config, ConfigError::MissingConfig(_)));
620
621        let client_builder = ConfigError::client_builder("Failed to build HTTP client");
622        assert!(matches!(client_builder, ConfigError::ClientBuilder(_)));
623    }
624
625    #[test]
626    fn test_error_display_formatting() {
627        // Test different error display formats
628        let api_error = Error::api(
629            404,
630            "not_found".to_string(),
631            "Resource not found".to_string(),
632        );
633        let display_str = format!("{}", api_error);
634        assert!(display_str.contains("API error 404"));
635        assert!(display_str.contains("not_found"));
636        assert!(display_str.contains("Resource not found"));
637
638        let timeout_error = Error::request_timeout("/api/test", 5000);
639        let timeout_str = format!("{}", timeout_error);
640        assert!(timeout_str.contains("Request timeout after 5000ms"));
641        assert!(timeout_str.contains("/api/test"));
642
643        let param_error = Error::invalid_parameter("amount", "Must be positive");
644        let param_str = format!("{}", param_error);
645        assert!(param_str.contains("Invalid parameter 'amount'"));
646        assert!(param_str.contains("Must be positive"));
647    }
648
649    #[test]
650    fn test_error_from_conversions() {
651        // Test From<CryptoError> conversion
652        let crypto_error = CryptoError::invalid_private_key("Invalid key");
653        let error: Error = crypto_error.into();
654        assert!(matches!(error, Error::Crypto(_)));
655
656        // Test From<ConfigError> conversion
657        let config_error = ConfigError::invalid_timeout("Invalid timeout");
658        let error: Error = config_error.into();
659        assert!(matches!(error, Error::Config(_)));
660
661        // Test From<TryFromSliceError> conversion
662        // Create a TryFromSliceError by attempting to convert a slice that's too short
663        let result: StdResult<[u8; 4], std::array::TryFromSliceError> =
664            [0u8; 2].as_slice().try_into();
665        let slice_error = result.unwrap_err();
666        let error: Error = slice_error.into();
667        assert!(matches!(
668            error,
669            Error::ArrayConversion {
670                expected: 32,
671                actual: 0
672            }
673        ));
674    }
675
676    #[test]
677    fn test_error_response_structure() {
678        let error_response = ErrorResponse {
679            error_code: "validation_error".to_string(),
680            message: "Invalid input parameters".to_string(),
681        };
682
683        // Test serialization
684        let json = serde_json::to_string(&error_response).expect("Should serialize");
685        assert!(json.contains("validation_error"));
686        assert!(json.contains("Invalid input parameters"));
687
688        // Test deserialization
689        let deserialized: ErrorResponse = serde_json::from_str(&json).expect("Should deserialize");
690        assert_eq!(deserialized.error_code, "validation_error");
691        assert_eq!(deserialized.message, "Invalid input parameters");
692    }
693
694    #[test]
695    fn test_reqwest_error_conversion() {
696        // Note: These tests use mock errors since we can't easily create real reqwest errors
697        // In practice, reqwest errors would be converted automatically via the From trait
698
699        // Test that the From<reqwest::Error> implementation exists and compiles
700        // This ensures the conversion logic is syntactically correct
701        // The implementation is tested indirectly through other integration tests
702    }
703
704    #[test]
705    fn test_error_debug_formatting() {
706        let error = Error::api(
707            500,
708            "server_error".to_string(),
709            "Internal error".to_string(),
710        );
711        let debug_str = format!("{:?}", error);
712        assert!(debug_str.contains("Api"));
713        assert!(debug_str.contains("status_code: 500"));
714
715        let crypto_error = CryptoError::invalid_private_key("Invalid format");
716        let crypto_debug = format!("{:?}", crypto_error);
717        assert!(crypto_debug.contains("InvalidPrivateKey"));
718
719        let config_error = ConfigError::invalid_network("Unknown network");
720        let config_debug = format!("{:?}", config_error);
721        assert!(config_debug.contains("InvalidNetwork"));
722    }
723
724    #[test]
725    fn test_result_type_alias() {
726        // Test that our Result type alias works correctly
727        let success_result: Result<String> = Ok("success".to_string());
728        assert!(success_result.is_ok());
729        if let Ok(value) = success_result {
730            assert_eq!(value, "success");
731        }
732
733        let error_result: Result<String> = Err(Error::custom("test error"));
734        assert!(error_result.is_err());
735        if let Err(error) = error_result {
736            assert!(matches!(error, Error::Custom(_)));
737        }
738    }
739
740    #[test]
741    fn test_error_source_chain() {
742        // Test that errors can be chained properly using the source() method from std::error::Error
743        let crypto_error = CryptoError::invalid_private_key("Base crypto error");
744        let main_error = Error::Crypto(crypto_error);
745
746        // The main error should have the crypto error as its source
747        assert!(main_error.source().is_some());
748
749        let config_error = ConfigError::invalid_timeout("Base config error");
750        let main_error = Error::Config(config_error);
751
752        // The main error should have the config error as its source
753        assert!(main_error.source().is_some());
754    }
755}