Skip to main content

txgate_core/
signing.rs

1//! Signing flow orchestration for the `TxGate` signing service.
2//!
3//! This module provides the [`SigningService`] that orchestrates the complete
4//! signing flow: parsing, policy checking, and signing.
5//!
6//! # Flow Overview
7//!
8//! The signing service performs the following steps:
9//!
10//! 1. **Parse** - Transform raw transaction bytes into a [`ParsedTx`]
11//! 2. **Check** - Evaluate the transaction against policy rules
12//! 3. **Sign** - If allowed, sign the transaction hash
13
14//!
15//! # Example
16//!
17//! ```ignore
18//! use txgate_core::signing::{SigningService, SigningResult};
19//! use txgate_chain::Chain;
20//! use txgate_policy::PolicyEngine;
21//! use txgate_crypto::Signer;
22//!
23//! // Create the service with your implementations
24//! let service = SigningService::new(chain, policy, signer);
25//!
26//! // Sign a transaction
27//! let result = service.sign(&raw_tx_bytes)?;
28//!
29//! // Or just check policy without signing (dry run)
30//! let result = service.check(&raw_tx_bytes)?;
31//! ```
32//!
33//! # Thread Safety
34//!
35//! [`SigningService`] is `Send + Sync` when all its components are,
36//! allowing it to be shared across async tasks.
37
38use crate::error::{ParseError, PolicyError, SignError};
39use crate::types::{ParsedTx, PolicyResult};
40
41// ============================================================================
42// Type Aliases
43// ============================================================================
44
45/// Signature as a 64-byte array (r || s without recovery ID).
46///
47/// This is the raw ECDSA signature without the recovery ID byte.
48/// For Ethereum transactions that need the recovery ID, it is returned
49/// separately in [`SigningResult::recovery_id`].
50pub type SignatureBytes = [u8; 64];
51
52// ============================================================================
53// SigningError
54// ============================================================================
55
56/// Errors that can occur during the signing flow.
57///
58/// This enum covers all failure modes in the signing orchestration:
59/// - Transaction parsing failures
60/// - Policy evaluation failures
61/// - Policy denial (transaction not allowed)
62/// - Signing operation failures
63#[derive(Debug, thiserror::Error)]
64pub enum SigningError {
65    /// Failed to parse the transaction.
66    ///
67    /// The raw transaction bytes could not be decoded into a [`ParsedTx`].
68    #[error("failed to parse transaction: {0}")]
69    ParseError(#[from] ParseError),
70
71    /// Policy evaluation failed.
72    ///
73    /// An error occurred while evaluating the policy rules, distinct from
74    /// a policy denial. This typically indicates a configuration or system error.
75    #[error("policy check failed: {0}")]
76    PolicyError(#[from] PolicyError),
77
78    /// Signing operation failed.
79    ///
80    /// The cryptographic signing operation failed.
81    #[error("signing failed: {0}")]
82    SignError(#[from] SignError),
83
84    /// Transaction denied by policy.
85    ///
86    /// The transaction was rejected by one or more policy rules.
87    /// The reason provides details about which rule denied it.
88    #[error("transaction denied by policy: {reason}")]
89    PolicyDenied {
90        /// Human-readable reason for the denial.
91        reason: String,
92    },
93}
94
95impl SigningError {
96    /// Create a policy denied error with the given reason.
97    #[must_use]
98    pub fn policy_denied(reason: impl Into<String>) -> Self {
99        Self::PolicyDenied {
100            reason: reason.into(),
101        }
102    }
103
104    /// Returns `true` if this error is a policy denial.
105    #[must_use]
106    pub const fn is_policy_denied(&self) -> bool {
107        matches!(self, Self::PolicyDenied { .. })
108    }
109
110    /// Returns the denial reason if this is a policy denial.
111    #[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// ============================================================================
121// PolicyCheckResult
122// ============================================================================
123
124/// Detailed result of a policy check operation.
125///
126/// This enum provides specific information about why a transaction was
127/// allowed or denied, enabling detailed error messages and audit logging.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum PolicyCheckResult {
130    /// Transaction is allowed by all policy rules.
131    Allowed,
132    /// Transaction was denied by a policy rule.
133    Denied {
134        /// The name of the rule that denied the transaction.
135        rule: String,
136        /// Human-readable reason for the denial.
137        reason: String,
138    },
139}
140
141impl PolicyCheckResult {
142    /// Returns `true` if the policy allows the transaction.
143    #[must_use]
144    pub const fn is_allowed(&self) -> bool {
145        matches!(self, Self::Allowed)
146    }
147
148    /// Returns `true` if the policy denies the transaction.
149    #[must_use]
150    pub const fn is_denied(&self) -> bool {
151        matches!(self, Self::Denied { .. })
152    }
153
154    /// Creates a denied result with the given rule and reason.
155    #[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// ============================================================================
174// SigningResult
175// ============================================================================
176
177/// Comprehensive result of a signing operation.
178///
179/// Contains all information about the signing attempt:
180/// - The parsed transaction
181/// - The policy check result
182/// - The signature (if signing was performed and allowed)
183/// - The recovery ID for ECDSA signatures
184///
185/// # Signature Format
186///
187/// For secp256k1 signatures, the signature is 64 bytes (r || s).
188/// The recovery ID is returned separately for flexibility in different
189/// transaction formats:
190///
191/// - Ethereum legacy: `v = recovery_id + 27`
192/// - Ethereum EIP-155: `v = recovery_id + 35 + chain_id * 2`
193/// - Ethereum EIP-2930/EIP-1559: `v = recovery_id`
194#[derive(Debug, Clone)]
195pub struct SigningResult {
196    /// The parsed transaction with all extracted fields.
197    pub parsed_tx: ParsedTx,
198
199    /// The result of the policy check.
200    pub policy_result: PolicyCheckResult,
201
202    /// The signature (if signing was performed and allowed).
203    ///
204    /// This is `None` for:
205    /// - Dry-run checks (using [`SigningService::check`])
206    /// - Transactions denied by policy
207    pub signature: Option<SignatureBytes>,
208
209    /// Recovery ID for ECDSA signatures (0 or 1).
210    ///
211    /// This is needed for Ethereum `ecrecover` operations.
212    /// `None` if no signature was produced.
213    pub recovery_id: Option<u8>,
214}
215
216impl SigningResult {
217    /// Create a new signing result for an allowed transaction.
218    #[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    /// Create a signing result for a dry-run check (no signature).
229    #[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    /// Returns `true` if the transaction was allowed.
240    #[must_use]
241    pub const fn is_allowed(&self) -> bool {
242        self.policy_result.is_allowed()
243    }
244
245    /// Returns `true` if a signature was produced.
246    #[must_use]
247    pub const fn has_signature(&self) -> bool {
248        self.signature.is_some()
249    }
250
251    /// Returns the signature as a 65-byte array with recovery ID appended.
252    ///
253    /// Returns `None` if no signature was produced.
254    #[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
268// ============================================================================
269// SigningService
270// ============================================================================
271
272/// Orchestrates the signing flow: parsing, policy checking, and signing.
273///
274/// The `SigningService` combines a chain parser, policy engine, and signer
275/// into a unified interface for processing transaction signing requests.
276///
277/// # Type Parameters
278///
279/// * `C` - Chain parser implementing the [`ChainParser`] trait
280/// * `P` - Policy engine implementing the [`PolicyEngineExt`] trait
281/// * `S` - Signer implementing the [`SignerExt`] trait
282///
283/// # Thread Safety
284///
285/// This struct is `Send + Sync` when all type parameters are, allowing
286/// it to be safely shared across threads and async tasks.
287///
288/// # Example
289///
290/// ```ignore
291/// use txgate_core::signing::SigningService;
292///
293/// // Create with concrete implementations
294/// let service = SigningService::new(ethereum_parser, policy_engine, secp256k1_signer);
295///
296/// // Sign a transaction (includes policy check)
297/// let result = service.sign(&raw_tx)?;
298/// if result.is_allowed() {
299///     let sig = result.signature.expect("signature present");
300///     println!("Signature: 0x{}", hex::encode(sig));
301/// }
302///
303/// // Or just check without signing
304/// let check_result = service.check(&raw_tx)?;
305/// if check_result.is_allowed() {
306///     println!("Transaction would be allowed");
307/// }
308/// ```
309pub struct SigningService<C, P, S> {
310    /// The chain parser for decoding raw transactions.
311    chain: C,
312    /// The policy engine for evaluating rules.
313    policy: P,
314    /// The signer for producing signatures.
315    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    /// Create a new signing service with the given components.
330    ///
331    /// # Arguments
332    ///
333    /// * `chain` - The chain parser for decoding raw transactions
334    /// * `policy` - The policy engine for evaluating rules
335    /// * `signer` - The signer for producing signatures
336    ///
337    /// # Example
338    ///
339    /// ```ignore
340    /// let service = SigningService::new(chain, policy, signer);
341    /// ```
342    #[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    /// Get a reference to the chain parser.
352    #[must_use]
353    pub const fn chain(&self) -> &C {
354        &self.chain
355    }
356
357    /// Get a reference to the policy engine.
358    #[must_use]
359    pub const fn policy(&self) -> &P {
360        &self.policy
361    }
362
363    /// Get a reference to the signer.
364    #[must_use]
365    pub const fn signer(&self) -> &S {
366        &self.signer
367    }
368}
369
370/// Trait for chain parsers.
371///
372/// This trait is defined here to avoid circular dependencies.
373/// It mirrors the `Chain` trait from `txgate-chain`.
374pub trait ChainParser: Send + Sync {
375    /// Parse raw transaction bytes into a [`ParsedTx`].
376    ///
377    /// # Errors
378    ///
379    /// Returns [`ParseError`] if the transaction cannot be decoded.
380    fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError>;
381}
382
383/// Trait for policy engines.
384///
385/// This trait is defined here to avoid circular dependencies.
386/// It mirrors the `PolicyEngine` trait from `txgate-policy`.
387pub trait PolicyEngineExt: Send + Sync {
388    /// Check if a transaction is allowed by policy rules.
389    ///
390    /// # Errors
391    ///
392    /// Returns [`PolicyError`] if policy evaluation fails.
393    fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError>;
394}
395
396/// Trait for signers.
397///
398/// This trait is defined here to avoid circular dependencies.
399/// It mirrors the `Signer` trait from `txgate-crypto`.
400pub trait SignerExt: Send + Sync {
401    /// Sign a 32-byte hash.
402    ///
403    /// Returns the signature as a `Vec<u8>`. For secp256k1, this is
404    /// 65 bytes: `r (32) || s (32) || v (1)`.
405    ///
406    /// # Errors
407    ///
408    /// Returns [`SignError`] if signing fails.
409    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    /// Sign a raw transaction.
419    ///
420    /// This method performs the complete signing flow:
421    ///
422    /// 1. **Parse** - Transform raw bytes into a [`ParsedTx`]
423    /// 2. **Check** - Evaluate against policy rules
424    /// 3. **Sign** - If allowed, sign the transaction hash
425    ///
426    /// # Arguments
427    ///
428    /// * `raw_tx` - The raw transaction bytes to sign
429    ///
430    /// # Returns
431    ///
432    /// A [`SigningResult`] containing the parsed transaction, policy result,
433    /// and signature (if allowed).
434    ///
435    /// # Errors
436    ///
437    /// Returns [`SigningError`] if:
438    /// - Transaction parsing fails ([`SigningError::ParseError`])
439    /// - Policy evaluation fails ([`SigningError::PolicyError`])
440    /// - Transaction is denied by policy ([`SigningError::PolicyDenied`])
441    /// - Signing operation fails ([`SigningError::SignError`])
442    ///
443    /// # Example
444    ///
445    /// ```ignore
446    /// let result = service.sign(&raw_tx)?;
447    ///
448    /// // Access the signature
449    /// let sig = result.signature.expect("signature present");
450    /// let recovery_id = result.recovery_id.expect("recovery ID present");
451    ///
452    /// // Or get as 65-byte array
453    /// let full_sig = result.signature_with_recovery_id().unwrap();
454    /// ```
455    pub fn sign(&self, raw_tx: &[u8]) -> Result<SigningResult, SigningError> {
456        // 1. Parse transaction
457        let parsed_tx = self.chain.parse(raw_tx)?;
458
459        // 2. Check policy
460        let policy_result = self.policy.check(&parsed_tx)?;
461
462        // 3. If denied, return error with reason
463        if let PolicyResult::Denied { reason, .. } = &policy_result {
464            return Err(SigningError::PolicyDenied {
465                reason: reason.clone(),
466            });
467        }
468
469        // 4. Sign the transaction hash
470        let sig_bytes = self.signer.sign(&parsed_tx.hash)?;
471
472        // Extract signature components (expecting 65 bytes: r || s || v)
473        let (signature, recovery_id) = extract_signature_components(&sig_bytes)?;
474
475        // Return result
476        Ok(SigningResult {
477            parsed_tx,
478            policy_result: PolicyCheckResult::Allowed,
479            signature: Some(signature),
480            recovery_id: Some(recovery_id),
481        })
482    }
483
484    /// Parse and check policy without signing (dry run).
485    ///
486    /// This method performs the parsing and policy check steps without
487    /// actually signing the transaction. Useful for validating transactions
488    /// before committing to sign them.
489    ///
490    /// # Arguments
491    ///
492    /// * `raw_tx` - The raw transaction bytes to check
493    ///
494    /// # Returns
495    ///
496    /// A [`SigningResult`] containing the parsed transaction and policy result.
497    /// The `signature` and `recovery_id` fields will be `None`.
498    ///
499    /// # Errors
500    ///
501    /// Returns [`SigningError`] if:
502    /// - Transaction parsing fails ([`SigningError::ParseError`])
503    /// - Policy evaluation fails ([`SigningError::PolicyError`])
504    ///
505    /// Note: This method does NOT return an error if the policy denies the
506    /// transaction. Instead, check `result.is_allowed()` or `result.policy_result`.
507    ///
508    /// # Example
509    ///
510    /// ```ignore
511    /// let result = service.check(&raw_tx)?;
512    ///
513    /// if result.is_allowed() {
514    ///     println!("Transaction would be allowed");
515    ///     // Optionally proceed to sign
516    ///     let sign_result = service.sign(&raw_tx)?;
517    /// } else {
518    ///     println!("Transaction would be denied");
519    /// }
520    /// ```
521    pub fn check(&self, raw_tx: &[u8]) -> Result<SigningResult, SigningError> {
522        // 1. Parse transaction
523        let parsed_tx = self.chain.parse(raw_tx)?;
524
525        // 2. Check policy
526        let policy_result = self.policy.check(&parsed_tx)?;
527
528        // 3. Return result without signing
529        Ok(SigningResult {
530            parsed_tx,
531            policy_result: policy_result.into(),
532            signature: None,
533            recovery_id: None,
534        })
535    }
536}
537
538// ============================================================================
539// Helper Functions
540// ============================================================================
541
542/// Extract signature components from a raw signature.
543///
544/// Expects 65 bytes: `r (32) || s (32) || v (1)`.
545fn extract_signature_components(sig: &[u8]) -> Result<(SignatureBytes, u8), SigningError> {
546    // Try to convert to a fixed-size array, which validates the length
547    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// ============================================================================
562// Tests
563// ============================================================================
564
565#[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    // ========================================================================
582    // Mock Implementations
583    // ========================================================================
584
585    /// Configuration for mock chain behavior.
586    #[derive(Clone)]
587    enum MockChainBehavior {
588        Success(ParsedTx),
589        Failure(MockParseErrorKind),
590    }
591
592    /// Cloneable parse error kinds for mocks.
593    #[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    /// Mock chain parser for testing.
609    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    /// Configuration for mock policy behavior.
637    #[derive(Clone)]
638    enum MockPolicyBehavior {
639        Allowed,
640        Denied { rule: String, reason: String },
641        Error(MockPolicyErrorKind),
642    }
643
644    /// Cloneable policy error kinds for mocks.
645    #[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    /// Mock policy engine for testing.
659    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    /// Configuration for mock signer behavior.
701    enum MockSignerBehavior {
702        Success { recovery_id: u8 },
703        Failure(MockSignErrorKind),
704    }
705
706    /// Cloneable sign error kinds for mocks.
707    #[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    /// Mock signer for testing.
721    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]); // r
751                    sig[32..64].copy_from_slice(&[0xcd; 32]); // s
752                    sig[64] = *recovery_id; // v
753                    Ok(sig)
754                }
755                MockSignerBehavior::Failure(kind) => Err(kind.to_error()),
756            }
757        }
758    }
759
760    /// Helper to create a test transaction.
761    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    // ========================================================================
777    // SigningError Tests
778    // ========================================================================
779
780    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    // ========================================================================
838    // PolicyCheckResult Tests
839    // ========================================================================
840
841    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    // ========================================================================
884    // SigningResult Tests
885    // ========================================================================
886
887    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    // ========================================================================
936    // SigningService Tests
937    // ========================================================================
938
939    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            // check() does NOT return error for denial
1029            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            // Test accessors compile and work
1056            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    // ========================================================================
1076    // Extract Signature Components Tests
1077    // ========================================================================
1078
1079    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    // ========================================================================
1115    // Send + Sync Tests
1116    // ========================================================================
1117
1118    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        // SigningService is Send + Sync when C, P, S are
1140        // We can't easily test this with mocks using RefCell (not Sync),
1141        // but the trait bounds enforce it for real implementations.
1142    }
1143}