1use std::fmt;
29
30#[derive(Debug, thiserror::Error)]
35pub enum TxGateError {
36 #[error("Parse error: {0}")]
38 Parse(#[from] ParseError),
39
40 #[error("Policy denied: {rule} - {reason}")]
42 PolicyDenied {
43 rule: String,
45 reason: String,
47 },
48
49 #[error("Signing error: {0}")]
51 Sign(#[from] SignError),
52
53 #[error("Storage error: {0}")]
55 Store(#[from] StoreError),
56
57 #[error("Configuration error: {0}")]
59 Config(#[from] ConfigError),
60
61 #[error("Policy error: {0}")]
63 Policy(#[from] PolicyError),
64}
65
66impl TxGateError {
67 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82#[repr(i32)]
83pub enum RpcErrorCode {
84 ParseFailed = -32700,
86 InvalidRequest = -32600,
88 MethodNotFound = -32601,
90 InvalidParams = -32602,
92 InternalError = -32603,
94
95 PolicyDenied = -32001,
98 ChainNotSupported = -32002,
100 KeyNotFound = -32003,
102 SignatureFailed = -32004,
104}
105
106impl RpcErrorCode {
107 #[must_use]
109 pub const fn code(self) -> i32 {
110 self as i32
111 }
112
113 #[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#[derive(Debug, thiserror::Error)]
165pub enum ParseError {
166 #[error("unknown transaction type")]
168 UnknownTxType,
169
170 #[error("RLP decoding failed: {context}")]
172 InvalidRlp {
173 context: String,
175 },
176
177 #[error("malformed transaction: {context}")]
179 MalformedTransaction {
180 context: String,
182 },
183
184 #[error("malformed calldata")]
186 MalformedCalldata,
187
188 #[error("unsupported chain: {chain}")]
190 UnsupportedChain {
191 chain: String,
193 },
194
195 #[error("invalid address: {address}")]
197 InvalidAddress {
198 address: String,
200 },
201}
202
203impl ParseError {
204 #[must_use]
206 pub fn invalid_rlp(context: impl Into<String>) -> Self {
207 Self::InvalidRlp {
208 context: context.into(),
209 }
210 }
211
212 #[must_use]
214 pub fn malformed_transaction(context: impl Into<String>) -> Self {
215 Self::MalformedTransaction {
216 context: context.into(),
217 }
218 }
219
220 #[must_use]
222 pub fn unsupported_chain(chain: impl Into<String>) -> Self {
223 Self::UnsupportedChain {
224 chain: chain.into(),
225 }
226 }
227
228 #[must_use]
230 pub fn invalid_address(address: impl Into<String>) -> Self {
231 Self::InvalidAddress {
232 address: address.into(),
233 }
234 }
235}
236
237#[derive(Debug, thiserror::Error)]
246pub enum SignError {
247 #[error("key not found: {name}")]
249 KeyNotFound {
250 name: String,
252 },
253
254 #[error("invalid key material")]
256 InvalidKey,
257
258 #[error("signature failed: {context}")]
260 SignatureFailed {
261 context: String,
263 },
264
265 #[error("wrong curve: expected {expected}, got {actual}")]
267 WrongCurve {
268 expected: String,
270 actual: String,
272 },
273}
274
275impl SignError {
276 #[must_use]
278 pub fn key_not_found(name: impl Into<String>) -> Self {
279 Self::KeyNotFound { name: name.into() }
280 }
281
282 #[must_use]
284 pub fn signature_failed(context: impl Into<String>) -> Self {
285 Self::SignatureFailed {
286 context: context.into(),
287 }
288 }
289
290 #[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#[derive(Debug, thiserror::Error)]
309pub enum StoreError {
310 #[error("I/O error: {0}")]
312 IoError(#[source] std::io::Error),
313
314 #[error("encryption failed")]
316 EncryptionFailed,
317
318 #[error("decryption failed (wrong password?)")]
320 DecryptionFailed,
321
322 #[error("key already exists: {name}")]
324 KeyExists {
325 name: String,
327 },
328
329 #[error("key not found: {name}")]
331 KeyNotFound {
332 name: String,
334 },
335
336 #[error("invalid key file format")]
338 InvalidFormat,
339
340 #[error("permission denied")]
342 PermissionDenied,
343}
344
345impl StoreError {
346 #[must_use]
348 pub const fn io_error(error: std::io::Error) -> Self {
349 Self::IoError(error)
350 }
351
352 #[must_use]
354 pub fn key_exists(name: impl Into<String>) -> Self {
355 Self::KeyExists { name: name.into() }
356 }
357
358 #[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 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#[derive(Debug, thiserror::Error)]
385pub enum PolicyError {
386 #[error("address is blacklisted: {address}")]
388 Blacklisted {
389 address: String,
391 },
392
393 #[error("address not in whitelist: {address}")]
395 NotWhitelisted {
396 address: String,
398 },
399
400 #[error("exceeds transaction limit: limit={limit}, amount={amount}")]
402 ExceedsTransactionLimit {
403 limit: String,
405 amount: String,
407 },
408
409 #[error("exceeds daily limit: limit={limit}, current={current}, amount={amount}")]
411 ExceedsDailyLimit {
412 limit: String,
414 current: String,
416 amount: String,
418 },
419
420 #[error("invalid configuration: {context}")]
422 InvalidConfiguration {
423 context: String,
425 },
426}
427
428impl PolicyError {
429 #[must_use]
431 pub fn blacklisted(address: impl Into<String>) -> Self {
432 Self::Blacklisted {
433 address: address.into(),
434 }
435 }
436
437 #[must_use]
439 pub fn not_whitelisted(address: impl Into<String>) -> Self {
440 Self::NotWhitelisted {
441 address: address.into(),
442 }
443 }
444
445 #[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 #[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 #[must_use]
470 pub fn invalid_configuration(context: impl Into<String>) -> Self {
471 Self::InvalidConfiguration {
472 context: context.into(),
473 }
474 }
475
476 #[must_use]
478 pub fn denial_reason(&self) -> String {
479 self.to_string()
480 }
481
482 #[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#[derive(Debug, thiserror::Error)]
501pub enum ConfigError {
502 #[error("configuration file not found: {path}")]
504 FileNotFound {
505 path: String,
507 },
508
509 #[error("failed to parse configuration: {context}")]
511 ParseFailed {
512 context: String,
514 },
515
516 #[error("invalid value for {field}: {value}")]
518 InvalidValue {
519 field: String,
521 value: String,
523 },
524
525 #[error("missing required field: {field}")]
527 MissingField {
528 field: String,
530 },
531
532 #[error("I/O error: {context}")]
534 Io {
535 context: String,
537 #[source]
539 source: std::io::Error,
540 },
541
542 #[error("could not determine home directory")]
544 NoHomeDirectory,
545}
546
547impl ConfigError {
548 #[must_use]
550 pub fn file_not_found(path: impl Into<String>) -> Self {
551 Self::FileNotFound { path: path.into() }
552 }
553
554 #[must_use]
556 pub fn parse_failed(context: impl Into<String>) -> Self {
557 Self::ParseFailed {
558 context: context.into(),
559 }
560 }
561
562 #[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 #[must_use]
573 pub fn missing_field(field: impl Into<String>) -> Self {
574 Self::MissingField {
575 field: field.into(),
576 }
577 }
578
579 #[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 #[must_use]
590 pub const fn no_home_directory() -> Self {
591 Self::NoHomeDirectory
592 }
593}
594
595pub type Result<T> = std::result::Result<T, TxGateError>;
601
602pub type ParseResult<T> = std::result::Result<T, ParseError>;
604
605pub type SignResult<T> = std::result::Result<T, SignError>;
607
608pub type StoreResult<T> = std::result::Result<T, StoreError>;
610
611pub type PolicyResult<T> = std::result::Result<T, PolicyError>;
613
614pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
616
617#[cfg(test)]
622mod tests {
623 #![allow(clippy::items_after_statements)]
624
625 use super::*;
626
627 #[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 #[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 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 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 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 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 let err = TxGateError::Config(ConfigError::missing_field("test"));
794 assert_eq!(RpcErrorCode::from(&err), RpcErrorCode::InternalError);
795 }
796
797 #[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 #[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 #[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 #[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 #[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 use std::error::Error;
1077 assert!(config_err.source().is_some());
1078 }
1079
1080 #[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 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}