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