Skip to main content

txgate_core/
error.rs

1//! Error types for the `TxGate` signing service.
2//!
3//! This module provides comprehensive error types for all failure modes
4//! in the `TxGate` system, organized by domain:
5//!
6//! - [`ParseError`] - Transaction parsing failures
7//! - [`SignError`] - Signing operation failures
8//! - [`StoreError`] - Key storage failures
9//! - [`PolicyError`] - Policy evaluation failures
10//! - [`ConfigError`] - Configuration failures
11//! - [`TxGateError`] - Top-level error that wraps all error types
12//!
13//! # Example
14//!
15//! ```rust
16//! use txgate_core::error::{ParseError, TxGateError};
17//!
18//! fn parse_transaction(data: &[u8]) -> Result<(), TxGateError> {
19//!     if data.is_empty() {
20//!         return Err(ParseError::MalformedTransaction {
21//!             context: "empty transaction data".to_string(),
22//!         }.into());
23//!     }
24//!     Ok(())
25//! }
26//! ```
27
28use std::fmt;
29
30/// Top-level error type for the `TxGate` signing service.
31///
32/// This enum wraps all domain-specific error types and provides
33/// automatic conversion via the `#[from]` attribute.
34#[derive(Debug, thiserror::Error)]
35pub enum TxGateError {
36    /// Transaction parsing failed.
37    #[error("Parse error: {0}")]
38    Parse(#[from] ParseError),
39
40    /// Policy denied the transaction.
41    #[error("Policy denied: {rule} - {reason}")]
42    PolicyDenied {
43        /// The policy rule that denied the request.
44        rule: String,
45        /// Human-readable reason for denial.
46        reason: String,
47    },
48
49    /// Signing operation failed.
50    #[error("Signing error: {0}")]
51    Sign(#[from] SignError),
52
53    /// Key storage operation failed.
54    #[error("Storage error: {0}")]
55    Store(#[from] StoreError),
56
57    /// Configuration error.
58    #[error("Configuration error: {0}")]
59    Config(#[from] ConfigError),
60
61    /// Policy evaluation error (not denial, but evaluation failure).
62    #[error("Policy error: {0}")]
63    Policy(#[from] PolicyError),
64}
65
66impl TxGateError {
67    /// Create a policy denied error.
68    #[must_use]
69    pub fn policy_denied(rule: impl Into<String>, reason: impl Into<String>) -> Self {
70        Self::PolicyDenied {
71            rule: rule.into(),
72            reason: reason.into(),
73        }
74    }
75}
76
77/// JSON-RPC 2.0 error codes.
78///
79/// Standard codes from -32700 to -32600 are defined by the JSON-RPC spec.
80/// Application-specific codes use the -32000 to -32099 range.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82#[repr(i32)]
83pub enum RpcErrorCode {
84    /// Invalid JSON was received by the server.
85    ParseFailed = -32700,
86    /// The JSON sent is not a valid Request object.
87    InvalidRequest = -32600,
88    /// The method does not exist or is not available.
89    MethodNotFound = -32601,
90    /// Invalid method parameter(s).
91    InvalidParams = -32602,
92    /// Internal JSON-RPC error.
93    InternalError = -32603,
94
95    // Application-specific codes (-32000 to -32099)
96    /// Policy denied the transaction.
97    PolicyDenied = -32001,
98    /// The requested chain is not supported.
99    ChainNotSupported = -32002,
100    /// The requested key was not found.
101    KeyNotFound = -32003,
102    /// The signing operation failed.
103    SignatureFailed = -32004,
104}
105
106impl RpcErrorCode {
107    /// Get the numeric error code value.
108    #[must_use]
109    pub const fn code(self) -> i32 {
110        self as i32
111    }
112
113    /// Get a human-readable message for this error code.
114    #[must_use]
115    pub const fn message(self) -> &'static str {
116        match self {
117            Self::ParseFailed => "Parse error",
118            Self::InvalidRequest => "Invalid Request",
119            Self::MethodNotFound => "Method not found",
120            Self::InvalidParams => "Invalid params",
121            Self::InternalError => "Internal error",
122            Self::PolicyDenied => "Policy denied",
123            Self::ChainNotSupported => "Chain not supported",
124            Self::KeyNotFound => "Key not found",
125            Self::SignatureFailed => "Signature failed",
126        }
127    }
128}
129
130impl fmt::Display for RpcErrorCode {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{} ({})", self.message(), self.code())
133    }
134}
135
136impl From<&TxGateError> for RpcErrorCode {
137    fn from(error: &TxGateError) -> Self {
138        match error {
139            TxGateError::Parse(ParseError::UnsupportedChain { .. }) => Self::ChainNotSupported,
140            TxGateError::Parse(_) => Self::ParseFailed,
141            TxGateError::PolicyDenied { .. } | TxGateError::Policy(_) => Self::PolicyDenied,
142            TxGateError::Sign(SignError::KeyNotFound { .. })
143            | TxGateError::Store(StoreError::KeyNotFound { .. }) => Self::KeyNotFound,
144            TxGateError::Sign(_) => Self::SignatureFailed,
145            TxGateError::Store(_) | TxGateError::Config(_) => Self::InternalError,
146        }
147    }
148}
149
150impl From<TxGateError> for RpcErrorCode {
151    fn from(error: TxGateError) -> Self {
152        Self::from(&error)
153    }
154}
155
156// ============================================================================
157// ParseError
158// ============================================================================
159
160/// Errors that can occur during transaction parsing.
161///
162/// These errors indicate that the input transaction data could not be
163/// parsed into a structured format for policy evaluation.
164#[derive(Debug, thiserror::Error)]
165pub enum ParseError {
166    /// The transaction type byte is not recognized.
167    #[error("unknown transaction type")]
168    UnknownTxType,
169
170    /// RLP decoding failed.
171    #[error("RLP decoding failed: {context}")]
172    InvalidRlp {
173        /// Context about what was being decoded.
174        context: String,
175    },
176
177    /// The transaction structure is malformed.
178    #[error("malformed transaction: {context}")]
179    MalformedTransaction {
180        /// Context about what was malformed.
181        context: String,
182    },
183
184    /// The calldata structure is invalid (e.g., wrong length for ERC-20).
185    #[error("malformed calldata")]
186    MalformedCalldata,
187
188    /// The chain is not supported by this signer.
189    #[error("unsupported chain: {chain}")]
190    UnsupportedChain {
191        /// The chain identifier that was requested.
192        chain: String,
193    },
194
195    /// The address format is invalid for the chain.
196    #[error("invalid address: {address}")]
197    InvalidAddress {
198        /// The malformed address string.
199        address: String,
200    },
201}
202
203impl ParseError {
204    /// Create an `InvalidRlp` error with context.
205    #[must_use]
206    pub fn invalid_rlp(context: impl Into<String>) -> Self {
207        Self::InvalidRlp {
208            context: context.into(),
209        }
210    }
211
212    /// Create a `MalformedTransaction` error with context.
213    #[must_use]
214    pub fn malformed_transaction(context: impl Into<String>) -> Self {
215        Self::MalformedTransaction {
216            context: context.into(),
217        }
218    }
219
220    /// Create an `UnsupportedChain` error.
221    #[must_use]
222    pub fn unsupported_chain(chain: impl Into<String>) -> Self {
223        Self::UnsupportedChain {
224            chain: chain.into(),
225        }
226    }
227
228    /// Create an `InvalidAddress` error.
229    #[must_use]
230    pub fn invalid_address(address: impl Into<String>) -> Self {
231        Self::InvalidAddress {
232            address: address.into(),
233        }
234    }
235}
236
237// ============================================================================
238// SignError
239// ============================================================================
240
241/// Errors that can occur during signing operations.
242///
243/// These errors indicate failures in the cryptographic signing process,
244/// including key access and algorithm mismatches.
245#[derive(Debug, thiserror::Error)]
246pub enum SignError {
247    /// The requested key does not exist in the key store.
248    #[error("key not found: {name}")]
249    KeyNotFound {
250        /// The name of the key that was not found.
251        name: String,
252    },
253
254    /// The key material is invalid or corrupted.
255    #[error("invalid key material")]
256    InvalidKey,
257
258    /// The signing operation failed.
259    #[error("signature failed: {context}")]
260    SignatureFailed {
261        /// Context about why signing failed.
262        context: String,
263    },
264
265    /// The key uses a different curve than expected.
266    #[error("wrong curve: expected {expected}, got {actual}")]
267    WrongCurve {
268        /// The expected curve name (e.g., "secp256k1").
269        expected: String,
270        /// The actual curve name of the key.
271        actual: String,
272    },
273}
274
275impl SignError {
276    /// Create a `KeyNotFound` error.
277    #[must_use]
278    pub fn key_not_found(name: impl Into<String>) -> Self {
279        Self::KeyNotFound { name: name.into() }
280    }
281
282    /// Create a `SignatureFailed` error with context.
283    #[must_use]
284    pub fn signature_failed(context: impl Into<String>) -> Self {
285        Self::SignatureFailed {
286            context: context.into(),
287        }
288    }
289
290    /// Create a `WrongCurve` error.
291    #[must_use]
292    pub fn wrong_curve(expected: impl Into<String>, actual: impl Into<String>) -> Self {
293        Self::WrongCurve {
294            expected: expected.into(),
295            actual: actual.into(),
296        }
297    }
298}
299
300// ============================================================================
301// StoreError
302// ============================================================================
303
304/// Errors that can occur during key storage operations.
305///
306/// These errors indicate failures in reading, writing, or managing
307/// encrypted key files.
308#[derive(Debug, thiserror::Error)]
309pub enum StoreError {
310    /// File system I/O error.
311    #[error("I/O error: {0}")]
312    IoError(#[source] std::io::Error),
313
314    /// Key encryption failed.
315    #[error("encryption failed")]
316    EncryptionFailed,
317
318    /// Key decryption failed (likely wrong password).
319    #[error("decryption failed (wrong password?)")]
320    DecryptionFailed,
321
322    /// A key with this name already exists.
323    #[error("key already exists: {name}")]
324    KeyExists {
325        /// The name of the existing key.
326        name: String,
327    },
328
329    /// The requested key does not exist.
330    #[error("key not found: {name}")]
331    KeyNotFound {
332        /// The name of the key that was not found.
333        name: String,
334    },
335
336    /// The key file format is invalid.
337    #[error("invalid key file format")]
338    InvalidFormat,
339
340    /// Insufficient file system permissions.
341    #[error("permission denied")]
342    PermissionDenied,
343}
344
345impl StoreError {
346    /// Create an `IoError` from a `std::io::Error`.
347    #[must_use]
348    pub const fn io_error(error: std::io::Error) -> Self {
349        Self::IoError(error)
350    }
351
352    /// Create a `KeyExists` error.
353    #[must_use]
354    pub fn key_exists(name: impl Into<String>) -> Self {
355        Self::KeyExists { name: name.into() }
356    }
357
358    /// Create a `KeyNotFound` error.
359    #[must_use]
360    pub fn key_not_found(name: impl Into<String>) -> Self {
361        Self::KeyNotFound { name: name.into() }
362    }
363}
364
365impl From<std::io::Error> for StoreError {
366    fn from(error: std::io::Error) -> Self {
367        // Map specific I/O errors to more specific store errors
368        match error.kind() {
369            std::io::ErrorKind::PermissionDenied => Self::PermissionDenied,
370            std::io::ErrorKind::NotFound => Self::InvalidFormat,
371            _ => Self::IoError(error),
372        }
373    }
374}
375
376// ============================================================================
377// PolicyError
378// ============================================================================
379
380/// Errors that can occur during policy evaluation.
381///
382/// Note: These are evaluation errors, not policy denials. For denials,
383/// see [`TxGateError::PolicyDenied`].
384#[derive(Debug, thiserror::Error)]
385pub enum PolicyError {
386    /// The address is on the blacklist.
387    #[error("address is blacklisted: {address}")]
388    Blacklisted {
389        /// The blacklisted address.
390        address: String,
391    },
392
393    /// The address is not on the whitelist (when whitelist is enabled).
394    #[error("address not in whitelist: {address}")]
395    NotWhitelisted {
396        /// The address that is not whitelisted.
397        address: String,
398    },
399
400    /// The transaction amount exceeds the per-transaction limit.
401    #[error("exceeds transaction limit: limit={limit}, amount={amount}")]
402    ExceedsTransactionLimit {
403        /// The configured limit.
404        limit: String,
405        /// The requested amount.
406        amount: String,
407    },
408
409    /// The policy configuration is invalid.
410    #[error("invalid configuration: {context}")]
411    InvalidConfiguration {
412        /// Context about what is invalid.
413        context: String,
414    },
415}
416
417impl PolicyError {
418    /// Create a `Blacklisted` error.
419    #[must_use]
420    pub fn blacklisted(address: impl Into<String>) -> Self {
421        Self::Blacklisted {
422            address: address.into(),
423        }
424    }
425
426    /// Create a `NotWhitelisted` error.
427    #[must_use]
428    pub fn not_whitelisted(address: impl Into<String>) -> Self {
429        Self::NotWhitelisted {
430            address: address.into(),
431        }
432    }
433
434    /// Create an `ExceedsTransactionLimit` error.
435    #[must_use]
436    pub fn exceeds_transaction_limit(limit: impl Into<String>, amount: impl Into<String>) -> Self {
437        Self::ExceedsTransactionLimit {
438            limit: limit.into(),
439            amount: amount.into(),
440        }
441    }
442
443    /// Create an `InvalidConfiguration` error.
444    #[must_use]
445    pub fn invalid_configuration(context: impl Into<String>) -> Self {
446        Self::InvalidConfiguration {
447            context: context.into(),
448        }
449    }
450
451    /// Convert this policy error into a denial reason string.
452    #[must_use]
453    pub fn denial_reason(&self) -> String {
454        self.to_string()
455    }
456
457    /// Get the rule name that triggered this error.
458    #[must_use]
459    pub const fn rule_name(&self) -> &'static str {
460        match self {
461            Self::Blacklisted { .. } => "blacklist",
462            Self::NotWhitelisted { .. } => "whitelist",
463            Self::ExceedsTransactionLimit { .. } => "tx_limit",
464            Self::InvalidConfiguration { .. } => "configuration",
465        }
466    }
467}
468
469// ============================================================================
470// ConfigError
471// ============================================================================
472
473/// Errors that can occur during configuration loading.
474#[derive(Debug, thiserror::Error)]
475pub enum ConfigError {
476    /// The configuration file was not found.
477    #[error("configuration file not found: {path}")]
478    FileNotFound {
479        /// The path that was not found.
480        path: String,
481    },
482
483    /// Failed to parse the configuration file.
484    #[error("failed to parse configuration: {context}")]
485    ParseFailed {
486        /// Context about the parsing failure.
487        context: String,
488    },
489
490    /// A configuration value is invalid.
491    #[error("invalid value for {field}: {value}")]
492    InvalidValue {
493        /// The field name with the invalid value.
494        field: String,
495        /// The invalid value.
496        value: String,
497    },
498
499    /// A required configuration field is missing.
500    #[error("missing required field: {field}")]
501    MissingField {
502        /// The name of the missing field.
503        field: String,
504    },
505
506    /// File system I/O error.
507    #[error("I/O error: {context}")]
508    Io {
509        /// Context about the I/O operation that failed.
510        context: String,
511        /// The underlying I/O error.
512        #[source]
513        source: std::io::Error,
514    },
515
516    /// Home directory could not be determined.
517    #[error("could not determine home directory")]
518    NoHomeDirectory,
519}
520
521impl ConfigError {
522    /// Create a `FileNotFound` error.
523    #[must_use]
524    pub fn file_not_found(path: impl Into<String>) -> Self {
525        Self::FileNotFound { path: path.into() }
526    }
527
528    /// Create a `ParseFailed` error.
529    #[must_use]
530    pub fn parse_failed(context: impl Into<String>) -> Self {
531        Self::ParseFailed {
532            context: context.into(),
533        }
534    }
535
536    /// Create an `InvalidValue` error.
537    #[must_use]
538    pub fn invalid_value(field: impl Into<String>, value: impl Into<String>) -> Self {
539        Self::InvalidValue {
540            field: field.into(),
541            value: value.into(),
542        }
543    }
544
545    /// Create a `MissingField` error.
546    #[must_use]
547    pub fn missing_field(field: impl Into<String>) -> Self {
548        Self::MissingField {
549            field: field.into(),
550        }
551    }
552
553    /// Create an `Io` error.
554    #[must_use]
555    pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
556        Self::Io {
557            context: context.into(),
558            source,
559        }
560    }
561
562    /// Create a `NoHomeDirectory` error.
563    #[must_use]
564    pub const fn no_home_directory() -> Self {
565        Self::NoHomeDirectory
566    }
567}
568
569// ============================================================================
570// Result type aliases
571// ============================================================================
572
573/// A `Result` type alias using [`TxGateError`] as the error type.
574pub type Result<T> = std::result::Result<T, TxGateError>;
575
576/// A `Result` type alias for parsing operations.
577pub type ParseResult<T> = std::result::Result<T, ParseError>;
578
579/// A `Result` type alias for signing operations.
580pub type SignResult<T> = std::result::Result<T, SignError>;
581
582/// A `Result` type alias for storage operations.
583pub type StoreResult<T> = std::result::Result<T, StoreError>;
584
585/// A `Result` type alias for policy operations.
586pub type PolicyResult<T> = std::result::Result<T, PolicyError>;
587
588/// A `Result` type alias for configuration operations.
589pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
590
591// ============================================================================
592// Unit Tests
593// ============================================================================
594
595#[cfg(test)]
596mod tests {
597    #![allow(clippy::items_after_statements)]
598
599    use super::*;
600
601    // ------------------------------------------------------------------------
602    // TxGateError tests
603    // ------------------------------------------------------------------------
604
605    #[test]
606    fn test_txgate_error_from_parse_error() {
607        let parse_err = ParseError::UnknownTxType;
608        let txgate_err: TxGateError = parse_err.into();
609
610        assert!(matches!(
611            txgate_err,
612            TxGateError::Parse(ParseError::UnknownTxType)
613        ));
614        assert_eq!(
615            txgate_err.to_string(),
616            "Parse error: unknown transaction type"
617        );
618    }
619
620    #[test]
621    fn test_txgate_error_from_sign_error() {
622        let sign_err = SignError::key_not_found("test-key");
623        let txgate_err: TxGateError = sign_err.into();
624
625        assert!(matches!(
626            txgate_err,
627            TxGateError::Sign(SignError::KeyNotFound { .. })
628        ));
629        assert_eq!(
630            txgate_err.to_string(),
631            "Signing error: key not found: test-key"
632        );
633    }
634
635    #[test]
636    fn test_txgate_error_from_store_error() {
637        let store_err = StoreError::DecryptionFailed;
638        let txgate_err: TxGateError = store_err.into();
639
640        assert!(matches!(
641            txgate_err,
642            TxGateError::Store(StoreError::DecryptionFailed)
643        ));
644        assert_eq!(
645            txgate_err.to_string(),
646            "Storage error: decryption failed (wrong password?)"
647        );
648    }
649
650    #[test]
651    fn test_txgate_error_from_policy_error() {
652        let policy_err = PolicyError::blacklisted("0x1234");
653        let txgate_err: TxGateError = policy_err.into();
654
655        assert!(matches!(
656            txgate_err,
657            TxGateError::Policy(PolicyError::Blacklisted { .. })
658        ));
659        assert_eq!(
660            txgate_err.to_string(),
661            "Policy error: address is blacklisted: 0x1234"
662        );
663    }
664
665    #[test]
666    fn test_txgate_error_from_config_error() {
667        let config_err = ConfigError::missing_field("api_key");
668        let txgate_err: TxGateError = config_err.into();
669
670        assert!(matches!(
671            txgate_err,
672            TxGateError::Config(ConfigError::MissingField { .. })
673        ));
674        assert_eq!(
675            txgate_err.to_string(),
676            "Configuration error: missing required field: api_key"
677        );
678    }
679
680    #[test]
681    fn test_txgate_error_policy_denied() {
682        let err = TxGateError::policy_denied("whitelist", "address not in whitelist");
683
684        assert!(matches!(err, TxGateError::PolicyDenied { .. }));
685        assert_eq!(
686            err.to_string(),
687            "Policy denied: whitelist - address not in whitelist"
688        );
689    }
690
691    // ------------------------------------------------------------------------
692    // RpcErrorCode tests
693    // ------------------------------------------------------------------------
694
695    #[test]
696    fn test_rpc_error_code_values() {
697        assert_eq!(RpcErrorCode::ParseFailed.code(), -32700);
698        assert_eq!(RpcErrorCode::InvalidRequest.code(), -32600);
699        assert_eq!(RpcErrorCode::MethodNotFound.code(), -32601);
700        assert_eq!(RpcErrorCode::InvalidParams.code(), -32602);
701        assert_eq!(RpcErrorCode::InternalError.code(), -32603);
702        assert_eq!(RpcErrorCode::PolicyDenied.code(), -32001);
703        assert_eq!(RpcErrorCode::ChainNotSupported.code(), -32002);
704        assert_eq!(RpcErrorCode::KeyNotFound.code(), -32003);
705        assert_eq!(RpcErrorCode::SignatureFailed.code(), -32004);
706    }
707
708    #[test]
709    fn test_rpc_error_code_messages() {
710        assert_eq!(RpcErrorCode::ParseFailed.message(), "Parse error");
711        assert_eq!(RpcErrorCode::InvalidRequest.message(), "Invalid Request");
712        assert_eq!(RpcErrorCode::MethodNotFound.message(), "Method not found");
713        assert_eq!(RpcErrorCode::InvalidParams.message(), "Invalid params");
714        assert_eq!(RpcErrorCode::InternalError.message(), "Internal error");
715        assert_eq!(RpcErrorCode::PolicyDenied.message(), "Policy denied");
716        assert_eq!(
717            RpcErrorCode::ChainNotSupported.message(),
718            "Chain not supported"
719        );
720        assert_eq!(RpcErrorCode::KeyNotFound.message(), "Key not found");
721        assert_eq!(RpcErrorCode::SignatureFailed.message(), "Signature failed");
722    }
723
724    #[test]
725    fn test_rpc_error_code_display() {
726        assert_eq!(
727            RpcErrorCode::ParseFailed.to_string(),
728            "Parse error (-32700)"
729        );
730        assert_eq!(
731            RpcErrorCode::KeyNotFound.to_string(),
732            "Key not found (-32003)"
733        );
734    }
735
736    #[test]
737    fn test_rpc_error_code_from_txgate_error() {
738        // Parse errors
739        let err = TxGateError::Parse(ParseError::UnknownTxType);
740        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::ParseFailed);
741
742        let err = TxGateError::Parse(ParseError::unsupported_chain("unknown"));
743        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::ChainNotSupported);
744
745        // Sign errors
746        let err = TxGateError::Sign(SignError::key_not_found("test"));
747        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::KeyNotFound);
748
749        let err = TxGateError::Sign(SignError::InvalidKey);
750        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::SignatureFailed);
751
752        // Policy errors
753        let err = TxGateError::policy_denied("whitelist", "not allowed");
754        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::PolicyDenied);
755
756        let err = TxGateError::Policy(PolicyError::blacklisted("0x1234"));
757        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::PolicyDenied);
758
759        // Store errors
760        let err = TxGateError::Store(StoreError::key_not_found("test"));
761        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::KeyNotFound);
762
763        let err = TxGateError::Store(StoreError::DecryptionFailed);
764        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::InternalError);
765
766        // Config errors
767        let err = TxGateError::Config(ConfigError::missing_field("test"));
768        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::InternalError);
769    }
770
771    // ------------------------------------------------------------------------
772    // ParseError tests
773    // ------------------------------------------------------------------------
774
775    #[test]
776    fn test_parse_error_display() {
777        assert_eq!(
778            ParseError::UnknownTxType.to_string(),
779            "unknown transaction type"
780        );
781
782        assert_eq!(
783            ParseError::invalid_rlp("failed to decode nonce").to_string(),
784            "RLP decoding failed: failed to decode nonce"
785        );
786
787        assert_eq!(
788            ParseError::malformed_transaction("missing to field").to_string(),
789            "malformed transaction: missing to field"
790        );
791
792        assert_eq!(
793            ParseError::MalformedCalldata.to_string(),
794            "malformed calldata"
795        );
796
797        assert_eq!(
798            ParseError::unsupported_chain("cosmos").to_string(),
799            "unsupported chain: cosmos"
800        );
801
802        assert_eq!(
803            ParseError::invalid_address("0xinvalid").to_string(),
804            "invalid address: 0xinvalid"
805        );
806    }
807
808    #[test]
809    fn test_parse_error_constructors() {
810        let err = ParseError::invalid_rlp("test context");
811        assert!(matches!(err, ParseError::InvalidRlp { context } if context == "test context"));
812
813        let err = ParseError::malformed_transaction("test context");
814        assert!(
815            matches!(err, ParseError::MalformedTransaction { context } if context == "test context")
816        );
817
818        let err = ParseError::unsupported_chain("test-chain");
819        assert!(matches!(err, ParseError::UnsupportedChain { chain } if chain == "test-chain"));
820
821        let err = ParseError::invalid_address("bad-addr");
822        assert!(matches!(err, ParseError::InvalidAddress { address } if address == "bad-addr"));
823    }
824
825    // ------------------------------------------------------------------------
826    // SignError tests
827    // ------------------------------------------------------------------------
828
829    #[test]
830    fn test_sign_error_display() {
831        assert_eq!(
832            SignError::key_not_found("my-key").to_string(),
833            "key not found: my-key"
834        );
835
836        assert_eq!(SignError::InvalidKey.to_string(), "invalid key material");
837
838        assert_eq!(
839            SignError::signature_failed("hash mismatch").to_string(),
840            "signature failed: hash mismatch"
841        );
842
843        assert_eq!(
844            SignError::wrong_curve("secp256k1", "ed25519").to_string(),
845            "wrong curve: expected secp256k1, got ed25519"
846        );
847    }
848
849    #[test]
850    fn test_sign_error_constructors() {
851        let err = SignError::key_not_found("test-key");
852        assert!(matches!(err, SignError::KeyNotFound { name } if name == "test-key"));
853
854        let err = SignError::signature_failed("test reason");
855        assert!(matches!(err, SignError::SignatureFailed { context } if context == "test reason"));
856
857        let err = SignError::wrong_curve("secp256k1", "ed25519");
858        assert!(
859            matches!(err, SignError::WrongCurve { expected, actual } if expected == "secp256k1" && actual == "ed25519")
860        );
861    }
862
863    // ------------------------------------------------------------------------
864    // StoreError tests
865    // ------------------------------------------------------------------------
866
867    #[test]
868    fn test_store_error_display() {
869        assert_eq!(
870            StoreError::EncryptionFailed.to_string(),
871            "encryption failed"
872        );
873
874        assert_eq!(
875            StoreError::DecryptionFailed.to_string(),
876            "decryption failed (wrong password?)"
877        );
878
879        assert_eq!(
880            StoreError::key_exists("existing-key").to_string(),
881            "key already exists: existing-key"
882        );
883
884        assert_eq!(
885            StoreError::key_not_found("missing-key").to_string(),
886            "key not found: missing-key"
887        );
888
889        assert_eq!(
890            StoreError::InvalidFormat.to_string(),
891            "invalid key file format"
892        );
893
894        assert_eq!(
895            StoreError::PermissionDenied.to_string(),
896            "permission denied"
897        );
898    }
899
900    #[test]
901    fn test_store_error_from_io_error() {
902        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
903        let store_err: StoreError = io_err.into();
904        assert!(matches!(store_err, StoreError::PermissionDenied));
905
906        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
907        let store_err: StoreError = io_err.into();
908        assert!(matches!(store_err, StoreError::InvalidFormat));
909
910        let io_err = std::io::Error::other("something else");
911        let store_err: StoreError = io_err.into();
912        assert!(matches!(store_err, StoreError::IoError(_)));
913    }
914
915    #[test]
916    fn test_store_error_constructors() {
917        let err = StoreError::key_exists("test-key");
918        assert!(matches!(err, StoreError::KeyExists { name } if name == "test-key"));
919
920        let err = StoreError::key_not_found("test-key");
921        assert!(matches!(err, StoreError::KeyNotFound { name } if name == "test-key"));
922    }
923
924    // ------------------------------------------------------------------------
925    // PolicyError tests
926    // ------------------------------------------------------------------------
927
928    #[test]
929    fn test_policy_error_display() {
930        assert_eq!(
931            PolicyError::blacklisted("0x1234").to_string(),
932            "address is blacklisted: 0x1234"
933        );
934
935        assert_eq!(
936            PolicyError::not_whitelisted("0x5678").to_string(),
937            "address not in whitelist: 0x5678"
938        );
939
940        assert_eq!(
941            PolicyError::exceeds_transaction_limit("5 ETH", "10 ETH").to_string(),
942            "exceeds transaction limit: limit=5 ETH, amount=10 ETH"
943        );
944
945        assert_eq!(
946            PolicyError::invalid_configuration("missing whitelist").to_string(),
947            "invalid configuration: missing whitelist"
948        );
949    }
950
951    #[test]
952    fn test_policy_error_rule_names() {
953        assert_eq!(PolicyError::blacklisted("x").rule_name(), "blacklist");
954        assert_eq!(PolicyError::not_whitelisted("x").rule_name(), "whitelist");
955        assert_eq!(
956            PolicyError::exceeds_transaction_limit("1", "2").rule_name(),
957            "tx_limit"
958        );
959        assert_eq!(
960            PolicyError::invalid_configuration("x").rule_name(),
961            "configuration"
962        );
963    }
964
965    #[test]
966    fn test_policy_error_denial_reason() {
967        let err = PolicyError::blacklisted("0x1234");
968        assert_eq!(err.denial_reason(), "address is blacklisted: 0x1234");
969    }
970
971    // ------------------------------------------------------------------------
972    // ConfigError tests
973    // ------------------------------------------------------------------------
974
975    #[test]
976    fn test_config_error_display() {
977        assert_eq!(
978            ConfigError::file_not_found("/path/to/config.toml").to_string(),
979            "configuration file not found: /path/to/config.toml"
980        );
981
982        assert_eq!(
983            ConfigError::parse_failed("invalid TOML syntax").to_string(),
984            "failed to parse configuration: invalid TOML syntax"
985        );
986
987        assert_eq!(
988            ConfigError::invalid_value("port", "-1").to_string(),
989            "invalid value for port: -1"
990        );
991
992        assert_eq!(
993            ConfigError::missing_field("api_key").to_string(),
994            "missing required field: api_key"
995        );
996    }
997
998    #[test]
999    fn test_config_error_constructors() {
1000        let err = ConfigError::file_not_found("/test/path");
1001        assert!(matches!(err, ConfigError::FileNotFound { path } if path == "/test/path"));
1002
1003        let err = ConfigError::parse_failed("test context");
1004        assert!(matches!(err, ConfigError::ParseFailed { context } if context == "test context"));
1005
1006        let err = ConfigError::invalid_value("field", "value");
1007        assert!(
1008            matches!(err, ConfigError::InvalidValue { field, value } if field == "field" && value == "value")
1009        );
1010
1011        let err = ConfigError::missing_field("test_field");
1012        assert!(matches!(err, ConfigError::MissingField { field } if field == "test_field"));
1013
1014        let io_err = std::io::Error::other("test io error");
1015        let err = ConfigError::io("reading config", io_err);
1016        assert!(matches!(err, ConfigError::Io { context, .. } if context == "reading config"));
1017
1018        let err = ConfigError::no_home_directory();
1019        assert!(matches!(err, ConfigError::NoHomeDirectory));
1020    }
1021
1022    #[test]
1023    fn test_config_error_io_display() {
1024        let io_err = std::io::Error::other("underlying error");
1025        let err = ConfigError::io("reading config file", io_err);
1026        assert_eq!(err.to_string(), "I/O error: reading config file");
1027    }
1028
1029    #[test]
1030    fn test_config_error_no_home_directory_display() {
1031        let err = ConfigError::no_home_directory();
1032        assert_eq!(err.to_string(), "could not determine home directory");
1033    }
1034
1035    #[test]
1036    fn test_config_error_io_source() {
1037        let io_err = std::io::Error::other("test error");
1038        let config_err = ConfigError::io("test context", io_err);
1039
1040        // Verify the source chain works
1041        use std::error::Error;
1042        assert!(config_err.source().is_some());
1043    }
1044
1045    // ------------------------------------------------------------------------
1046    // Error trait implementation tests
1047    // ------------------------------------------------------------------------
1048
1049    #[test]
1050    fn test_store_error_source() {
1051        let io_err = std::io::Error::other("test error");
1052        let store_err = StoreError::io_error(io_err);
1053
1054        // Verify the source chain works
1055        use std::error::Error;
1056        assert!(store_err.source().is_some());
1057    }
1058
1059    #[test]
1060    fn test_txgate_error_is_send_sync() {
1061        fn assert_send_sync<T: Send + Sync>() {}
1062        assert_send_sync::<TxGateError>();
1063        assert_send_sync::<ParseError>();
1064        assert_send_sync::<SignError>();
1065        assert_send_sync::<StoreError>();
1066        assert_send_sync::<PolicyError>();
1067        assert_send_sync::<ConfigError>();
1068    }
1069}