1use crate::error::{ParseError, PolicyError, SignError};
39use crate::types::{ParsedTx, PolicyResult};
40
41pub type SignatureBytes = [u8; 64];
51
52#[derive(Debug, thiserror::Error)]
64pub enum SigningError {
65 #[error("failed to parse transaction: {0}")]
69 ParseError(#[from] ParseError),
70
71 #[error("policy check failed: {0}")]
76 PolicyError(#[from] PolicyError),
77
78 #[error("signing failed: {0}")]
82 SignError(#[from] SignError),
83
84 #[error("transaction denied by policy: {reason}")]
89 PolicyDenied {
90 reason: String,
92 },
93}
94
95impl SigningError {
96 #[must_use]
98 pub fn policy_denied(reason: impl Into<String>) -> Self {
99 Self::PolicyDenied {
100 reason: reason.into(),
101 }
102 }
103
104 #[must_use]
106 pub const fn is_policy_denied(&self) -> bool {
107 matches!(self, Self::PolicyDenied { .. })
108 }
109
110 #[must_use]
112 pub fn denial_reason(&self) -> Option<&str> {
113 match self {
114 Self::PolicyDenied { reason } => Some(reason),
115 _ => None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum PolicyCheckResult {
130 Allowed,
132 Denied {
134 rule: String,
136 reason: String,
138 },
139}
140
141impl PolicyCheckResult {
142 #[must_use]
144 pub const fn is_allowed(&self) -> bool {
145 matches!(self, Self::Allowed)
146 }
147
148 #[must_use]
150 pub const fn is_denied(&self) -> bool {
151 matches!(self, Self::Denied { .. })
152 }
153
154 #[must_use]
156 pub fn denied(rule: impl Into<String>, reason: impl Into<String>) -> Self {
157 Self::Denied {
158 rule: rule.into(),
159 reason: reason.into(),
160 }
161 }
162}
163
164impl From<PolicyResult> for PolicyCheckResult {
165 fn from(result: PolicyResult) -> Self {
166 match result {
167 PolicyResult::Allowed => Self::Allowed,
168 PolicyResult::Denied { rule, reason } => Self::Denied { rule, reason },
169 }
170 }
171}
172
173#[derive(Debug, Clone)]
195pub struct SigningResult {
196 pub parsed_tx: ParsedTx,
198
199 pub policy_result: PolicyCheckResult,
201
202 pub signature: Option<SignatureBytes>,
208
209 pub recovery_id: Option<u8>,
214}
215
216impl SigningResult {
217 #[must_use]
219 pub const fn allowed(parsed_tx: ParsedTx, signature: SignatureBytes, recovery_id: u8) -> Self {
220 Self {
221 parsed_tx,
222 policy_result: PolicyCheckResult::Allowed,
223 signature: Some(signature),
224 recovery_id: Some(recovery_id),
225 }
226 }
227
228 #[must_use]
230 pub const fn checked(parsed_tx: ParsedTx, policy_result: PolicyCheckResult) -> Self {
231 Self {
232 parsed_tx,
233 policy_result,
234 signature: None,
235 recovery_id: None,
236 }
237 }
238
239 #[must_use]
241 pub const fn is_allowed(&self) -> bool {
242 self.policy_result.is_allowed()
243 }
244
245 #[must_use]
247 pub const fn has_signature(&self) -> bool {
248 self.signature.is_some()
249 }
250
251 #[must_use]
255 pub fn signature_with_recovery_id(&self) -> Option<[u8; 65]> {
256 match (self.signature, self.recovery_id) {
257 (Some(sig), Some(v)) => {
258 let mut result = [0u8; 65];
259 result[..64].copy_from_slice(&sig);
260 result[64] = v;
261 Some(result)
262 }
263 _ => None,
264 }
265 }
266}
267
268pub struct SigningService<C, P, S> {
310 chain: C,
312 policy: P,
314 signer: S,
316}
317
318impl<C, P, S> std::fmt::Debug for SigningService<C, P, S> {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 f.debug_struct("SigningService")
321 .field("chain", &"<Chain>")
322 .field("policy", &"<PolicyEngine>")
323 .field("signer", &"<Signer>")
324 .finish()
325 }
326}
327
328impl<C, P, S> SigningService<C, P, S> {
329 #[must_use]
343 pub const fn new(chain: C, policy: P, signer: S) -> Self {
344 Self {
345 chain,
346 policy,
347 signer,
348 }
349 }
350
351 #[must_use]
353 pub const fn chain(&self) -> &C {
354 &self.chain
355 }
356
357 #[must_use]
359 pub const fn policy(&self) -> &P {
360 &self.policy
361 }
362
363 #[must_use]
365 pub const fn signer(&self) -> &S {
366 &self.signer
367 }
368}
369
370pub trait ChainParser: Send + Sync {
375 fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError>;
381}
382
383pub trait PolicyEngineExt: Send + Sync {
388 fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError>;
394}
395
396pub trait SignerExt: Send + Sync {
401 fn sign(&self, hash: &[u8; 32]) -> Result<Vec<u8>, SignError>;
410}
411
412impl<C, P, S> SigningService<C, P, S>
413where
414 C: ChainParser,
415 P: PolicyEngineExt,
416 S: SignerExt,
417{
418 pub fn sign(&self, raw_tx: &[u8]) -> Result<SigningResult, SigningError> {
456 let parsed_tx = self.chain.parse(raw_tx)?;
458
459 let policy_result = self.policy.check(&parsed_tx)?;
461
462 if let PolicyResult::Denied { reason, .. } = &policy_result {
464 return Err(SigningError::PolicyDenied {
465 reason: reason.clone(),
466 });
467 }
468
469 let sig_bytes = self.signer.sign(&parsed_tx.hash)?;
471
472 let (signature, recovery_id) = extract_signature_components(&sig_bytes)?;
474
475 Ok(SigningResult {
477 parsed_tx,
478 policy_result: PolicyCheckResult::Allowed,
479 signature: Some(signature),
480 recovery_id: Some(recovery_id),
481 })
482 }
483
484 pub fn check(&self, raw_tx: &[u8]) -> Result<SigningResult, SigningError> {
522 let parsed_tx = self.chain.parse(raw_tx)?;
524
525 let policy_result = self.policy.check(&parsed_tx)?;
527
528 Ok(SigningResult {
530 parsed_tx,
531 policy_result: policy_result.into(),
532 signature: None,
533 recovery_id: None,
534 })
535 }
536}
537
538fn extract_signature_components(sig: &[u8]) -> Result<(SignatureBytes, u8), SigningError> {
546 let sig_array: [u8; 65] = sig.try_into().map_err(|_| {
548 SigningError::SignError(SignError::signature_failed(format!(
549 "expected 65-byte signature, got {} bytes",
550 sig.len()
551 )))
552 })?;
553
554 let mut signature = [0u8; 64];
555 signature.copy_from_slice(&sig_array[..64]);
556 let recovery_id = sig_array[64];
557
558 Ok((signature, recovery_id))
559}
560
561#[cfg(test)]
566mod tests {
567 #![allow(
568 clippy::expect_used,
569 clippy::unwrap_used,
570 clippy::panic,
571 clippy::indexing_slicing,
572 clippy::large_enum_variant,
573 clippy::redundant_clone,
574 dead_code
575 )]
576
577 use super::*;
578 use crate::types::TxType;
579 use std::collections::HashMap;
580
581 #[derive(Clone)]
587 enum MockChainBehavior {
588 Success(ParsedTx),
589 Failure(MockParseErrorKind),
590 }
591
592 #[derive(Clone, Copy)]
594 enum MockParseErrorKind {
595 UnknownTxType,
596 MalformedTransaction,
597 }
598
599 impl MockParseErrorKind {
600 fn to_error(self) -> ParseError {
601 match self {
602 Self::UnknownTxType => ParseError::UnknownTxType,
603 Self::MalformedTransaction => ParseError::malformed_transaction("mock error"),
604 }
605 }
606 }
607
608 struct MockChain {
610 behavior: MockChainBehavior,
611 }
612
613 impl MockChain {
614 fn success(tx: ParsedTx) -> Self {
615 Self {
616 behavior: MockChainBehavior::Success(tx),
617 }
618 }
619
620 fn failure(kind: MockParseErrorKind) -> Self {
621 Self {
622 behavior: MockChainBehavior::Failure(kind),
623 }
624 }
625 }
626
627 impl ChainParser for MockChain {
628 fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
629 match &self.behavior {
630 MockChainBehavior::Success(tx) => Ok(tx.clone()),
631 MockChainBehavior::Failure(kind) => Err(kind.to_error()),
632 }
633 }
634 }
635
636 #[derive(Clone)]
638 enum MockPolicyBehavior {
639 Allowed,
640 Denied { rule: String, reason: String },
641 Error(MockPolicyErrorKind),
642 }
643
644 #[derive(Clone, Copy)]
646 enum MockPolicyErrorKind {
647 InvalidConfiguration,
648 }
649
650 impl MockPolicyErrorKind {
651 fn to_error(self) -> PolicyError {
652 match self {
653 Self::InvalidConfiguration => PolicyError::invalid_configuration("mock error"),
654 }
655 }
656 }
657
658 struct MockPolicy {
660 check_behavior: MockPolicyBehavior,
661 }
662
663 impl MockPolicy {
664 fn allowed() -> Self {
665 Self {
666 check_behavior: MockPolicyBehavior::Allowed,
667 }
668 }
669
670 fn denied(rule: &str, reason: &str) -> Self {
671 Self {
672 check_behavior: MockPolicyBehavior::Denied {
673 rule: rule.to_string(),
674 reason: reason.to_string(),
675 },
676 }
677 }
678
679 #[allow(dead_code)]
680 fn check_error(kind: MockPolicyErrorKind) -> Self {
681 Self {
682 check_behavior: MockPolicyBehavior::Error(kind),
683 }
684 }
685 }
686
687 impl PolicyEngineExt for MockPolicy {
688 fn check(&self, _tx: &ParsedTx) -> Result<PolicyResult, PolicyError> {
689 match &self.check_behavior {
690 MockPolicyBehavior::Allowed => Ok(PolicyResult::Allowed),
691 MockPolicyBehavior::Denied { rule, reason } => Ok(PolicyResult::Denied {
692 rule: rule.clone(),
693 reason: reason.clone(),
694 }),
695 MockPolicyBehavior::Error(kind) => Err(kind.to_error()),
696 }
697 }
698 }
699
700 enum MockSignerBehavior {
702 Success { recovery_id: u8 },
703 Failure(MockSignErrorKind),
704 }
705
706 #[derive(Clone, Copy)]
708 enum MockSignErrorKind {
709 InvalidKey,
710 }
711
712 impl MockSignErrorKind {
713 fn to_error(self) -> SignError {
714 match self {
715 Self::InvalidKey => SignError::InvalidKey,
716 }
717 }
718 }
719
720 struct MockSigner {
722 behavior: MockSignerBehavior,
723 }
724
725 impl MockSigner {
726 fn success() -> Self {
727 Self {
728 behavior: MockSignerBehavior::Success { recovery_id: 0 },
729 }
730 }
731
732 fn success_with_recovery_id(recovery_id: u8) -> Self {
733 Self {
734 behavior: MockSignerBehavior::Success { recovery_id },
735 }
736 }
737
738 fn failure(kind: MockSignErrorKind) -> Self {
739 Self {
740 behavior: MockSignerBehavior::Failure(kind),
741 }
742 }
743 }
744
745 impl SignerExt for MockSigner {
746 fn sign(&self, _hash: &[u8; 32]) -> Result<Vec<u8>, SignError> {
747 match &self.behavior {
748 MockSignerBehavior::Success { recovery_id } => {
749 let mut sig = vec![0u8; 65];
750 sig[..32].copy_from_slice(&[0xab; 32]); sig[32..64].copy_from_slice(&[0xcd; 32]); sig[64] = *recovery_id; Ok(sig)
754 }
755 MockSignerBehavior::Failure(kind) => Err(kind.to_error()),
756 }
757 }
758 }
759
760 fn test_tx() -> ParsedTx {
762 ParsedTx {
763 hash: [0x42; 32],
764 recipient: Some("0x1234".to_string()),
765 amount: Some(crate::U256::from(100)),
766 token: Some("ETH".to_string()),
767 token_address: None,
768 tx_type: TxType::Transfer,
769 chain: "ethereum".to_string(),
770 nonce: Some(1),
771 chain_id: Some(1),
772 metadata: HashMap::new(),
773 }
774 }
775
776 mod signing_error_tests {
781 use super::*;
782
783 #[test]
784 fn test_from_parse_error() {
785 let err = ParseError::UnknownTxType;
786 let signing_err: SigningError = err.into();
787
788 assert!(matches!(signing_err, SigningError::ParseError(_)));
789 assert!(!signing_err.is_policy_denied());
790 assert!(signing_err.denial_reason().is_none());
791 }
792
793 #[test]
794 fn test_from_policy_error() {
795 let err = PolicyError::invalid_configuration("test");
796 let signing_err: SigningError = err.into();
797
798 assert!(matches!(signing_err, SigningError::PolicyError(_)));
799 assert!(!signing_err.is_policy_denied());
800 }
801
802 #[test]
803 fn test_from_sign_error() {
804 let err = SignError::InvalidKey;
805 let signing_err: SigningError = err.into();
806
807 assert!(matches!(signing_err, SigningError::SignError(_)));
808 assert!(!signing_err.is_policy_denied());
809 }
810
811 #[test]
812 fn test_policy_denied() {
813 let err = SigningError::policy_denied("blacklisted address");
814
815 assert!(err.is_policy_denied());
816 assert_eq!(err.denial_reason(), Some("blacklisted address"));
817 assert!(err.to_string().contains("denied by policy"));
818 }
819
820 #[test]
821 fn test_error_display() {
822 let parse_err: SigningError = ParseError::UnknownTxType.into();
823 assert!(parse_err.to_string().contains("parse transaction"));
824
825 let policy_err: SigningError = PolicyError::invalid_configuration("test").into();
826 assert!(policy_err.to_string().contains("policy check failed"));
827
828 let sign_err: SigningError = SignError::InvalidKey.into();
829 assert!(sign_err.to_string().contains("signing failed"));
830
831 let denied_err = SigningError::policy_denied("test reason");
832 assert!(denied_err.to_string().contains("denied by policy"));
833 assert!(denied_err.to_string().contains("test reason"));
834 }
835 }
836
837 mod policy_check_result_tests {
842 use super::*;
843
844 #[test]
845 fn test_allowed() {
846 let result = PolicyCheckResult::Allowed;
847 assert!(result.is_allowed());
848 assert!(!result.is_denied());
849 }
850
851 #[test]
852 fn test_denied() {
853 let result = PolicyCheckResult::denied("blacklist", "address blocked");
854 assert!(!result.is_allowed());
855 assert!(result.is_denied());
856 }
857
858 #[test]
859 fn test_from_policy_result_allowed() {
860 let policy_result = PolicyResult::Allowed;
861 let check_result: PolicyCheckResult = policy_result.into();
862 assert!(check_result.is_allowed());
863 }
864
865 #[test]
866 fn test_from_policy_result_denied() {
867 let policy_result = PolicyResult::Denied {
868 rule: "whitelist".to_string(),
869 reason: "not in list".to_string(),
870 };
871 let check_result: PolicyCheckResult = policy_result.into();
872 assert!(check_result.is_denied());
873
874 if let PolicyCheckResult::Denied { rule, reason } = check_result {
875 assert_eq!(rule, "whitelist");
876 assert_eq!(reason, "not in list");
877 } else {
878 panic!("expected Denied variant");
879 }
880 }
881 }
882
883 mod signing_result_tests {
888 use super::*;
889
890 #[test]
891 fn test_allowed_constructor() {
892 let tx = test_tx();
893 let sig = [0xab; 64];
894 let result = SigningResult::allowed(tx.clone(), sig, 0);
895
896 assert!(result.is_allowed());
897 assert!(result.has_signature());
898 assert_eq!(result.signature, Some(sig));
899 assert_eq!(result.recovery_id, Some(0));
900 assert_eq!(result.parsed_tx.hash, tx.hash);
901 }
902
903 #[test]
904 fn test_checked_constructor() {
905 let tx = test_tx();
906 let result = SigningResult::checked(tx.clone(), PolicyCheckResult::Allowed);
907
908 assert!(result.is_allowed());
909 assert!(!result.has_signature());
910 assert!(result.signature.is_none());
911 assert!(result.recovery_id.is_none());
912 }
913
914 #[test]
915 fn test_signature_with_recovery_id() {
916 let tx = test_tx();
917 let sig = [0xab; 64];
918 let result = SigningResult::allowed(tx, sig, 1);
919
920 let full_sig = result.signature_with_recovery_id().unwrap();
921 assert_eq!(full_sig.len(), 65);
922 assert_eq!(&full_sig[..64], &sig);
923 assert_eq!(full_sig[64], 1);
924 }
925
926 #[test]
927 fn test_signature_with_recovery_id_none() {
928 let tx = test_tx();
929 let result = SigningResult::checked(tx, PolicyCheckResult::Allowed);
930
931 assert!(result.signature_with_recovery_id().is_none());
932 }
933 }
934
935 mod signing_service_tests {
940 use super::*;
941
942 #[test]
943 fn test_successful_signing_flow() {
944 let tx = test_tx();
945 let chain = MockChain::success(tx.clone());
946 let policy = MockPolicy::allowed();
947 let signer = MockSigner::success();
948
949 let service = SigningService::new(chain, policy, signer);
950 let result = service.sign(&[0x01, 0x02, 0x03]).unwrap();
951
952 assert!(result.is_allowed());
953 assert!(result.has_signature());
954 assert!(result.signature.is_some());
955 assert_eq!(result.recovery_id, Some(0));
956 assert_eq!(result.parsed_tx.hash, tx.hash);
957 }
958
959 #[test]
960 fn test_policy_denial_flow() {
961 let tx = test_tx();
962 let chain = MockChain::success(tx);
963 let policy = MockPolicy::denied("blacklist", "address is blacklisted");
964 let signer = MockSigner::success();
965
966 let service = SigningService::new(chain, policy, signer);
967 let result = service.sign(&[0x01]);
968
969 assert!(result.is_err());
970 let err = result.unwrap_err();
971 assert!(err.is_policy_denied());
972 assert_eq!(err.denial_reason(), Some("address is blacklisted"));
973 }
974
975 #[test]
976 fn test_parse_error_handling() {
977 let chain = MockChain::failure(MockParseErrorKind::UnknownTxType);
978 let policy = MockPolicy::allowed();
979 let signer = MockSigner::success();
980
981 let service = SigningService::new(chain, policy, signer);
982 let result = service.sign(&[0x01]);
983
984 assert!(result.is_err());
985 assert!(matches!(result.unwrap_err(), SigningError::ParseError(_)));
986 }
987
988 #[test]
989 fn test_sign_error_handling() {
990 let tx = test_tx();
991 let chain = MockChain::success(tx);
992 let policy = MockPolicy::allowed();
993 let signer = MockSigner::failure(MockSignErrorKind::InvalidKey);
994
995 let service = SigningService::new(chain, policy, signer);
996 let result = service.sign(&[0x01]);
997
998 assert!(result.is_err());
999 assert!(matches!(result.unwrap_err(), SigningError::SignError(_)));
1000 }
1001
1002 #[test]
1003 fn test_dry_run_check() {
1004 let tx = test_tx();
1005 let chain = MockChain::success(tx.clone());
1006 let policy = MockPolicy::allowed();
1007 let signer = MockSigner::success();
1008
1009 let service = SigningService::new(chain, policy, signer);
1010 let result = service.check(&[0x01]).unwrap();
1011
1012 assert!(result.is_allowed());
1013 assert!(!result.has_signature());
1014 assert!(result.signature.is_none());
1015 assert_eq!(result.parsed_tx.hash, tx.hash);
1016 }
1017
1018 #[test]
1019 fn test_dry_run_check_denied() {
1020 let tx = test_tx();
1021 let chain = MockChain::success(tx);
1022 let policy = MockPolicy::denied("tx_limit", "exceeds limit");
1023 let signer = MockSigner::success();
1024
1025 let service = SigningService::new(chain, policy, signer);
1026 let result = service.check(&[0x01]).unwrap();
1027
1028 assert!(!result.is_allowed());
1030 assert!(!result.has_signature());
1031 }
1032
1033 #[test]
1034 fn test_recovery_id_passed_through() {
1035 let tx = test_tx();
1036 let chain = MockChain::success(tx);
1037 let policy = MockPolicy::allowed();
1038 let signer = MockSigner::success_with_recovery_id(1);
1039
1040 let service = SigningService::new(chain, policy, signer);
1041 let result = service.sign(&[0x01]).unwrap();
1042
1043 assert_eq!(result.recovery_id, Some(1));
1044 }
1045
1046 #[test]
1047 fn test_accessors() {
1048 let tx = test_tx();
1049 let chain = MockChain::success(tx);
1050 let policy = MockPolicy::allowed();
1051 let signer = MockSigner::success();
1052
1053 let service = SigningService::new(chain, policy, signer);
1054
1055 let _ = service.chain();
1057 let _ = service.policy();
1058 let _ = service.signer();
1059 }
1060
1061 #[test]
1062 fn test_debug_impl() {
1063 let tx = test_tx();
1064 let chain = MockChain::success(tx);
1065 let policy = MockPolicy::allowed();
1066 let signer = MockSigner::success();
1067
1068 let service = SigningService::new(chain, policy, signer);
1069 let debug_str = format!("{service:?}");
1070
1071 assert!(debug_str.contains("SigningService"));
1072 }
1073 }
1074
1075 mod extract_signature_tests {
1080 use super::*;
1081
1082 #[test]
1083 fn test_valid_65_byte_signature() {
1084 let mut sig = vec![0u8; 65];
1085 sig[..32].copy_from_slice(&[0xaa; 32]);
1086 sig[32..64].copy_from_slice(&[0xbb; 32]);
1087 sig[64] = 1;
1088
1089 let (signature, recovery_id) = extract_signature_components(&sig).unwrap();
1090
1091 assert_eq!(&signature[..32], &[0xaa; 32]);
1092 assert_eq!(&signature[32..64], &[0xbb; 32]);
1093 assert_eq!(recovery_id, 1);
1094 }
1095
1096 #[test]
1097 fn test_invalid_length_too_short() {
1098 let sig = vec![0u8; 64];
1099 let result = extract_signature_components(&sig);
1100
1101 assert!(result.is_err());
1102 assert!(matches!(result.unwrap_err(), SigningError::SignError(_)));
1103 }
1104
1105 #[test]
1106 fn test_invalid_length_too_long() {
1107 let sig = vec![0u8; 66];
1108 let result = extract_signature_components(&sig);
1109
1110 assert!(result.is_err());
1111 }
1112 }
1113
1114 mod send_sync_tests {
1119 use super::*;
1120
1121 #[test]
1122 fn test_signing_error_is_send_sync() {
1123 fn assert_send_sync<T: Send + Sync>() {}
1124 assert_send_sync::<SigningError>();
1125 }
1126
1127 #[test]
1128 fn test_signing_result_is_send_sync() {
1129 fn assert_send_sync<T: Send + Sync>() {}
1130 assert_send_sync::<SigningResult>();
1131 }
1132
1133 #[test]
1134 fn test_policy_check_result_is_send_sync() {
1135 fn assert_send_sync<T: Send + Sync>() {}
1136 assert_send_sync::<PolicyCheckResult>();
1137 }
1138
1139 }
1143}