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//! 4. **Record** - Update policy history for limit tracking
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    /// Record a signed transaction for limit tracking.
396    ///
397    /// # Errors
398    ///
399    /// Returns [`PolicyError`] if recording fails.
400    fn record(&self, tx: &ParsedTx) -> Result<(), PolicyError>;
401}
402
403/// Trait for signers.
404///
405/// This trait is defined here to avoid circular dependencies.
406/// It mirrors the `Signer` trait from `txgate-crypto`.
407pub trait SignerExt: Send + Sync {
408    /// Sign a 32-byte hash.
409    ///
410    /// Returns the signature as a `Vec<u8>`. For secp256k1, this is
411    /// 65 bytes: `r (32) || s (32) || v (1)`.
412    ///
413    /// # Errors
414    ///
415    /// Returns [`SignError`] if signing fails.
416    fn sign(&self, hash: &[u8; 32]) -> Result<Vec<u8>, SignError>;
417}
418
419impl<C, P, S> SigningService<C, P, S>
420where
421    C: ChainParser,
422    P: PolicyEngineExt,
423    S: SignerExt,
424{
425    /// Sign a raw transaction.
426    ///
427    /// This method performs the complete signing flow:
428    ///
429    /// 1. **Parse** - Transform raw bytes into a [`ParsedTx`]
430    /// 2. **Check** - Evaluate against policy rules
431    /// 3. **Sign** - If allowed, sign the transaction hash
432    /// 4. **Record** - Update policy history
433    ///
434    /// # Arguments
435    ///
436    /// * `raw_tx` - The raw transaction bytes to sign
437    ///
438    /// # Returns
439    ///
440    /// A [`SigningResult`] containing the parsed transaction, policy result,
441    /// and signature (if allowed).
442    ///
443    /// # Errors
444    ///
445    /// Returns [`SigningError`] if:
446    /// - Transaction parsing fails ([`SigningError::ParseError`])
447    /// - Policy evaluation fails ([`SigningError::PolicyError`])
448    /// - Transaction is denied by policy ([`SigningError::PolicyDenied`])
449    /// - Signing operation fails ([`SigningError::SignError`])
450    ///
451    /// # Example
452    ///
453    /// ```ignore
454    /// let result = service.sign(&raw_tx)?;
455    ///
456    /// // Access the signature
457    /// let sig = result.signature.expect("signature present");
458    /// let recovery_id = result.recovery_id.expect("recovery ID present");
459    ///
460    /// // Or get as 65-byte array
461    /// let full_sig = result.signature_with_recovery_id().unwrap();
462    /// ```
463    pub fn sign(&self, raw_tx: &[u8]) -> Result<SigningResult, SigningError> {
464        // 1. Parse transaction
465        let parsed_tx = self.chain.parse(raw_tx)?;
466
467        // 2. Check policy
468        let policy_result = self.policy.check(&parsed_tx)?;
469
470        // 3. If denied, return error with reason
471        if let PolicyResult::Denied { reason, .. } = &policy_result {
472            return Err(SigningError::PolicyDenied {
473                reason: reason.clone(),
474            });
475        }
476
477        // 4. Sign the transaction hash
478        let sig_bytes = self.signer.sign(&parsed_tx.hash)?;
479
480        // Extract signature components (expecting 65 bytes: r || s || v)
481        let (signature, recovery_id) = extract_signature_components(&sig_bytes)?;
482
483        // 5. Record in history
484        self.policy.record(&parsed_tx)?;
485
486        // 6. Return result
487        Ok(SigningResult {
488            parsed_tx,
489            policy_result: PolicyCheckResult::Allowed,
490            signature: Some(signature),
491            recovery_id: Some(recovery_id),
492        })
493    }
494
495    /// Parse and check policy without signing (dry run).
496    ///
497    /// This method performs the parsing and policy check steps without
498    /// actually signing the transaction. Useful for validating transactions
499    /// before committing to sign them.
500    ///
501    /// # Arguments
502    ///
503    /// * `raw_tx` - The raw transaction bytes to check
504    ///
505    /// # Returns
506    ///
507    /// A [`SigningResult`] containing the parsed transaction and policy result.
508    /// The `signature` and `recovery_id` fields will be `None`.
509    ///
510    /// # Errors
511    ///
512    /// Returns [`SigningError`] if:
513    /// - Transaction parsing fails ([`SigningError::ParseError`])
514    /// - Policy evaluation fails ([`SigningError::PolicyError`])
515    ///
516    /// Note: This method does NOT return an error if the policy denies the
517    /// transaction. Instead, check `result.is_allowed()` or `result.policy_result`.
518    ///
519    /// # Example
520    ///
521    /// ```ignore
522    /// let result = service.check(&raw_tx)?;
523    ///
524    /// if result.is_allowed() {
525    ///     println!("Transaction would be allowed");
526    ///     // Optionally proceed to sign
527    ///     let sign_result = service.sign(&raw_tx)?;
528    /// } else {
529    ///     println!("Transaction would be denied");
530    /// }
531    /// ```
532    pub fn check(&self, raw_tx: &[u8]) -> Result<SigningResult, SigningError> {
533        // 1. Parse transaction
534        let parsed_tx = self.chain.parse(raw_tx)?;
535
536        // 2. Check policy
537        let policy_result = self.policy.check(&parsed_tx)?;
538
539        // 3. Return result without signing
540        Ok(SigningResult {
541            parsed_tx,
542            policy_result: policy_result.into(),
543            signature: None,
544            recovery_id: None,
545        })
546    }
547}
548
549// ============================================================================
550// Helper Functions
551// ============================================================================
552
553/// Extract signature components from a raw signature.
554///
555/// Expects 65 bytes: `r (32) || s (32) || v (1)`.
556fn extract_signature_components(sig: &[u8]) -> Result<(SignatureBytes, u8), SigningError> {
557    // Try to convert to a fixed-size array, which validates the length
558    let sig_array: [u8; 65] = sig.try_into().map_err(|_| {
559        SigningError::SignError(SignError::signature_failed(format!(
560            "expected 65-byte signature, got {} bytes",
561            sig.len()
562        )))
563    })?;
564
565    let mut signature = [0u8; 64];
566    signature.copy_from_slice(&sig_array[..64]);
567    let recovery_id = sig_array[64];
568
569    Ok((signature, recovery_id))
570}
571
572// ============================================================================
573// Tests
574// ============================================================================
575
576#[cfg(test)]
577mod tests {
578    #![allow(
579        clippy::expect_used,
580        clippy::unwrap_used,
581        clippy::panic,
582        clippy::indexing_slicing,
583        clippy::large_enum_variant,
584        clippy::redundant_clone,
585        dead_code
586    )]
587
588    use super::*;
589    use crate::types::TxType;
590    use std::collections::HashMap;
591    use std::sync::atomic::{AtomicUsize, Ordering};
592
593    // ========================================================================
594    // Mock Implementations
595    // ========================================================================
596
597    /// Configuration for mock chain behavior.
598    #[derive(Clone)]
599    enum MockChainBehavior {
600        Success(ParsedTx),
601        Failure(MockParseErrorKind),
602    }
603
604    /// Cloneable parse error kinds for mocks.
605    #[derive(Clone, Copy)]
606    enum MockParseErrorKind {
607        UnknownTxType,
608        MalformedTransaction,
609    }
610
611    impl MockParseErrorKind {
612        fn to_error(self) -> ParseError {
613            match self {
614                Self::UnknownTxType => ParseError::UnknownTxType,
615                Self::MalformedTransaction => ParseError::malformed_transaction("mock error"),
616            }
617        }
618    }
619
620    /// Mock chain parser for testing.
621    struct MockChain {
622        behavior: MockChainBehavior,
623    }
624
625    impl MockChain {
626        fn success(tx: ParsedTx) -> Self {
627            Self {
628                behavior: MockChainBehavior::Success(tx),
629            }
630        }
631
632        fn failure(kind: MockParseErrorKind) -> Self {
633            Self {
634                behavior: MockChainBehavior::Failure(kind),
635            }
636        }
637    }
638
639    impl ChainParser for MockChain {
640        fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
641            match &self.behavior {
642                MockChainBehavior::Success(tx) => Ok(tx.clone()),
643                MockChainBehavior::Failure(kind) => Err(kind.to_error()),
644            }
645        }
646    }
647
648    /// Configuration for mock policy behavior.
649    #[derive(Clone)]
650    enum MockPolicyBehavior {
651        Allowed,
652        Denied { rule: String, reason: String },
653        Error(MockPolicyErrorKind),
654    }
655
656    /// Cloneable policy error kinds for mocks.
657    #[derive(Clone, Copy)]
658    enum MockPolicyErrorKind {
659        InvalidConfiguration,
660    }
661
662    impl MockPolicyErrorKind {
663        fn to_error(self) -> PolicyError {
664            match self {
665                Self::InvalidConfiguration => PolicyError::invalid_configuration("mock error"),
666            }
667        }
668    }
669
670    /// Mock policy engine for testing.
671    struct MockPolicy {
672        check_behavior: MockPolicyBehavior,
673        record_count: AtomicUsize,
674    }
675
676    impl MockPolicy {
677        fn allowed() -> Self {
678            Self {
679                check_behavior: MockPolicyBehavior::Allowed,
680                record_count: AtomicUsize::new(0),
681            }
682        }
683
684        fn denied(rule: &str, reason: &str) -> Self {
685            Self {
686                check_behavior: MockPolicyBehavior::Denied {
687                    rule: rule.to_string(),
688                    reason: reason.to_string(),
689                },
690                record_count: AtomicUsize::new(0),
691            }
692        }
693
694        #[allow(dead_code)]
695        fn check_error(kind: MockPolicyErrorKind) -> Self {
696            Self {
697                check_behavior: MockPolicyBehavior::Error(kind),
698                record_count: AtomicUsize::new(0),
699            }
700        }
701
702        fn recorded_count(&self) -> usize {
703            self.record_count.load(Ordering::SeqCst)
704        }
705    }
706
707    impl PolicyEngineExt for MockPolicy {
708        fn check(&self, _tx: &ParsedTx) -> Result<PolicyResult, PolicyError> {
709            match &self.check_behavior {
710                MockPolicyBehavior::Allowed => Ok(PolicyResult::Allowed),
711                MockPolicyBehavior::Denied { rule, reason } => Ok(PolicyResult::Denied {
712                    rule: rule.clone(),
713                    reason: reason.clone(),
714                }),
715                MockPolicyBehavior::Error(kind) => Err(kind.to_error()),
716            }
717        }
718
719        fn record(&self, _tx: &ParsedTx) -> Result<(), PolicyError> {
720            self.record_count.fetch_add(1, Ordering::SeqCst);
721            Ok(())
722        }
723    }
724
725    /// Configuration for mock signer behavior.
726    enum MockSignerBehavior {
727        Success { recovery_id: u8 },
728        Failure(MockSignErrorKind),
729    }
730
731    /// Cloneable sign error kinds for mocks.
732    #[derive(Clone, Copy)]
733    enum MockSignErrorKind {
734        InvalidKey,
735    }
736
737    impl MockSignErrorKind {
738        fn to_error(self) -> SignError {
739            match self {
740                Self::InvalidKey => SignError::InvalidKey,
741            }
742        }
743    }
744
745    /// Mock signer for testing.
746    struct MockSigner {
747        behavior: MockSignerBehavior,
748    }
749
750    impl MockSigner {
751        fn success() -> Self {
752            Self {
753                behavior: MockSignerBehavior::Success { recovery_id: 0 },
754            }
755        }
756
757        fn success_with_recovery_id(recovery_id: u8) -> Self {
758            Self {
759                behavior: MockSignerBehavior::Success { recovery_id },
760            }
761        }
762
763        fn failure(kind: MockSignErrorKind) -> Self {
764            Self {
765                behavior: MockSignerBehavior::Failure(kind),
766            }
767        }
768    }
769
770    impl SignerExt for MockSigner {
771        fn sign(&self, _hash: &[u8; 32]) -> Result<Vec<u8>, SignError> {
772            match &self.behavior {
773                MockSignerBehavior::Success { recovery_id } => {
774                    let mut sig = vec![0u8; 65];
775                    sig[..32].copy_from_slice(&[0xab; 32]); // r
776                    sig[32..64].copy_from_slice(&[0xcd; 32]); // s
777                    sig[64] = *recovery_id; // v
778                    Ok(sig)
779                }
780                MockSignerBehavior::Failure(kind) => Err(kind.to_error()),
781            }
782        }
783    }
784
785    /// Helper to create a test transaction.
786    fn test_tx() -> ParsedTx {
787        ParsedTx {
788            hash: [0x42; 32],
789            recipient: Some("0x1234".to_string()),
790            amount: Some(crate::U256::from(100)),
791            token: Some("ETH".to_string()),
792            token_address: None,
793            tx_type: TxType::Transfer,
794            chain: "ethereum".to_string(),
795            nonce: Some(1),
796            chain_id: Some(1),
797            metadata: HashMap::new(),
798        }
799    }
800
801    // ========================================================================
802    // SigningError Tests
803    // ========================================================================
804
805    mod signing_error_tests {
806        use super::*;
807
808        #[test]
809        fn test_from_parse_error() {
810            let err = ParseError::UnknownTxType;
811            let signing_err: SigningError = err.into();
812
813            assert!(matches!(signing_err, SigningError::ParseError(_)));
814            assert!(!signing_err.is_policy_denied());
815            assert!(signing_err.denial_reason().is_none());
816        }
817
818        #[test]
819        fn test_from_policy_error() {
820            let err = PolicyError::invalid_configuration("test");
821            let signing_err: SigningError = err.into();
822
823            assert!(matches!(signing_err, SigningError::PolicyError(_)));
824            assert!(!signing_err.is_policy_denied());
825        }
826
827        #[test]
828        fn test_from_sign_error() {
829            let err = SignError::InvalidKey;
830            let signing_err: SigningError = err.into();
831
832            assert!(matches!(signing_err, SigningError::SignError(_)));
833            assert!(!signing_err.is_policy_denied());
834        }
835
836        #[test]
837        fn test_policy_denied() {
838            let err = SigningError::policy_denied("blacklisted address");
839
840            assert!(err.is_policy_denied());
841            assert_eq!(err.denial_reason(), Some("blacklisted address"));
842            assert!(err.to_string().contains("denied by policy"));
843        }
844
845        #[test]
846        fn test_error_display() {
847            let parse_err: SigningError = ParseError::UnknownTxType.into();
848            assert!(parse_err.to_string().contains("parse transaction"));
849
850            let policy_err: SigningError = PolicyError::invalid_configuration("test").into();
851            assert!(policy_err.to_string().contains("policy check failed"));
852
853            let sign_err: SigningError = SignError::InvalidKey.into();
854            assert!(sign_err.to_string().contains("signing failed"));
855
856            let denied_err = SigningError::policy_denied("test reason");
857            assert!(denied_err.to_string().contains("denied by policy"));
858            assert!(denied_err.to_string().contains("test reason"));
859        }
860    }
861
862    // ========================================================================
863    // PolicyCheckResult Tests
864    // ========================================================================
865
866    mod policy_check_result_tests {
867        use super::*;
868
869        #[test]
870        fn test_allowed() {
871            let result = PolicyCheckResult::Allowed;
872            assert!(result.is_allowed());
873            assert!(!result.is_denied());
874        }
875
876        #[test]
877        fn test_denied() {
878            let result = PolicyCheckResult::denied("blacklist", "address blocked");
879            assert!(!result.is_allowed());
880            assert!(result.is_denied());
881        }
882
883        #[test]
884        fn test_from_policy_result_allowed() {
885            let policy_result = PolicyResult::Allowed;
886            let check_result: PolicyCheckResult = policy_result.into();
887            assert!(check_result.is_allowed());
888        }
889
890        #[test]
891        fn test_from_policy_result_denied() {
892            let policy_result = PolicyResult::Denied {
893                rule: "whitelist".to_string(),
894                reason: "not in list".to_string(),
895            };
896            let check_result: PolicyCheckResult = policy_result.into();
897            assert!(check_result.is_denied());
898
899            if let PolicyCheckResult::Denied { rule, reason } = check_result {
900                assert_eq!(rule, "whitelist");
901                assert_eq!(reason, "not in list");
902            } else {
903                panic!("expected Denied variant");
904            }
905        }
906    }
907
908    // ========================================================================
909    // SigningResult Tests
910    // ========================================================================
911
912    mod signing_result_tests {
913        use super::*;
914
915        #[test]
916        fn test_allowed_constructor() {
917            let tx = test_tx();
918            let sig = [0xab; 64];
919            let result = SigningResult::allowed(tx.clone(), sig, 0);
920
921            assert!(result.is_allowed());
922            assert!(result.has_signature());
923            assert_eq!(result.signature, Some(sig));
924            assert_eq!(result.recovery_id, Some(0));
925            assert_eq!(result.parsed_tx.hash, tx.hash);
926        }
927
928        #[test]
929        fn test_checked_constructor() {
930            let tx = test_tx();
931            let result = SigningResult::checked(tx.clone(), PolicyCheckResult::Allowed);
932
933            assert!(result.is_allowed());
934            assert!(!result.has_signature());
935            assert!(result.signature.is_none());
936            assert!(result.recovery_id.is_none());
937        }
938
939        #[test]
940        fn test_signature_with_recovery_id() {
941            let tx = test_tx();
942            let sig = [0xab; 64];
943            let result = SigningResult::allowed(tx, sig, 1);
944
945            let full_sig = result.signature_with_recovery_id().unwrap();
946            assert_eq!(full_sig.len(), 65);
947            assert_eq!(&full_sig[..64], &sig);
948            assert_eq!(full_sig[64], 1);
949        }
950
951        #[test]
952        fn test_signature_with_recovery_id_none() {
953            let tx = test_tx();
954            let result = SigningResult::checked(tx, PolicyCheckResult::Allowed);
955
956            assert!(result.signature_with_recovery_id().is_none());
957        }
958    }
959
960    // ========================================================================
961    // SigningService Tests
962    // ========================================================================
963
964    mod signing_service_tests {
965        use super::*;
966
967        #[test]
968        fn test_successful_signing_flow() {
969            let tx = test_tx();
970            let chain = MockChain::success(tx.clone());
971            let policy = MockPolicy::allowed();
972            let signer = MockSigner::success();
973
974            let service = SigningService::new(chain, policy, signer);
975            let result = service.sign(&[0x01, 0x02, 0x03]).unwrap();
976
977            assert!(result.is_allowed());
978            assert!(result.has_signature());
979            assert!(result.signature.is_some());
980            assert_eq!(result.recovery_id, Some(0));
981            assert_eq!(result.parsed_tx.hash, tx.hash);
982        }
983
984        #[test]
985        fn test_policy_denial_flow() {
986            let tx = test_tx();
987            let chain = MockChain::success(tx);
988            let policy = MockPolicy::denied("blacklist", "address is blacklisted");
989            let signer = MockSigner::success();
990
991            let service = SigningService::new(chain, policy, signer);
992            let result = service.sign(&[0x01]);
993
994            assert!(result.is_err());
995            let err = result.unwrap_err();
996            assert!(err.is_policy_denied());
997            assert_eq!(err.denial_reason(), Some("address is blacklisted"));
998        }
999
1000        #[test]
1001        fn test_parse_error_handling() {
1002            let chain = MockChain::failure(MockParseErrorKind::UnknownTxType);
1003            let policy = MockPolicy::allowed();
1004            let signer = MockSigner::success();
1005
1006            let service = SigningService::new(chain, policy, signer);
1007            let result = service.sign(&[0x01]);
1008
1009            assert!(result.is_err());
1010            assert!(matches!(result.unwrap_err(), SigningError::ParseError(_)));
1011        }
1012
1013        #[test]
1014        fn test_sign_error_handling() {
1015            let tx = test_tx();
1016            let chain = MockChain::success(tx);
1017            let policy = MockPolicy::allowed();
1018            let signer = MockSigner::failure(MockSignErrorKind::InvalidKey);
1019
1020            let service = SigningService::new(chain, policy, signer);
1021            let result = service.sign(&[0x01]);
1022
1023            assert!(result.is_err());
1024            assert!(matches!(result.unwrap_err(), SigningError::SignError(_)));
1025        }
1026
1027        #[test]
1028        fn test_dry_run_check() {
1029            let tx = test_tx();
1030            let chain = MockChain::success(tx.clone());
1031            let policy = MockPolicy::allowed();
1032            let signer = MockSigner::success();
1033
1034            let service = SigningService::new(chain, policy, signer);
1035            let result = service.check(&[0x01]).unwrap();
1036
1037            assert!(result.is_allowed());
1038            assert!(!result.has_signature());
1039            assert!(result.signature.is_none());
1040            assert_eq!(result.parsed_tx.hash, tx.hash);
1041        }
1042
1043        #[test]
1044        fn test_dry_run_check_denied() {
1045            let tx = test_tx();
1046            let chain = MockChain::success(tx);
1047            let policy = MockPolicy::denied("tx_limit", "exceeds limit");
1048            let signer = MockSigner::success();
1049
1050            let service = SigningService::new(chain, policy, signer);
1051            let result = service.check(&[0x01]).unwrap();
1052
1053            // check() does NOT return error for denial
1054            assert!(!result.is_allowed());
1055            assert!(!result.has_signature());
1056        }
1057
1058        #[test]
1059        fn test_record_called_on_success() {
1060            let tx = test_tx();
1061            let chain = MockChain::success(tx);
1062            let policy = MockPolicy::allowed();
1063            let signer = MockSigner::success();
1064
1065            let service = SigningService::new(chain, policy, signer);
1066
1067            // Before signing
1068            assert_eq!(service.policy().recorded_count(), 0);
1069
1070            // Sign
1071            service.sign(&[0x01]).unwrap();
1072
1073            // After signing - record should have been called
1074            assert_eq!(service.policy().recorded_count(), 1);
1075        }
1076
1077        #[test]
1078        fn test_record_not_called_on_denial() {
1079            let tx = test_tx();
1080            let chain = MockChain::success(tx);
1081            let policy = MockPolicy::denied("test", "denied");
1082            let signer = MockSigner::success();
1083
1084            let service = SigningService::new(chain, policy, signer);
1085
1086            // Try to sign (will fail due to denial)
1087            let _ = service.sign(&[0x01]);
1088
1089            // Record should NOT have been called
1090            assert_eq!(service.policy().recorded_count(), 0);
1091        }
1092
1093        #[test]
1094        fn test_record_not_called_on_check() {
1095            let tx = test_tx();
1096            let chain = MockChain::success(tx);
1097            let policy = MockPolicy::allowed();
1098            let signer = MockSigner::success();
1099
1100            let service = SigningService::new(chain, policy, signer);
1101
1102            // Check (dry run)
1103            service.check(&[0x01]).unwrap();
1104
1105            // Record should NOT have been called
1106            assert_eq!(service.policy().recorded_count(), 0);
1107        }
1108
1109        #[test]
1110        fn test_recovery_id_passed_through() {
1111            let tx = test_tx();
1112            let chain = MockChain::success(tx);
1113            let policy = MockPolicy::allowed();
1114            let signer = MockSigner::success_with_recovery_id(1);
1115
1116            let service = SigningService::new(chain, policy, signer);
1117            let result = service.sign(&[0x01]).unwrap();
1118
1119            assert_eq!(result.recovery_id, Some(1));
1120        }
1121
1122        #[test]
1123        fn test_accessors() {
1124            let tx = test_tx();
1125            let chain = MockChain::success(tx);
1126            let policy = MockPolicy::allowed();
1127            let signer = MockSigner::success();
1128
1129            let service = SigningService::new(chain, policy, signer);
1130
1131            // Test accessors compile and work
1132            let _ = service.chain();
1133            let _ = service.policy();
1134            let _ = service.signer();
1135        }
1136
1137        #[test]
1138        fn test_debug_impl() {
1139            let tx = test_tx();
1140            let chain = MockChain::success(tx);
1141            let policy = MockPolicy::allowed();
1142            let signer = MockSigner::success();
1143
1144            let service = SigningService::new(chain, policy, signer);
1145            let debug_str = format!("{service:?}");
1146
1147            assert!(debug_str.contains("SigningService"));
1148        }
1149    }
1150
1151    // ========================================================================
1152    // Extract Signature Components Tests
1153    // ========================================================================
1154
1155    mod extract_signature_tests {
1156        use super::*;
1157
1158        #[test]
1159        fn test_valid_65_byte_signature() {
1160            let mut sig = vec![0u8; 65];
1161            sig[..32].copy_from_slice(&[0xaa; 32]);
1162            sig[32..64].copy_from_slice(&[0xbb; 32]);
1163            sig[64] = 1;
1164
1165            let (signature, recovery_id) = extract_signature_components(&sig).unwrap();
1166
1167            assert_eq!(&signature[..32], &[0xaa; 32]);
1168            assert_eq!(&signature[32..64], &[0xbb; 32]);
1169            assert_eq!(recovery_id, 1);
1170        }
1171
1172        #[test]
1173        fn test_invalid_length_too_short() {
1174            let sig = vec![0u8; 64];
1175            let result = extract_signature_components(&sig);
1176
1177            assert!(result.is_err());
1178            assert!(matches!(result.unwrap_err(), SigningError::SignError(_)));
1179        }
1180
1181        #[test]
1182        fn test_invalid_length_too_long() {
1183            let sig = vec![0u8; 66];
1184            let result = extract_signature_components(&sig);
1185
1186            assert!(result.is_err());
1187        }
1188    }
1189
1190    // ========================================================================
1191    // Send + Sync Tests
1192    // ========================================================================
1193
1194    mod send_sync_tests {
1195        use super::*;
1196
1197        #[test]
1198        fn test_signing_error_is_send_sync() {
1199            fn assert_send_sync<T: Send + Sync>() {}
1200            assert_send_sync::<SigningError>();
1201        }
1202
1203        #[test]
1204        fn test_signing_result_is_send_sync() {
1205            fn assert_send_sync<T: Send + Sync>() {}
1206            assert_send_sync::<SigningResult>();
1207        }
1208
1209        #[test]
1210        fn test_policy_check_result_is_send_sync() {
1211            fn assert_send_sync<T: Send + Sync>() {}
1212            assert_send_sync::<PolicyCheckResult>();
1213        }
1214
1215        // SigningService is Send + Sync when C, P, S are
1216        // We can't easily test this with mocks using RefCell (not Sync),
1217        // but the trait bounds enforce it for real implementations.
1218    }
1219}