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 transaction would exceed the daily limit.
410    #[error("exceeds daily limit: limit={limit}, current={current}, amount={amount}")]
411    ExceedsDailyLimit {
412        /// The configured daily limit.
413        limit: String,
414        /// The current daily total.
415        current: String,
416        /// The requested amount.
417        amount: String,
418    },
419
420    /// The policy configuration is invalid.
421    #[error("invalid configuration: {context}")]
422    InvalidConfiguration {
423        /// Context about what is invalid.
424        context: String,
425    },
426}
427
428impl PolicyError {
429    /// Create a `Blacklisted` error.
430    #[must_use]
431    pub fn blacklisted(address: impl Into<String>) -> Self {
432        Self::Blacklisted {
433            address: address.into(),
434        }
435    }
436
437    /// Create a `NotWhitelisted` error.
438    #[must_use]
439    pub fn not_whitelisted(address: impl Into<String>) -> Self {
440        Self::NotWhitelisted {
441            address: address.into(),
442        }
443    }
444
445    /// Create an `ExceedsTransactionLimit` error.
446    #[must_use]
447    pub fn exceeds_transaction_limit(limit: impl Into<String>, amount: impl Into<String>) -> Self {
448        Self::ExceedsTransactionLimit {
449            limit: limit.into(),
450            amount: amount.into(),
451        }
452    }
453
454    /// Create an `ExceedsDailyLimit` error.
455    #[must_use]
456    pub fn exceeds_daily_limit(
457        limit: impl Into<String>,
458        current: impl Into<String>,
459        amount: impl Into<String>,
460    ) -> Self {
461        Self::ExceedsDailyLimit {
462            limit: limit.into(),
463            current: current.into(),
464            amount: amount.into(),
465        }
466    }
467
468    /// Create an `InvalidConfiguration` error.
469    #[must_use]
470    pub fn invalid_configuration(context: impl Into<String>) -> Self {
471        Self::InvalidConfiguration {
472            context: context.into(),
473        }
474    }
475
476    /// Convert this policy error into a denial reason string.
477    #[must_use]
478    pub fn denial_reason(&self) -> String {
479        self.to_string()
480    }
481
482    /// Get the rule name that triggered this error.
483    #[must_use]
484    pub const fn rule_name(&self) -> &'static str {
485        match self {
486            Self::Blacklisted { .. } => "blacklist",
487            Self::NotWhitelisted { .. } => "whitelist",
488            Self::ExceedsTransactionLimit { .. } => "tx_limit",
489            Self::ExceedsDailyLimit { .. } => "daily_limit",
490            Self::InvalidConfiguration { .. } => "configuration",
491        }
492    }
493}
494
495// ============================================================================
496// ConfigError
497// ============================================================================
498
499/// Errors that can occur during configuration loading.
500#[derive(Debug, thiserror::Error)]
501pub enum ConfigError {
502    /// The configuration file was not found.
503    #[error("configuration file not found: {path}")]
504    FileNotFound {
505        /// The path that was not found.
506        path: String,
507    },
508
509    /// Failed to parse the configuration file.
510    #[error("failed to parse configuration: {context}")]
511    ParseFailed {
512        /// Context about the parsing failure.
513        context: String,
514    },
515
516    /// A configuration value is invalid.
517    #[error("invalid value for {field}: {value}")]
518    InvalidValue {
519        /// The field name with the invalid value.
520        field: String,
521        /// The invalid value.
522        value: String,
523    },
524
525    /// A required configuration field is missing.
526    #[error("missing required field: {field}")]
527    MissingField {
528        /// The name of the missing field.
529        field: String,
530    },
531
532    /// File system I/O error.
533    #[error("I/O error: {context}")]
534    Io {
535        /// Context about the I/O operation that failed.
536        context: String,
537        /// The underlying I/O error.
538        #[source]
539        source: std::io::Error,
540    },
541
542    /// Home directory could not be determined.
543    #[error("could not determine home directory")]
544    NoHomeDirectory,
545}
546
547impl ConfigError {
548    /// Create a `FileNotFound` error.
549    #[must_use]
550    pub fn file_not_found(path: impl Into<String>) -> Self {
551        Self::FileNotFound { path: path.into() }
552    }
553
554    /// Create a `ParseFailed` error.
555    #[must_use]
556    pub fn parse_failed(context: impl Into<String>) -> Self {
557        Self::ParseFailed {
558            context: context.into(),
559        }
560    }
561
562    /// Create an `InvalidValue` error.
563    #[must_use]
564    pub fn invalid_value(field: impl Into<String>, value: impl Into<String>) -> Self {
565        Self::InvalidValue {
566            field: field.into(),
567            value: value.into(),
568        }
569    }
570
571    /// Create a `MissingField` error.
572    #[must_use]
573    pub fn missing_field(field: impl Into<String>) -> Self {
574        Self::MissingField {
575            field: field.into(),
576        }
577    }
578
579    /// Create an `Io` error.
580    #[must_use]
581    pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
582        Self::Io {
583            context: context.into(),
584            source,
585        }
586    }
587
588    /// Create a `NoHomeDirectory` error.
589    #[must_use]
590    pub const fn no_home_directory() -> Self {
591        Self::NoHomeDirectory
592    }
593}
594
595// ============================================================================
596// Result type aliases
597// ============================================================================
598
599/// A `Result` type alias using [`TxGateError`] as the error type.
600pub type Result<T> = std::result::Result<T, TxGateError>;
601
602/// A `Result` type alias for parsing operations.
603pub type ParseResult<T> = std::result::Result<T, ParseError>;
604
605/// A `Result` type alias for signing operations.
606pub type SignResult<T> = std::result::Result<T, SignError>;
607
608/// A `Result` type alias for storage operations.
609pub type StoreResult<T> = std::result::Result<T, StoreError>;
610
611/// A `Result` type alias for policy operations.
612pub type PolicyResult<T> = std::result::Result<T, PolicyError>;
613
614/// A `Result` type alias for configuration operations.
615pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
616
617// ============================================================================
618// Unit Tests
619// ============================================================================
620
621#[cfg(test)]
622mod tests {
623    #![allow(clippy::items_after_statements)]
624
625    use super::*;
626
627    // ------------------------------------------------------------------------
628    // TxGateError tests
629    // ------------------------------------------------------------------------
630
631    #[test]
632    fn test_txgate_error_from_parse_error() {
633        let parse_err = ParseError::UnknownTxType;
634        let txgate_err: TxGateError = parse_err.into();
635
636        assert!(matches!(
637            txgate_err,
638            TxGateError::Parse(ParseError::UnknownTxType)
639        ));
640        assert_eq!(
641            txgate_err.to_string(),
642            "Parse error: unknown transaction type"
643        );
644    }
645
646    #[test]
647    fn test_txgate_error_from_sign_error() {
648        let sign_err = SignError::key_not_found("test-key");
649        let txgate_err: TxGateError = sign_err.into();
650
651        assert!(matches!(
652            txgate_err,
653            TxGateError::Sign(SignError::KeyNotFound { .. })
654        ));
655        assert_eq!(
656            txgate_err.to_string(),
657            "Signing error: key not found: test-key"
658        );
659    }
660
661    #[test]
662    fn test_txgate_error_from_store_error() {
663        let store_err = StoreError::DecryptionFailed;
664        let txgate_err: TxGateError = store_err.into();
665
666        assert!(matches!(
667            txgate_err,
668            TxGateError::Store(StoreError::DecryptionFailed)
669        ));
670        assert_eq!(
671            txgate_err.to_string(),
672            "Storage error: decryption failed (wrong password?)"
673        );
674    }
675
676    #[test]
677    fn test_txgate_error_from_policy_error() {
678        let policy_err = PolicyError::blacklisted("0x1234");
679        let txgate_err: TxGateError = policy_err.into();
680
681        assert!(matches!(
682            txgate_err,
683            TxGateError::Policy(PolicyError::Blacklisted { .. })
684        ));
685        assert_eq!(
686            txgate_err.to_string(),
687            "Policy error: address is blacklisted: 0x1234"
688        );
689    }
690
691    #[test]
692    fn test_txgate_error_from_config_error() {
693        let config_err = ConfigError::missing_field("api_key");
694        let txgate_err: TxGateError = config_err.into();
695
696        assert!(matches!(
697            txgate_err,
698            TxGateError::Config(ConfigError::MissingField { .. })
699        ));
700        assert_eq!(
701            txgate_err.to_string(),
702            "Configuration error: missing required field: api_key"
703        );
704    }
705
706    #[test]
707    fn test_txgate_error_policy_denied() {
708        let err = TxGateError::policy_denied("whitelist", "address not in whitelist");
709
710        assert!(matches!(err, TxGateError::PolicyDenied { .. }));
711        assert_eq!(
712            err.to_string(),
713            "Policy denied: whitelist - address not in whitelist"
714        );
715    }
716
717    // ------------------------------------------------------------------------
718    // RpcErrorCode tests
719    // ------------------------------------------------------------------------
720
721    #[test]
722    fn test_rpc_error_code_values() {
723        assert_eq!(RpcErrorCode::ParseFailed.code(), -32700);
724        assert_eq!(RpcErrorCode::InvalidRequest.code(), -32600);
725        assert_eq!(RpcErrorCode::MethodNotFound.code(), -32601);
726        assert_eq!(RpcErrorCode::InvalidParams.code(), -32602);
727        assert_eq!(RpcErrorCode::InternalError.code(), -32603);
728        assert_eq!(RpcErrorCode::PolicyDenied.code(), -32001);
729        assert_eq!(RpcErrorCode::ChainNotSupported.code(), -32002);
730        assert_eq!(RpcErrorCode::KeyNotFound.code(), -32003);
731        assert_eq!(RpcErrorCode::SignatureFailed.code(), -32004);
732    }
733
734    #[test]
735    fn test_rpc_error_code_messages() {
736        assert_eq!(RpcErrorCode::ParseFailed.message(), "Parse error");
737        assert_eq!(RpcErrorCode::InvalidRequest.message(), "Invalid Request");
738        assert_eq!(RpcErrorCode::MethodNotFound.message(), "Method not found");
739        assert_eq!(RpcErrorCode::InvalidParams.message(), "Invalid params");
740        assert_eq!(RpcErrorCode::InternalError.message(), "Internal error");
741        assert_eq!(RpcErrorCode::PolicyDenied.message(), "Policy denied");
742        assert_eq!(
743            RpcErrorCode::ChainNotSupported.message(),
744            "Chain not supported"
745        );
746        assert_eq!(RpcErrorCode::KeyNotFound.message(), "Key not found");
747        assert_eq!(RpcErrorCode::SignatureFailed.message(), "Signature failed");
748    }
749
750    #[test]
751    fn test_rpc_error_code_display() {
752        assert_eq!(
753            RpcErrorCode::ParseFailed.to_string(),
754            "Parse error (-32700)"
755        );
756        assert_eq!(
757            RpcErrorCode::KeyNotFound.to_string(),
758            "Key not found (-32003)"
759        );
760    }
761
762    #[test]
763    fn test_rpc_error_code_from_txgate_error() {
764        // Parse errors
765        let err = TxGateError::Parse(ParseError::UnknownTxType);
766        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::ParseFailed);
767
768        let err = TxGateError::Parse(ParseError::unsupported_chain("unknown"));
769        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::ChainNotSupported);
770
771        // Sign errors
772        let err = TxGateError::Sign(SignError::key_not_found("test"));
773        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::KeyNotFound);
774
775        let err = TxGateError::Sign(SignError::InvalidKey);
776        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::SignatureFailed);
777
778        // Policy errors
779        let err = TxGateError::policy_denied("whitelist", "not allowed");
780        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::PolicyDenied);
781
782        let err = TxGateError::Policy(PolicyError::blacklisted("0x1234"));
783        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::PolicyDenied);
784
785        // Store errors
786        let err = TxGateError::Store(StoreError::key_not_found("test"));
787        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::KeyNotFound);
788
789        let err = TxGateError::Store(StoreError::DecryptionFailed);
790        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::InternalError);
791
792        // Config errors
793        let err = TxGateError::Config(ConfigError::missing_field("test"));
794        assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::InternalError);
795    }
796
797    // ------------------------------------------------------------------------
798    // ParseError tests
799    // ------------------------------------------------------------------------
800
801    #[test]
802    fn test_parse_error_display() {
803        assert_eq!(
804            ParseError::UnknownTxType.to_string(),
805            "unknown transaction type"
806        );
807
808        assert_eq!(
809            ParseError::invalid_rlp("failed to decode nonce").to_string(),
810            "RLP decoding failed: failed to decode nonce"
811        );
812
813        assert_eq!(
814            ParseError::malformed_transaction("missing to field").to_string(),
815            "malformed transaction: missing to field"
816        );
817
818        assert_eq!(
819            ParseError::MalformedCalldata.to_string(),
820            "malformed calldata"
821        );
822
823        assert_eq!(
824            ParseError::unsupported_chain("cosmos").to_string(),
825            "unsupported chain: cosmos"
826        );
827
828        assert_eq!(
829            ParseError::invalid_address("0xinvalid").to_string(),
830            "invalid address: 0xinvalid"
831        );
832    }
833
834    #[test]
835    fn test_parse_error_constructors() {
836        let err = ParseError::invalid_rlp("test context");
837        assert!(matches!(err, ParseError::InvalidRlp { context } if context == "test context"));
838
839        let err = ParseError::malformed_transaction("test context");
840        assert!(
841            matches!(err, ParseError::MalformedTransaction { context } if context == "test context")
842        );
843
844        let err = ParseError::unsupported_chain("test-chain");
845        assert!(matches!(err, ParseError::UnsupportedChain { chain } if chain == "test-chain"));
846
847        let err = ParseError::invalid_address("bad-addr");
848        assert!(matches!(err, ParseError::InvalidAddress { address } if address == "bad-addr"));
849    }
850
851    // ------------------------------------------------------------------------
852    // SignError tests
853    // ------------------------------------------------------------------------
854
855    #[test]
856    fn test_sign_error_display() {
857        assert_eq!(
858            SignError::key_not_found("my-key").to_string(),
859            "key not found: my-key"
860        );
861
862        assert_eq!(SignError::InvalidKey.to_string(), "invalid key material");
863
864        assert_eq!(
865            SignError::signature_failed("hash mismatch").to_string(),
866            "signature failed: hash mismatch"
867        );
868
869        assert_eq!(
870            SignError::wrong_curve("secp256k1", "ed25519").to_string(),
871            "wrong curve: expected secp256k1, got ed25519"
872        );
873    }
874
875    #[test]
876    fn test_sign_error_constructors() {
877        let err = SignError::key_not_found("test-key");
878        assert!(matches!(err, SignError::KeyNotFound { name } if name == "test-key"));
879
880        let err = SignError::signature_failed("test reason");
881        assert!(matches!(err, SignError::SignatureFailed { context } if context == "test reason"));
882
883        let err = SignError::wrong_curve("secp256k1", "ed25519");
884        assert!(
885            matches!(err, SignError::WrongCurve { expected, actual } if expected == "secp256k1" && actual == "ed25519")
886        );
887    }
888
889    // ------------------------------------------------------------------------
890    // StoreError tests
891    // ------------------------------------------------------------------------
892
893    #[test]
894    fn test_store_error_display() {
895        assert_eq!(
896            StoreError::EncryptionFailed.to_string(),
897            "encryption failed"
898        );
899
900        assert_eq!(
901            StoreError::DecryptionFailed.to_string(),
902            "decryption failed (wrong password?)"
903        );
904
905        assert_eq!(
906            StoreError::key_exists("existing-key").to_string(),
907            "key already exists: existing-key"
908        );
909
910        assert_eq!(
911            StoreError::key_not_found("missing-key").to_string(),
912            "key not found: missing-key"
913        );
914
915        assert_eq!(
916            StoreError::InvalidFormat.to_string(),
917            "invalid key file format"
918        );
919
920        assert_eq!(
921            StoreError::PermissionDenied.to_string(),
922            "permission denied"
923        );
924    }
925
926    #[test]
927    fn test_store_error_from_io_error() {
928        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
929        let store_err: StoreError = io_err.into();
930        assert!(matches!(store_err, StoreError::PermissionDenied));
931
932        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
933        let store_err: StoreError = io_err.into();
934        assert!(matches!(store_err, StoreError::InvalidFormat));
935
936        let io_err = std::io::Error::other("something else");
937        let store_err: StoreError = io_err.into();
938        assert!(matches!(store_err, StoreError::IoError(_)));
939    }
940
941    #[test]
942    fn test_store_error_constructors() {
943        let err = StoreError::key_exists("test-key");
944        assert!(matches!(err, StoreError::KeyExists { name } if name == "test-key"));
945
946        let err = StoreError::key_not_found("test-key");
947        assert!(matches!(err, StoreError::KeyNotFound { name } if name == "test-key"));
948    }
949
950    // ------------------------------------------------------------------------
951    // PolicyError tests
952    // ------------------------------------------------------------------------
953
954    #[test]
955    fn test_policy_error_display() {
956        assert_eq!(
957            PolicyError::blacklisted("0x1234").to_string(),
958            "address is blacklisted: 0x1234"
959        );
960
961        assert_eq!(
962            PolicyError::not_whitelisted("0x5678").to_string(),
963            "address not in whitelist: 0x5678"
964        );
965
966        assert_eq!(
967            PolicyError::exceeds_transaction_limit("5 ETH", "10 ETH").to_string(),
968            "exceeds transaction limit: limit=5 ETH, amount=10 ETH"
969        );
970
971        assert_eq!(
972            PolicyError::exceeds_daily_limit("10 ETH", "8 ETH", "5 ETH").to_string(),
973            "exceeds daily limit: limit=10 ETH, current=8 ETH, amount=5 ETH"
974        );
975
976        assert_eq!(
977            PolicyError::invalid_configuration("missing whitelist").to_string(),
978            "invalid configuration: missing whitelist"
979        );
980    }
981
982    #[test]
983    fn test_policy_error_rule_names() {
984        assert_eq!(PolicyError::blacklisted("x").rule_name(), "blacklist");
985        assert_eq!(PolicyError::not_whitelisted("x").rule_name(), "whitelist");
986        assert_eq!(
987            PolicyError::exceeds_transaction_limit("1", "2").rule_name(),
988            "tx_limit"
989        );
990        assert_eq!(
991            PolicyError::exceeds_daily_limit("1", "2", "3").rule_name(),
992            "daily_limit"
993        );
994        assert_eq!(
995            PolicyError::invalid_configuration("x").rule_name(),
996            "configuration"
997        );
998    }
999
1000    #[test]
1001    fn test_policy_error_denial_reason() {
1002        let err = PolicyError::blacklisted("0x1234");
1003        assert_eq!(err.denial_reason(), "address is blacklisted: 0x1234");
1004    }
1005
1006    // ------------------------------------------------------------------------
1007    // ConfigError tests
1008    // ------------------------------------------------------------------------
1009
1010    #[test]
1011    fn test_config_error_display() {
1012        assert_eq!(
1013            ConfigError::file_not_found("/path/to/config.toml").to_string(),
1014            "configuration file not found: /path/to/config.toml"
1015        );
1016
1017        assert_eq!(
1018            ConfigError::parse_failed("invalid TOML syntax").to_string(),
1019            "failed to parse configuration: invalid TOML syntax"
1020        );
1021
1022        assert_eq!(
1023            ConfigError::invalid_value("port", "-1").to_string(),
1024            "invalid value for port: -1"
1025        );
1026
1027        assert_eq!(
1028            ConfigError::missing_field("api_key").to_string(),
1029            "missing required field: api_key"
1030        );
1031    }
1032
1033    #[test]
1034    fn test_config_error_constructors() {
1035        let err = ConfigError::file_not_found("/test/path");
1036        assert!(matches!(err, ConfigError::FileNotFound { path } if path == "/test/path"));
1037
1038        let err = ConfigError::parse_failed("test context");
1039        assert!(matches!(err, ConfigError::ParseFailed { context } if context == "test context"));
1040
1041        let err = ConfigError::invalid_value("field", "value");
1042        assert!(
1043            matches!(err, ConfigError::InvalidValue { field, value } if field == "field" && value == "value")
1044        );
1045
1046        let err = ConfigError::missing_field("test_field");
1047        assert!(matches!(err, ConfigError::MissingField { field } if field == "test_field"));
1048
1049        let io_err = std::io::Error::other("test io error");
1050        let err = ConfigError::io("reading config", io_err);
1051        assert!(matches!(err, ConfigError::Io { context, .. } if context == "reading config"));
1052
1053        let err = ConfigError::no_home_directory();
1054        assert!(matches!(err, ConfigError::NoHomeDirectory));
1055    }
1056
1057    #[test]
1058    fn test_config_error_io_display() {
1059        let io_err = std::io::Error::other("underlying error");
1060        let err = ConfigError::io("reading config file", io_err);
1061        assert_eq!(err.to_string(), "I/O error: reading config file");
1062    }
1063
1064    #[test]
1065    fn test_config_error_no_home_directory_display() {
1066        let err = ConfigError::no_home_directory();
1067        assert_eq!(err.to_string(), "could not determine home directory");
1068    }
1069
1070    #[test]
1071    fn test_config_error_io_source() {
1072        let io_err = std::io::Error::other("test error");
1073        let config_err = ConfigError::io("test context", io_err);
1074
1075        // Verify the source chain works
1076        use std::error::Error;
1077        assert!(config_err.source().is_some());
1078    }
1079
1080    // ------------------------------------------------------------------------
1081    // Error trait implementation tests
1082    // ------------------------------------------------------------------------
1083
1084    #[test]
1085    fn test_store_error_source() {
1086        let io_err = std::io::Error::other("test error");
1087        let store_err = StoreError::io_error(io_err);
1088
1089        // Verify the source chain works
1090        use std::error::Error;
1091        assert!(store_err.source().is_some());
1092    }
1093
1094    #[test]
1095    fn test_txgate_error_is_send_sync() {
1096        fn assert_send_sync<T: Send + Sync>() {}
1097        assert_send_sync::<TxGateError>();
1098        assert_send_sync::<ParseError>();
1099        assert_send_sync::<SignError>();
1100        assert_send_sync::<StoreError>();
1101        assert_send_sync::<PolicyError>();
1102        assert_send_sync::<ConfigError>();
1103    }
1104}