Skip to main content

txgate_policy/
engine.rs

1//! Policy engine for transaction rule enforcement.
2//!
3//! This module provides the core policy engine that evaluates transactions against
4//! configured rules including whitelists, blacklists, and amount limits.
5//!
6//! # Rule Evaluation Order
7//!
8//! Rules are evaluated in strict priority order:
9//!
10//! 1. **Blacklist** - Highest priority. If the recipient is blacklisted, DENY immediately.
11//! 2. **Whitelist** - If whitelist is enabled and recipient is not whitelisted, DENY.
12//! 3. **Transaction Limit** - If amount exceeds per-transaction limit, DENY.
13//! 4. **Daily Limit** - If amount would exceed daily limit, DENY.
14//! 5. **Allow** - If all checks pass, the transaction is ALLOWED.
15//!
16//! # Thread Safety
17//!
18//! The [`DefaultPolicyEngine`] is `Send + Sync` and can be safely shared across threads.
19//!
20//! # Example
21//!
22//! ```no_run
23//! use txgate_policy::engine::{PolicyEngine, DefaultPolicyEngine};
24//! use txgate_policy::config::PolicyConfig;
25//! use txgate_policy::history::TransactionHistory;
26//! use txgate_core::types::{ParsedTx, PolicyResult};
27//! use alloy_primitives::U256;
28//! use std::sync::Arc;
29//!
30//! // Create a policy config
31//! let config = PolicyConfig::new()
32//!     .with_blacklist(vec!["0xBAD".to_string()])
33//!     .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64));
34//!
35//! // Create transaction history
36//! let history = Arc::new(TransactionHistory::in_memory().unwrap());
37//!
38//! // Create the engine
39//! let engine = DefaultPolicyEngine::new(config, history).unwrap();
40//!
41//! // Check a transaction
42//! let tx = ParsedTx::default();
43//! let result = engine.check(&tx);
44//! ```
45
46use crate::config::PolicyConfig;
47use crate::history::TransactionHistory;
48use alloy_primitives::U256;
49use std::sync::Arc;
50use txgate_core::error::PolicyError;
51use txgate_core::types::{ParsedTx, PolicyResult};
52
53/// Trait for policy engines that enforce transaction rules.
54///
55/// Implementors of this trait can check transactions against policy rules
56/// and record signed transactions for tracking purposes (e.g., daily limits).
57///
58/// # Thread Safety
59///
60/// All implementations must be `Send + Sync` to allow concurrent access
61/// from multiple request handlers.
62///
63/// # Example
64///
65/// ```no_run
66/// use txgate_policy::engine::PolicyEngine;
67/// use txgate_core::types::{ParsedTx, PolicyResult};
68/// use txgate_core::error::PolicyError;
69///
70/// fn process_transaction(engine: &dyn PolicyEngine, tx: &ParsedTx) -> Result<(), PolicyError> {
71///     let result = engine.check(tx)?;
72///     if result.is_allowed() {
73///         // Transaction approved, record it
74///         engine.record(tx)?;
75///     }
76///     Ok(())
77/// }
78/// ```
79pub trait PolicyEngine: Send + Sync {
80    /// Check if a transaction is allowed by policy rules.
81    ///
82    /// Evaluates the transaction against all configured policy rules in order
83    /// of priority (blacklist > whitelist > `tx_limit` > `daily_limit`).
84    ///
85    /// # Arguments
86    ///
87    /// * `tx` - The parsed transaction to check
88    ///
89    /// # Returns
90    ///
91    /// * `Ok(PolicyResult::Allowed)` - Transaction passes all policy checks
92    /// * `Ok(PolicyResult::Denied { rule, reason })` - Transaction denied by a rule
93    /// * `Err(PolicyError)` - Policy evaluation failed (e.g., database error)
94    ///
95    /// # Errors
96    ///
97    /// Returns [`PolicyError`] if policy evaluation fails due to:
98    /// - Database errors when checking daily limits
99    /// - Invalid policy configuration
100    fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError>;
101
102    /// Record a transaction that was signed (for limit tracking).
103    ///
104    /// This should be called after a transaction is successfully signed
105    /// to update the daily spending totals.
106    ///
107    /// # Arguments
108    ///
109    /// * `tx` - The signed transaction to record
110    ///
111    /// # Errors
112    ///
113    /// Returns [`PolicyError`] if recording fails due to:
114    /// - Database errors
115    /// - Internal errors
116    fn record(&self, tx: &ParsedTx) -> Result<(), PolicyError>;
117}
118
119/// Detailed result of a policy check operation.
120///
121/// Provides specific information about why a transaction was allowed or denied,
122/// enabling detailed error messages and audit logging.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum PolicyCheckResult {
125    /// Transaction is allowed by all policy rules.
126    Allowed,
127
128    /// Transaction denied - recipient address is blacklisted.
129    DeniedBlacklisted {
130        /// The blacklisted address.
131        address: String,
132    },
133
134    /// Transaction denied - recipient address is not in the whitelist.
135    DeniedNotWhitelisted {
136        /// The address that is not whitelisted.
137        address: String,
138    },
139
140    /// Transaction denied - amount exceeds single transaction limit.
141    DeniedExceedsTransactionLimit {
142        /// Token identifier (e.g., "ETH" or contract address).
143        token: String,
144        /// The requested transaction amount.
145        amount: U256,
146        /// The configured limit.
147        limit: U256,
148    },
149
150    /// Transaction denied - amount would exceed daily limit.
151    DeniedExceedsDailyLimit {
152        /// Token identifier (e.g., "ETH" or contract address).
153        token: String,
154        /// The requested transaction amount.
155        amount: U256,
156        /// The current daily total before this transaction.
157        daily_total: U256,
158        /// The configured daily limit.
159        limit: U256,
160    },
161}
162
163impl PolicyCheckResult {
164    /// Returns `true` if the transaction is allowed.
165    #[must_use]
166    pub const fn is_allowed(&self) -> bool {
167        matches!(self, Self::Allowed)
168    }
169
170    /// Returns `true` if the transaction is denied.
171    #[must_use]
172    pub const fn is_denied(&self) -> bool {
173        !self.is_allowed()
174    }
175
176    /// Returns the rule name that caused the denial, if any.
177    #[must_use]
178    pub const fn rule_name(&self) -> Option<&'static str> {
179        match self {
180            Self::Allowed => None,
181            Self::DeniedBlacklisted { .. } => Some("blacklist"),
182            Self::DeniedNotWhitelisted { .. } => Some("whitelist"),
183            Self::DeniedExceedsTransactionLimit { .. } => Some("tx_limit"),
184            Self::DeniedExceedsDailyLimit { .. } => Some("daily_limit"),
185        }
186    }
187
188    /// Returns a human-readable reason for the denial, if any.
189    #[must_use]
190    pub fn reason(&self) -> Option<String> {
191        match self {
192            Self::Allowed => None,
193            Self::DeniedBlacklisted { address } => {
194                Some(format!("recipient address is blacklisted: {address}"))
195            }
196            Self::DeniedNotWhitelisted { address } => {
197                Some(format!("recipient address not in whitelist: {address}"))
198            }
199            Self::DeniedExceedsTransactionLimit {
200                token,
201                amount,
202                limit,
203            } => Some(format!(
204                "amount {amount} exceeds transaction limit {limit} for {token}"
205            )),
206            Self::DeniedExceedsDailyLimit {
207                token,
208                amount,
209                daily_total,
210                limit,
211            } => Some(format!(
212                "amount {amount} plus daily total {daily_total} exceeds daily limit {limit} for {token}"
213            )),
214        }
215    }
216}
217
218impl From<PolicyCheckResult> for PolicyResult {
219    fn from(result: PolicyCheckResult) -> Self {
220        if result == PolicyCheckResult::Allowed {
221            Self::Allowed
222        } else {
223            let rule = result.rule_name().unwrap_or("unknown").to_string();
224            let reason = result
225                .reason()
226                .unwrap_or_else(|| "policy denied".to_string());
227            Self::Denied { rule, reason }
228        }
229    }
230}
231
232/// Default policy engine implementation.
233///
234/// Evaluates transactions against configured whitelist, blacklist, and limit rules.
235/// Uses [`TransactionHistory`] for tracking daily spending totals.
236///
237/// # Rule Evaluation Order
238///
239/// 1. Blacklist check (highest priority)
240/// 2. Whitelist check (if enabled)
241/// 3. Transaction limit check
242/// 4. Daily limit check
243/// 5. Allow (if all checks pass)
244///
245/// # Thread Safety
246///
247/// This struct is `Send + Sync` and can be safely shared across threads.
248/// The underlying [`TransactionHistory`] handles concurrent access.
249pub struct DefaultPolicyEngine {
250    /// Policy configuration with whitelist, blacklist, and limits.
251    config: PolicyConfig,
252    /// Transaction history for tracking daily totals.
253    history: Arc<TransactionHistory>,
254}
255
256impl std::fmt::Debug for DefaultPolicyEngine {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        f.debug_struct("DefaultPolicyEngine")
259            .field("config", &self.config)
260            .field("history", &"<TransactionHistory>")
261            .finish()
262    }
263}
264
265impl DefaultPolicyEngine {
266    /// Creates a new policy engine with the given configuration and history.
267    ///
268    /// # Arguments
269    ///
270    /// * `config` - Policy configuration with whitelist, blacklist, and limits
271    /// * `history` - Transaction history for tracking daily totals
272    ///
273    /// # Returns
274    ///
275    /// A new `DefaultPolicyEngine` instance.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`PolicyError::InvalidConfiguration`] if the configuration is invalid
280    /// (e.g., an address appears in both whitelist and blacklist).
281    ///
282    /// # Example
283    ///
284    /// ```no_run
285    /// use txgate_policy::engine::DefaultPolicyEngine;
286    /// use txgate_policy::config::PolicyConfig;
287    /// use txgate_policy::history::TransactionHistory;
288    /// use alloy_primitives::U256;
289    /// use std::sync::Arc;
290    ///
291    /// let config = PolicyConfig::new()
292    ///     .with_whitelist(vec!["0xAAA".to_string()])
293    ///     .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64));
294    ///
295    /// let history = Arc::new(TransactionHistory::in_memory().unwrap());
296    /// let engine = DefaultPolicyEngine::new(config, history).unwrap();
297    /// ```
298    pub fn new(
299        config: PolicyConfig,
300        history: Arc<TransactionHistory>,
301    ) -> Result<Self, PolicyError> {
302        // Validate configuration
303        config.validate()?;
304
305        Ok(Self { config, history })
306    }
307
308    /// Check recipient against blacklist.
309    ///
310    /// # Returns
311    ///
312    /// * `Some(DeniedBlacklisted)` - If the recipient is blacklisted
313    /// * `None` - If the recipient is not blacklisted or has no recipient
314    fn check_blacklist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
315        let recipient = tx.recipient.as_ref()?;
316
317        if self.config.is_blacklisted(recipient) {
318            return Some(PolicyCheckResult::DeniedBlacklisted {
319                address: recipient.clone(),
320            });
321        }
322
323        None
324    }
325
326    /// Check recipient against whitelist (if enabled).
327    ///
328    /// # Returns
329    ///
330    /// * `Some(DeniedNotWhitelisted)` - If whitelist is enabled and recipient is not whitelisted
331    /// * `None` - If whitelist is disabled, recipient is whitelisted, or transaction has no recipient
332    fn check_whitelist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
333        // Whitelist only applies if enabled
334        if !self.config.whitelist_enabled {
335            return None;
336        }
337
338        let recipient = tx.recipient.as_ref()?;
339
340        if !self.config.is_whitelisted(recipient) {
341            return Some(PolicyCheckResult::DeniedNotWhitelisted {
342                address: recipient.clone(),
343            });
344        }
345
346        None
347    }
348
349    /// Check transaction amount against per-transaction limit.
350    ///
351    /// # Returns
352    ///
353    /// * `Some(DeniedExceedsTransactionLimit)` - If amount exceeds the configured limit
354    /// * `None` - If no limit is configured, amount is within limit, or transaction has no amount
355    fn check_transaction_limit(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
356        let amount = tx.amount?;
357
358        // Determine token key: use token_address if present, otherwise "ETH"
359        let token = tx.token_address.as_deref().unwrap_or("ETH");
360
361        // Get the configured limit for this token
362        let limit = self.config.get_transaction_limit(token)?;
363
364        // Check if amount exceeds limit
365        if amount > limit {
366            return Some(PolicyCheckResult::DeniedExceedsTransactionLimit {
367                token: token.to_string(),
368                amount,
369                limit,
370            });
371        }
372
373        None
374    }
375
376    /// Check daily total against daily limit.
377    ///
378    /// # Returns
379    ///
380    /// * `Ok(Some(DeniedExceedsDailyLimit))` - If amount would exceed daily limit
381    /// * `Ok(None)` - If no limit is configured, within limit, or transaction has no amount
382    ///
383    /// # Errors
384    ///
385    /// Returns [`PolicyError`] if fetching the daily total fails.
386    fn check_daily_limit(&self, tx: &ParsedTx) -> Result<Option<PolicyCheckResult>, PolicyError> {
387        let Some(amount) = tx.amount else {
388            return Ok(None);
389        };
390
391        // Determine token key: use token_address if present, otherwise "ETH"
392        let token = tx.token_address.as_deref().unwrap_or("ETH");
393
394        // Get the configured daily limit for this token
395        let Some(limit) = self.config.get_daily_limit(token) else {
396            return Ok(None);
397        };
398
399        // Get current daily total from history
400        let daily_total = self.history.daily_total(token).map_err(|e| {
401            PolicyError::invalid_configuration(format!("failed to get daily total: {e}"))
402        })?;
403
404        // Check if new transaction would exceed daily limit
405        // Use saturating_add to prevent overflow
406        let new_total = daily_total.saturating_add(amount);
407
408        if new_total > limit {
409            return Ok(Some(PolicyCheckResult::DeniedExceedsDailyLimit {
410                token: token.to_string(),
411                amount,
412                daily_total,
413                limit,
414            }));
415        }
416
417        Ok(None)
418    }
419}
420
421impl PolicyEngine for DefaultPolicyEngine {
422    fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError> {
423        // 1. Check blacklist (highest priority)
424        if let Some(result) = self.check_blacklist(tx) {
425            return Ok(result.into());
426        }
427
428        // 2. Check whitelist (if enabled)
429        if let Some(result) = self.check_whitelist(tx) {
430            return Ok(result.into());
431        }
432
433        // 3. Check transaction limit
434        if let Some(result) = self.check_transaction_limit(tx) {
435            return Ok(result.into());
436        }
437
438        // 4. Check daily limit
439        if let Some(result) = self.check_daily_limit(tx)? {
440            return Ok(result.into());
441        }
442
443        // 5. All checks passed
444        Ok(PolicyResult::Allowed)
445    }
446
447    fn record(&self, tx: &ParsedTx) -> Result<(), PolicyError> {
448        // Determine token key: use token_address if present, otherwise "ETH"
449        let token = tx.token_address.as_deref().unwrap_or("ETH");
450
451        // Get amount, default to zero if not present
452        let amount = tx.amount.unwrap_or(U256::ZERO);
453
454        // Get transaction hash as hex string
455        let hash = hex::encode(tx.hash);
456
457        // Record in history
458        self.history.record(token, amount, &hash).map_err(|e| {
459            PolicyError::invalid_configuration(format!("failed to record transaction: {e}"))
460        })
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    #![allow(
467        clippy::expect_used,
468        clippy::unwrap_used,
469        clippy::panic,
470        clippy::indexing_slicing,
471        clippy::similar_names,
472        clippy::redundant_clone,
473        clippy::manual_string_new,
474        clippy::needless_raw_string_hashes,
475        clippy::needless_collect,
476        clippy::unreadable_literal
477    )]
478
479    use super::*;
480    use std::collections::HashMap;
481    use txgate_core::types::TxType;
482
483    /// Helper to create a basic test transaction.
484    fn create_test_tx(recipient: Option<&str>, amount: Option<U256>) -> ParsedTx {
485        ParsedTx {
486            hash: [0xab; 32],
487            recipient: recipient.map(String::from),
488            amount,
489            token: Some("ETH".to_string()),
490            token_address: None,
491            tx_type: TxType::Transfer,
492            chain: "ethereum".to_string(),
493            nonce: Some(1),
494            chain_id: Some(1),
495            metadata: HashMap::new(),
496        }
497    }
498
499    /// Helper to create a token transfer transaction.
500    fn create_token_tx(
501        recipient: Option<&str>,
502        amount: Option<U256>,
503        token_address: &str,
504    ) -> ParsedTx {
505        ParsedTx {
506            hash: [0xcd; 32],
507            recipient: recipient.map(String::from),
508            amount,
509            token: Some("USDC".to_string()),
510            token_address: Some(token_address.to_string()),
511            tx_type: TxType::TokenTransfer,
512            chain: "ethereum".to_string(),
513            nonce: Some(2),
514            chain_id: Some(1),
515            metadata: HashMap::new(),
516        }
517    }
518
519    // =========================================================================
520    // PolicyCheckResult tests
521    // =========================================================================
522
523    mod policy_check_result_tests {
524        use super::*;
525
526        #[test]
527        fn test_allowed_is_allowed() {
528            let result = PolicyCheckResult::Allowed;
529            assert!(result.is_allowed());
530            assert!(!result.is_denied());
531            assert!(result.rule_name().is_none());
532            assert!(result.reason().is_none());
533        }
534
535        #[test]
536        fn test_denied_blacklisted() {
537            let result = PolicyCheckResult::DeniedBlacklisted {
538                address: "0xBAD".to_string(),
539            };
540            assert!(!result.is_allowed());
541            assert!(result.is_denied());
542            assert_eq!(result.rule_name(), Some("blacklist"));
543            assert!(result.reason().unwrap().contains("blacklisted"));
544        }
545
546        #[test]
547        fn test_denied_not_whitelisted() {
548            let result = PolicyCheckResult::DeniedNotWhitelisted {
549                address: "0xUNKNOWN".to_string(),
550            };
551            assert!(!result.is_allowed());
552            assert!(result.is_denied());
553            assert_eq!(result.rule_name(), Some("whitelist"));
554            assert!(result.reason().unwrap().contains("not in whitelist"));
555        }
556
557        #[test]
558        fn test_denied_exceeds_transaction_limit() {
559            let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
560                token: "ETH".to_string(),
561                amount: U256::from(10u64),
562                limit: U256::from(5u64),
563            };
564            assert!(!result.is_allowed());
565            assert!(result.is_denied());
566            assert_eq!(result.rule_name(), Some("tx_limit"));
567            assert!(result
568                .reason()
569                .unwrap()
570                .contains("exceeds transaction limit"));
571        }
572
573        #[test]
574        fn test_denied_exceeds_daily_limit() {
575            let result = PolicyCheckResult::DeniedExceedsDailyLimit {
576                token: "ETH".to_string(),
577                amount: U256::from(5u64),
578                daily_total: U256::from(8u64),
579                limit: U256::from(10u64),
580            };
581            assert!(!result.is_allowed());
582            assert!(result.is_denied());
583            assert_eq!(result.rule_name(), Some("daily_limit"));
584            assert!(result.reason().unwrap().contains("exceeds daily limit"));
585        }
586
587        #[test]
588        fn test_conversion_to_policy_result_allowed() {
589            let check_result = PolicyCheckResult::Allowed;
590            let policy_result: PolicyResult = check_result.into();
591            assert!(policy_result.is_allowed());
592        }
593
594        #[test]
595        fn test_conversion_to_policy_result_denied() {
596            let check_result = PolicyCheckResult::DeniedBlacklisted {
597                address: "0xBAD".to_string(),
598            };
599            let policy_result: PolicyResult = check_result.into();
600            assert!(policy_result.is_denied());
601
602            if let PolicyResult::Denied { rule, reason } = policy_result {
603                assert_eq!(rule, "blacklist");
604                assert!(reason.contains("blacklisted"));
605            } else {
606                panic!("expected Denied variant");
607            }
608        }
609    }
610
611    // =========================================================================
612    // DefaultPolicyEngine creation tests
613    // =========================================================================
614
615    mod engine_creation_tests {
616        use super::*;
617
618        #[test]
619        fn test_create_engine_with_valid_config() {
620            let config = PolicyConfig::new()
621                .with_whitelist(vec!["0xAAA".to_string()])
622                .with_blacklist(vec!["0xBBB".to_string()]);
623
624            let history = Arc::new(TransactionHistory::in_memory().unwrap());
625            let engine = DefaultPolicyEngine::new(config, history);
626
627            assert!(engine.is_ok());
628        }
629
630        #[test]
631        fn test_create_engine_with_invalid_config() {
632            // Address in both whitelist and blacklist
633            let config = PolicyConfig::new()
634                .with_whitelist(vec!["0xAAA".to_string()])
635                .with_blacklist(vec!["0xAAA".to_string()]);
636
637            let history = Arc::new(TransactionHistory::in_memory().unwrap());
638            let engine = DefaultPolicyEngine::new(config, history);
639
640            assert!(engine.is_err());
641            let err = engine.unwrap_err();
642            assert!(matches!(err, PolicyError::InvalidConfiguration { .. }));
643        }
644
645        #[test]
646        fn test_create_engine_with_empty_config() {
647            let config = PolicyConfig::new();
648            let history = Arc::new(TransactionHistory::in_memory().unwrap());
649            let engine = DefaultPolicyEngine::new(config, history);
650
651            assert!(engine.is_ok());
652        }
653    }
654
655    // =========================================================================
656    // Blacklist tests
657    // =========================================================================
658
659    mod blacklist_tests {
660        use super::*;
661
662        #[test]
663        fn test_blacklist_blocks_transaction() {
664            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
665
666            let history = Arc::new(TransactionHistory::in_memory().unwrap());
667            let engine = DefaultPolicyEngine::new(config, history).unwrap();
668
669            let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
670            let result = engine.check(&tx).unwrap();
671
672            assert!(result.is_denied());
673            if let PolicyResult::Denied { rule, .. } = result {
674                assert_eq!(rule, "blacklist");
675            } else {
676                panic!("expected Denied variant");
677            }
678        }
679
680        #[test]
681        fn test_blacklist_case_insensitive() {
682            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
683
684            let history = Arc::new(TransactionHistory::in_memory().unwrap());
685            let engine = DefaultPolicyEngine::new(config, history).unwrap();
686
687            let tx = create_test_tx(Some("0xbad"), Some(U256::from(100u64)));
688            let result = engine.check(&tx).unwrap();
689
690            assert!(result.is_denied());
691        }
692
693        #[test]
694        fn test_non_blacklisted_address_allowed() {
695            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
696
697            let history = Arc::new(TransactionHistory::in_memory().unwrap());
698            let engine = DefaultPolicyEngine::new(config, history).unwrap();
699
700            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
701            let result = engine.check(&tx).unwrap();
702
703            assert!(result.is_allowed());
704        }
705
706        #[test]
707        fn test_no_recipient_skips_blacklist_check() {
708            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
709
710            let history = Arc::new(TransactionHistory::in_memory().unwrap());
711            let engine = DefaultPolicyEngine::new(config, history).unwrap();
712
713            let tx = create_test_tx(None, Some(U256::from(100u64)));
714            let result = engine.check(&tx).unwrap();
715
716            // Should pass since there's no recipient to check
717            assert!(result.is_allowed());
718        }
719    }
720
721    // =========================================================================
722    // Whitelist tests
723    // =========================================================================
724
725    mod whitelist_tests {
726        use super::*;
727
728        #[test]
729        fn test_whitelist_allows_only_whitelisted_when_enabled() {
730            let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
731
732            let history = Arc::new(TransactionHistory::in_memory().unwrap());
733            let engine = DefaultPolicyEngine::new(config, history).unwrap();
734
735            // Whitelisted address should be allowed
736            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
737            let result = engine.check(&tx).unwrap();
738            assert!(result.is_allowed());
739
740            // Non-whitelisted address should be denied
741            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
742            let result = engine.check(&tx).unwrap();
743            assert!(result.is_denied());
744            if let PolicyResult::Denied { rule, .. } = result {
745                assert_eq!(rule, "whitelist");
746            }
747        }
748
749        #[test]
750        fn test_whitelist_disabled_allows_all() {
751            let config = PolicyConfig::new()
752                .with_whitelist(vec!["0xGOOD".to_string()])
753                .with_whitelist_enabled(false);
754
755            let history = Arc::new(TransactionHistory::in_memory().unwrap());
756            let engine = DefaultPolicyEngine::new(config, history).unwrap();
757
758            // Any address should be allowed when whitelist is disabled
759            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
760            let result = engine.check(&tx).unwrap();
761            assert!(result.is_allowed());
762        }
763
764        #[test]
765        fn test_whitelist_case_insensitive() {
766            let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
767
768            let history = Arc::new(TransactionHistory::in_memory().unwrap());
769            let engine = DefaultPolicyEngine::new(config, history).unwrap();
770
771            let tx = create_test_tx(Some("0xgood"), Some(U256::from(100u64)));
772            let result = engine.check(&tx).unwrap();
773            assert!(result.is_allowed());
774        }
775
776        #[test]
777        fn test_no_recipient_skips_whitelist_check() {
778            let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
779
780            let history = Arc::new(TransactionHistory::in_memory().unwrap());
781            let engine = DefaultPolicyEngine::new(config, history).unwrap();
782
783            let tx = create_test_tx(None, Some(U256::from(100u64)));
784            let result = engine.check(&tx).unwrap();
785
786            // Should pass since there's no recipient to check
787            assert!(result.is_allowed());
788        }
789    }
790
791    // =========================================================================
792    // Transaction limit tests
793    // =========================================================================
794
795    mod transaction_limit_tests {
796        use super::*;
797
798        #[test]
799        fn test_transaction_limit_enforcement() {
800            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
801
802            let history = Arc::new(TransactionHistory::in_memory().unwrap());
803            let engine = DefaultPolicyEngine::new(config, history).unwrap();
804
805            // Within limit - should be allowed
806            let tx = create_test_tx(Some("0xREC"), Some(U256::from(50u64)));
807            let result = engine.check(&tx).unwrap();
808            assert!(result.is_allowed());
809
810            // At limit - should be allowed
811            let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
812            let result = engine.check(&tx).unwrap();
813            assert!(result.is_allowed());
814
815            // Above limit - should be denied
816            let tx = create_test_tx(Some("0xREC"), Some(U256::from(101u64)));
817            let result = engine.check(&tx).unwrap();
818            assert!(result.is_denied());
819            if let PolicyResult::Denied { rule, .. } = result {
820                assert_eq!(rule, "tx_limit");
821            }
822        }
823
824        #[test]
825        fn test_transaction_limit_for_token() {
826            let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
827            let config =
828                PolicyConfig::new().with_transaction_limit(token_address, U256::from(1000u64));
829
830            let history = Arc::new(TransactionHistory::in_memory().unwrap());
831            let engine = DefaultPolicyEngine::new(config, history).unwrap();
832
833            // Above limit for token - should be denied
834            let tx = create_token_tx(Some("0xREC"), Some(U256::from(1001u64)), token_address);
835            let result = engine.check(&tx).unwrap();
836            assert!(result.is_denied());
837
838            // ETH transfer should not be affected by token limit
839            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1001u64)));
840            let result = engine.check(&tx).unwrap();
841            assert!(result.is_allowed());
842        }
843
844        #[test]
845        fn test_no_transaction_limit_allows_any_amount() {
846            let config = PolicyConfig::new();
847
848            let history = Arc::new(TransactionHistory::in_memory().unwrap());
849            let engine = DefaultPolicyEngine::new(config, history).unwrap();
850
851            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
852            let result = engine.check(&tx).unwrap();
853            assert!(result.is_allowed());
854        }
855
856        #[test]
857        fn test_zero_transaction_limit_denies_everything() {
858            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::ZERO);
859
860            let history = Arc::new(TransactionHistory::in_memory().unwrap());
861            let engine = DefaultPolicyEngine::new(config, history).unwrap();
862
863            // Even a tiny amount should be denied
864            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
865            let result = engine.check(&tx).unwrap();
866            assert!(result.is_denied());
867
868            // Zero amount should be allowed (not exceeding)
869            let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
870            let result = engine.check(&tx).unwrap();
871            assert!(result.is_allowed());
872        }
873
874        #[test]
875        fn test_no_amount_skips_transaction_limit_check() {
876            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
877
878            let history = Arc::new(TransactionHistory::in_memory().unwrap());
879            let engine = DefaultPolicyEngine::new(config, history).unwrap();
880
881            let tx = create_test_tx(Some("0xREC"), None);
882            let result = engine.check(&tx).unwrap();
883            assert!(result.is_allowed());
884        }
885    }
886
887    // =========================================================================
888    // Daily limit tests
889    // =========================================================================
890
891    mod daily_limit_tests {
892        use super::*;
893
894        #[test]
895        fn test_daily_limit_enforcement() {
896            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
897
898            let history = Arc::new(TransactionHistory::in_memory().unwrap());
899            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
900
901            // First transaction within limit
902            let tx = create_test_tx(Some("0xREC"), Some(U256::from(500u64)));
903            let result = engine.check(&tx).unwrap();
904            assert!(result.is_allowed());
905
906            // Record the transaction
907            engine.record(&tx).unwrap();
908
909            // Second transaction that would exceed daily limit
910            let tx = create_test_tx(Some("0xREC"), Some(U256::from(600u64)));
911            let result = engine.check(&tx).unwrap();
912            assert!(result.is_denied());
913            if let PolicyResult::Denied { rule, .. } = result {
914                assert_eq!(rule, "daily_limit");
915            }
916
917            // Transaction that keeps us within limit should be allowed
918            let tx = create_test_tx(Some("0xREC"), Some(U256::from(400u64)));
919            let result = engine.check(&tx).unwrap();
920            assert!(result.is_allowed());
921        }
922
923        #[test]
924        fn test_daily_limit_for_token() {
925            let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
926            let config = PolicyConfig::new().with_daily_limit(token_address, U256::from(1000u64));
927
928            let history = Arc::new(TransactionHistory::in_memory().unwrap());
929            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
930
931            // Record a token transaction
932            let tx = create_token_tx(Some("0xREC"), Some(U256::from(800u64)), token_address);
933            engine.record(&tx).unwrap();
934
935            // Next token transaction that exceeds limit
936            let tx = create_token_tx(Some("0xREC"), Some(U256::from(300u64)), token_address);
937            let result = engine.check(&tx).unwrap();
938            assert!(result.is_denied());
939
940            // ETH transaction should not be affected
941            let tx = create_test_tx(Some("0xREC"), Some(U256::from(10000u64)));
942            let result = engine.check(&tx).unwrap();
943            assert!(result.is_allowed());
944        }
945
946        #[test]
947        fn test_no_daily_limit_allows_any_amount() {
948            let config = PolicyConfig::new();
949
950            let history = Arc::new(TransactionHistory::in_memory().unwrap());
951            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
952
953            // Record a large transaction
954            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX / U256::from(2)));
955            engine.record(&tx).unwrap();
956
957            // Another large transaction should be allowed
958            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX / U256::from(2)));
959            let result = engine.check(&tx).unwrap();
960            assert!(result.is_allowed());
961        }
962
963        #[test]
964        fn test_zero_daily_limit_denies_everything() {
965            let config = PolicyConfig::new().with_daily_limit("ETH", U256::ZERO);
966
967            let history = Arc::new(TransactionHistory::in_memory().unwrap());
968            let engine = DefaultPolicyEngine::new(config, history).unwrap();
969
970            // Even a tiny amount should be denied
971            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
972            let result = engine.check(&tx).unwrap();
973            assert!(result.is_denied());
974
975            // Zero amount should be allowed
976            let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
977            let result = engine.check(&tx).unwrap();
978            assert!(result.is_allowed());
979        }
980
981        #[test]
982        fn test_no_amount_skips_daily_limit_check() {
983            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(100u64));
984
985            let history = Arc::new(TransactionHistory::in_memory().unwrap());
986            let engine = DefaultPolicyEngine::new(config, history).unwrap();
987
988            let tx = create_test_tx(Some("0xREC"), None);
989            let result = engine.check(&tx).unwrap();
990            assert!(result.is_allowed());
991        }
992    }
993
994    // =========================================================================
995    // Rule precedence tests
996    // =========================================================================
997
998    mod rule_precedence_tests {
999        use super::*;
1000
1001        #[test]
1002        fn test_blacklist_takes_precedence_over_whitelist() {
1003            // This test verifies that blacklist check happens before whitelist
1004            // We can't have an address in both lists (validation prevents it),
1005            // but we can test that a blacklisted address is denied even if
1006            // whitelist is enabled with other addresses
1007            let config = PolicyConfig::new()
1008                .with_whitelist(vec!["0xGOOD".to_string()])
1009                .with_blacklist(vec!["0xBAD".to_string()]);
1010
1011            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1012            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1013
1014            // Blacklisted address should be denied
1015            let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
1016            let result = engine.check(&tx).unwrap();
1017            assert!(result.is_denied());
1018            if let PolicyResult::Denied { rule, .. } = result {
1019                assert_eq!(rule, "blacklist");
1020            }
1021        }
1022
1023        #[test]
1024        fn test_whitelist_takes_precedence_over_tx_limit() {
1025            let config = PolicyConfig::new()
1026                .with_whitelist(vec!["0xGOOD".to_string()])
1027                .with_transaction_limit("ETH", U256::from(100u64));
1028
1029            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1030            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1031
1032            // Non-whitelisted address should be denied by whitelist, not tx_limit
1033            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(1000u64)));
1034            let result = engine.check(&tx).unwrap();
1035            assert!(result.is_denied());
1036            if let PolicyResult::Denied { rule, .. } = result {
1037                assert_eq!(rule, "whitelist");
1038            }
1039        }
1040
1041        #[test]
1042        fn test_tx_limit_takes_precedence_over_daily_limit() {
1043            let config = PolicyConfig::new()
1044                .with_transaction_limit("ETH", U256::from(50u64))
1045                .with_daily_limit("ETH", U256::from(100u64));
1046
1047            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1048            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1049
1050            // Amount exceeds both limits, should be denied by tx_limit first
1051            let tx = create_test_tx(Some("0xREC"), Some(U256::from(60u64)));
1052            let result = engine.check(&tx).unwrap();
1053            assert!(result.is_denied());
1054            if let PolicyResult::Denied { rule, .. } = result {
1055                assert_eq!(rule, "tx_limit");
1056            }
1057        }
1058
1059        #[test]
1060        fn test_full_rule_evaluation_order() {
1061            // Create a config with all rules
1062            let config = PolicyConfig::new()
1063                .with_whitelist(vec!["0xGOOD".to_string(), "0xALSO_GOOD".to_string()])
1064                .with_blacklist(vec!["0xBAD".to_string()])
1065                .with_transaction_limit("ETH", U256::from(100u64))
1066                .with_daily_limit("ETH", U256::from(200u64));
1067
1068            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1069            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1070
1071            // 1. Blacklisted address - denied by blacklist
1072            let tx = create_test_tx(Some("0xBAD"), Some(U256::from(50u64)));
1073            let result = engine.check(&tx).unwrap();
1074            if let PolicyResult::Denied { rule, .. } = result {
1075                assert_eq!(rule, "blacklist");
1076            }
1077
1078            // 2. Not whitelisted - denied by whitelist
1079            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(50u64)));
1080            let result = engine.check(&tx).unwrap();
1081            if let PolicyResult::Denied { rule, .. } = result {
1082                assert_eq!(rule, "whitelist");
1083            }
1084
1085            // 3. Whitelisted but over tx limit - denied by tx_limit
1086            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(150u64)));
1087            let result = engine.check(&tx).unwrap();
1088            if let PolicyResult::Denied { rule, .. } = result {
1089                assert_eq!(rule, "tx_limit");
1090            }
1091
1092            // 4. Record some transactions to affect daily limit
1093            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(80u64)));
1094            engine.record(&tx).unwrap();
1095            let tx = create_test_tx(Some("0xALSO_GOOD"), Some(U256::from(80u64)));
1096            engine.record(&tx).unwrap();
1097
1098            // 5. Within tx limit but over daily limit - denied by daily_limit
1099            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(50u64)));
1100            let result = engine.check(&tx).unwrap();
1101            if let PolicyResult::Denied { rule, .. } = result {
1102                assert_eq!(rule, "daily_limit");
1103            }
1104
1105            // 6. All checks pass - allowed
1106            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(10u64)));
1107            let result = engine.check(&tx).unwrap();
1108            assert!(result.is_allowed());
1109        }
1110    }
1111
1112    // =========================================================================
1113    // Record transaction tests
1114    // =========================================================================
1115
1116    mod record_tests {
1117        use super::*;
1118
1119        #[test]
1120        fn test_record_updates_history() {
1121            let config = PolicyConfig::new();
1122            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1123            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1124
1125            // Record a transaction
1126            let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
1127            engine.record(&tx).unwrap();
1128
1129            // Check that history was updated
1130            let total = history.daily_total("ETH").unwrap();
1131            assert_eq!(total, U256::from(100u64));
1132
1133            // Record another transaction
1134            let tx = create_test_tx(Some("0xREC"), Some(U256::from(50u64)));
1135            engine.record(&tx).unwrap();
1136
1137            let total = history.daily_total("ETH").unwrap();
1138            assert_eq!(total, U256::from(150u64));
1139        }
1140
1141        #[test]
1142        fn test_record_token_transaction() {
1143            let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
1144            let config = PolicyConfig::new();
1145            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1146            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1147
1148            // Record a token transaction
1149            let tx = create_token_tx(Some("0xREC"), Some(U256::from(1000u64)), token_address);
1150            engine.record(&tx).unwrap();
1151
1152            // Check that history was updated for the token
1153            let total = history.daily_total(token_address).unwrap();
1154            assert_eq!(total, U256::from(1000u64));
1155
1156            // ETH should still be zero
1157            let eth_total = history.daily_total("ETH").unwrap();
1158            assert_eq!(eth_total, U256::ZERO);
1159        }
1160
1161        #[test]
1162        fn test_record_with_no_amount() {
1163            let config = PolicyConfig::new();
1164            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1165            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1166
1167            // Record a transaction with no amount (defaults to zero)
1168            let tx = create_test_tx(Some("0xREC"), None);
1169            engine.record(&tx).unwrap();
1170
1171            // History should show zero
1172            let total = history.daily_total("ETH").unwrap();
1173            assert_eq!(total, U256::ZERO);
1174        }
1175    }
1176
1177    // =========================================================================
1178    // Send + Sync tests
1179    // =========================================================================
1180
1181    mod send_sync_tests {
1182        use super::*;
1183
1184        #[test]
1185        fn test_policy_engine_is_send_sync() {
1186            fn assert_send_sync<T: Send + Sync>() {}
1187            assert_send_sync::<DefaultPolicyEngine>();
1188        }
1189
1190        #[test]
1191        fn test_policy_check_result_is_send_sync() {
1192            fn assert_send_sync<T: Send + Sync>() {}
1193            assert_send_sync::<PolicyCheckResult>();
1194        }
1195
1196        #[test]
1197        fn test_engine_can_be_shared_across_threads() {
1198            use std::thread;
1199
1200            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(1000u64));
1201
1202            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1203            let engine = Arc::new(DefaultPolicyEngine::new(config, history).unwrap());
1204
1205            let mut handles = vec![];
1206
1207            for i in 0..5 {
1208                let engine_clone = Arc::clone(&engine);
1209                let handle = thread::spawn(move || {
1210                    let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64 * (i + 1))));
1211                    engine_clone.check(&tx)
1212                });
1213                handles.push(handle);
1214            }
1215
1216            for handle in handles {
1217                let result = handle.join().unwrap().unwrap();
1218                // All should be allowed since they're all under the limit
1219                assert!(result.is_allowed());
1220            }
1221        }
1222    }
1223
1224    // =========================================================================
1225    // Edge case tests
1226    // =========================================================================
1227
1228    mod edge_case_tests {
1229        use super::*;
1230
1231        #[test]
1232        fn test_empty_config_allows_everything() {
1233            let config = PolicyConfig::new();
1234            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1235            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1236
1237            let tx = create_test_tx(Some("0xANYONE"), Some(U256::MAX));
1238            let result = engine.check(&tx).unwrap();
1239            assert!(result.is_allowed());
1240        }
1241
1242        #[test]
1243        fn test_empty_transaction() {
1244            let config = PolicyConfig::new()
1245                .with_whitelist(vec!["0xGOOD".to_string()])
1246                .with_transaction_limit("ETH", U256::from(100u64));
1247
1248            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1249            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1250
1251            // Transaction with no recipient and no amount
1252            let tx = ParsedTx::default();
1253            let result = engine.check(&tx).unwrap();
1254
1255            // Should be allowed because:
1256            // - No recipient to blacklist/whitelist
1257            // - No amount to check against limits
1258            assert!(result.is_allowed());
1259        }
1260
1261        #[test]
1262        fn test_max_u256_amount() {
1263            let config = PolicyConfig::new()
1264                .with_transaction_limit("ETH", U256::MAX)
1265                .with_daily_limit("ETH", U256::MAX);
1266
1267            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1268            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1269
1270            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
1271            let result = engine.check(&tx).unwrap();
1272            assert!(result.is_allowed());
1273        }
1274
1275        #[test]
1276        fn test_daily_limit_with_overflow_protection() {
1277            let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1278
1279            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1280            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1281
1282            // Record a large transaction
1283            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
1284            engine.record(&tx).unwrap();
1285
1286            // Try another transaction - should use saturating_add and not overflow
1287            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
1288            let result = engine.check(&tx).unwrap();
1289
1290            // The new_total would saturate at MAX, which equals the limit, so it should be allowed
1291            assert!(result.is_allowed());
1292        }
1293
1294        #[test]
1295        fn test_daily_limit_exactly_at_limit_after_saturation() {
1296            let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1297
1298            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1299            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1300
1301            // Record U256::MAX - 50
1302            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX - U256::from(50u64)));
1303            engine.record(&tx).unwrap();
1304
1305            // Try to send 100 more - this will saturate at MAX and exceed limit
1306            let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
1307            let result = engine.check(&tx).unwrap();
1308
1309            // Should be denied because new_total (saturated at MAX) > MAX is false,
1310            // but MAX + 100 saturates to MAX which is not > MAX, so it's allowed
1311            assert!(result.is_allowed());
1312        }
1313
1314        #[test]
1315        fn test_engine_debug_format() {
1316            let config = PolicyConfig::new();
1317            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1318            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1319
1320            let debug_str = format!("{engine:?}");
1321            assert!(debug_str.contains("DefaultPolicyEngine"));
1322            assert!(debug_str.contains("config"));
1323            assert!(debug_str.contains("TransactionHistory"));
1324        }
1325
1326        #[test]
1327        fn test_check_result_equality() {
1328            let result1 = PolicyCheckResult::Allowed;
1329            let result2 = PolicyCheckResult::Allowed;
1330            assert_eq!(result1, result2);
1331
1332            let result3 = PolicyCheckResult::DeniedBlacklisted {
1333                address: "0xBAD".to_string(),
1334            };
1335            let result4 = PolicyCheckResult::DeniedBlacklisted {
1336                address: "0xBAD".to_string(),
1337            };
1338            assert_eq!(result3, result4);
1339        }
1340
1341        #[test]
1342        fn test_check_result_clone() {
1343            let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
1344                token: "ETH".to_string(),
1345                amount: U256::from(100u64),
1346                limit: U256::from(50u64),
1347            };
1348            let cloned = result.clone();
1349            assert_eq!(result, cloned);
1350        }
1351    }
1352
1353    // =========================================================================
1354    // Additional coverage tests for uncovered branches
1355    // =========================================================================
1356
1357    mod additional_coverage_tests {
1358        use super::*;
1359
1360        #[test]
1361        fn test_transaction_with_token_address_no_limit() {
1362            let config = PolicyConfig::new();
1363            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1364            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1365
1366            // Token transaction without any configured limits
1367            let tx = create_token_tx(
1368                Some("0xREC"),
1369                Some(U256::MAX),
1370                "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1371            );
1372            let result = engine.check(&tx).unwrap();
1373            assert!(result.is_allowed());
1374        }
1375
1376        #[test]
1377        fn test_record_transaction_with_token_address() {
1378            let config = PolicyConfig::new();
1379            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1380            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1381
1382            let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
1383            let tx = create_token_tx(Some("0xREC"), Some(U256::from(1000u64)), token_address);
1384
1385            engine.record(&tx).unwrap();
1386
1387            // Verify the token address was used as the key
1388            let total = history.daily_total(token_address).unwrap();
1389            assert_eq!(total, U256::from(1000u64));
1390        }
1391
1392        #[test]
1393        fn test_whitelist_disabled_explicitly() {
1394            // Test that whitelist is properly disabled even when addresses are present
1395            let config = PolicyConfig::new()
1396                .with_whitelist(vec!["0xGOOD".to_string()])
1397                .with_whitelist_enabled(false);
1398
1399            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1400            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1401
1402            // Non-whitelisted address should be allowed when whitelist is disabled
1403            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
1404            let result = engine.check(&tx).unwrap();
1405            assert!(result.is_allowed());
1406        }
1407
1408        #[test]
1409        fn test_policy_check_result_all_variants_have_rule_names() {
1410            // Verify all denied variants return a rule name
1411            let blacklisted = PolicyCheckResult::DeniedBlacklisted {
1412                address: "0x1".to_string(),
1413            };
1414            assert_eq!(blacklisted.rule_name(), Some("blacklist"));
1415
1416            let not_whitelisted = PolicyCheckResult::DeniedNotWhitelisted {
1417                address: "0x2".to_string(),
1418            };
1419            assert_eq!(not_whitelisted.rule_name(), Some("whitelist"));
1420
1421            let tx_limit = PolicyCheckResult::DeniedExceedsTransactionLimit {
1422                token: "ETH".to_string(),
1423                amount: U256::from(10u64),
1424                limit: U256::from(5u64),
1425            };
1426            assert_eq!(tx_limit.rule_name(), Some("tx_limit"));
1427
1428            let daily_limit = PolicyCheckResult::DeniedExceedsDailyLimit {
1429                token: "ETH".to_string(),
1430                amount: U256::from(5u64),
1431                daily_total: U256::from(8u64),
1432                limit: U256::from(10u64),
1433            };
1434            assert_eq!(daily_limit.rule_name(), Some("daily_limit"));
1435        }
1436
1437        #[test]
1438        fn test_conversion_edge_case_unknown_rule() {
1439            // This tests the unwrap_or fallback in the conversion
1440            // Though in practice this should never happen with current variants
1441            let allowed = PolicyCheckResult::Allowed;
1442            let policy_result: PolicyResult = allowed.into();
1443            assert!(policy_result.is_allowed());
1444        }
1445
1446        // =========================================================================
1447        // Daily Limit Boundary Tests
1448        // =========================================================================
1449
1450        #[test]
1451        fn test_daily_limit_amount_exactly_equal_to_limit() {
1452            // Arrange: Configure daily limit
1453            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1454            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1455            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1456
1457            // Act: Try to send exactly the limit amount
1458            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1000u64)));
1459            let result = engine.check(&tx).unwrap();
1460
1461            // Assert: Should be allowed (not exceeding)
1462            assert!(result.is_allowed());
1463        }
1464
1465        #[test]
1466        fn test_daily_limit_boundary_total_plus_amount_equals_limit() {
1467            // Arrange: Configure daily limit and record some transactions
1468            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1469            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1470            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1471
1472            // Record transactions totaling 600
1473            let tx1 = create_test_tx(Some("0xREC"), Some(U256::from(300u64)));
1474            engine.record(&tx1).unwrap();
1475            let tx2 = create_test_tx(Some("0xREC"), Some(U256::from(300u64)));
1476            engine.record(&tx2).unwrap();
1477
1478            // Act: Try to send exactly the remaining amount (400)
1479            let tx3 = create_test_tx(Some("0xREC"), Some(U256::from(400u64)));
1480            let result = engine.check(&tx3).unwrap();
1481
1482            // Assert: Should be allowed (total = 1000, exactly at limit)
1483            assert!(result.is_allowed());
1484        }
1485
1486        #[test]
1487        fn test_daily_limit_boundary_one_over_limit() {
1488            // Arrange: Configure daily limit
1489            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1490            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1491            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1492
1493            // Act: Try to send one unit over the limit
1494            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1001u64)));
1495            let result = engine.check(&tx).unwrap();
1496
1497            // Assert: Should be denied
1498            assert!(result.is_denied());
1499            if let PolicyResult::Denied { rule, .. } = result {
1500                assert_eq!(rule, "daily_limit");
1501            }
1502        }
1503
1504        #[test]
1505        fn test_daily_limit_u256_max_amount() {
1506            // Arrange: Configure daily limit to U256::MAX
1507            let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1508            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1509            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1510
1511            // Act: Try to send U256::MAX
1512            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
1513            let result = engine.check(&tx).unwrap();
1514
1515            // Assert: Should be allowed
1516            assert!(result.is_allowed());
1517        }
1518
1519        #[test]
1520        fn test_daily_limit_overflow_protection_with_saturating_add() {
1521            // Arrange: Configure daily limit to U256::MAX
1522            let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1523            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1524            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1525
1526            // Record a very large transaction
1527            let large_tx = create_test_tx(Some("0xREC"), Some(U256::MAX - U256::from(10u64)));
1528            engine.record(&large_tx).unwrap();
1529
1530            // Act: Try to add more (this would overflow without saturating_add)
1531            let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
1532            let result = engine.check(&tx).unwrap();
1533
1534            // Assert: The saturating_add in check_daily_limit should prevent overflow
1535            // new_total = (U256::MAX - 10) + 100 = saturates to U256::MAX
1536            // U256::MAX > U256::MAX is false, so it should be allowed
1537            assert!(result.is_allowed());
1538        }
1539
1540        #[test]
1541        fn test_daily_limit_with_u256_max_minus_one() {
1542            // Arrange: Configure limit to U256::MAX and record MAX - 1
1543            let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1544            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1545            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1546
1547            // Record U256::MAX - 1
1548            let tx1 = create_test_tx(Some("0xREC"), Some(U256::MAX - U256::from(1u64)));
1549            engine.record(&tx1).unwrap();
1550
1551            // Act: Try to send 1 more (total would be exactly MAX)
1552            let tx2 = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
1553            let result = engine.check(&tx2).unwrap();
1554
1555            // Assert: Should be allowed (total = MAX, not > MAX)
1556            assert!(result.is_allowed());
1557        }
1558
1559        #[test]
1560        fn test_daily_limit_accumulation_near_limit() {
1561            // Arrange: Configure daily limit
1562            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1563            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1564            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1565
1566            // Record transactions approaching the limit
1567            engine
1568                .record(&create_test_tx(Some("0xREC"), Some(U256::from(250u64))))
1569                .unwrap();
1570            engine
1571                .record(&create_test_tx(Some("0xREC"), Some(U256::from(250u64))))
1572                .unwrap();
1573            engine
1574                .record(&create_test_tx(Some("0xREC"), Some(U256::from(250u64))))
1575                .unwrap();
1576            engine
1577                .record(&create_test_tx(Some("0xREC"), Some(U256::from(249u64))))
1578                .unwrap();
1579
1580            // Total is now 999
1581
1582            // Act: Try to send 1 more (total = 1000)
1583            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
1584            let result = engine.check(&tx).unwrap();
1585
1586            // Assert: Should be allowed
1587            assert!(result.is_allowed());
1588
1589            // Act: Try to send 2 more (total = 1001)
1590            let tx2 = create_test_tx(Some("0xREC"), Some(U256::from(2u64)));
1591            let result2 = engine.check(&tx2).unwrap();
1592
1593            // Assert: Should be denied
1594            assert!(result2.is_denied());
1595        }
1596
1597        #[test]
1598        fn test_daily_limit_zero_amount_with_existing_total() {
1599            // Arrange: Configure daily limit and record some transactions
1600            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1601            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1602            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1603
1604            // Record transactions at the limit
1605            engine
1606                .record(&create_test_tx(Some("0xREC"), Some(U256::from(1000u64))))
1607                .unwrap();
1608
1609            // Act: Try to send zero amount
1610            let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
1611            let result = engine.check(&tx).unwrap();
1612
1613            // Assert: Should be allowed (not exceeding)
1614            assert!(result.is_allowed());
1615        }
1616
1617        #[test]
1618        fn test_daily_limit_multiple_tokens_independent() {
1619            // Arrange: Configure limits for multiple tokens
1620            let config = PolicyConfig::new()
1621                .with_daily_limit("ETH", U256::from(1000u64))
1622                .with_daily_limit(
1623                    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1624                    U256::from(5000u64),
1625                );
1626
1627            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1628            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1629
1630            // Act: Record ETH at limit
1631            engine
1632                .record(&create_test_tx(Some("0xREC"), Some(U256::from(1000u64))))
1633                .unwrap();
1634
1635            // Token should still have full limit available
1636            let token_tx = create_token_tx(
1637                Some("0xREC"),
1638                Some(U256::from(5000u64)),
1639                "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1640            );
1641            let result = engine.check(&token_tx).unwrap();
1642
1643            // Assert: Token transaction should be allowed
1644            assert!(result.is_allowed());
1645        }
1646
1647        #[test]
1648        fn test_daily_limit_reason_message_includes_values() {
1649            // Arrange: Configure daily limit and approach it
1650            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1651            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1652            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1653
1654            // Record transaction
1655            engine
1656                .record(&create_test_tx(Some("0xREC"), Some(U256::from(900u64))))
1657                .unwrap();
1658
1659            // Act: Try to exceed limit
1660            let tx = create_test_tx(Some("0xREC"), Some(U256::from(200u64)));
1661            let result = engine.check(&tx).unwrap();
1662
1663            // Assert: Should be denied with descriptive reason
1664            assert!(result.is_denied());
1665            if let PolicyResult::Denied { reason, .. } = result {
1666                assert!(reason.contains("200")); // amount
1667                assert!(reason.contains("900")); // daily_total
1668                assert!(reason.contains("1000")); // limit
1669                assert!(reason.contains("ETH"));
1670            }
1671        }
1672
1673        // =====================================================================
1674        // Phase 2: PolicyCheckResult Denial Reason Messages
1675        // =====================================================================
1676
1677        #[test]
1678        fn should_generate_blacklisted_denial_reason_with_address() {
1679            // Arrange: Create blacklisted denial result
1680            let result = PolicyCheckResult::DeniedBlacklisted {
1681                address: "0xBADDEADBEEF123456789".to_string(),
1682            };
1683
1684            // Act: Get the reason message
1685            let reason = result.reason();
1686
1687            // Assert: Reason contains the address and clear explanation
1688            assert!(reason.is_some());
1689            let reason_str = reason.unwrap();
1690            assert!(
1691                reason_str.contains("0xBADDEADBEEF123456789"),
1692                "Reason should include the blacklisted address"
1693            );
1694            assert!(
1695                reason_str.contains("blacklisted"),
1696                "Reason should mention blacklisting"
1697            );
1698            assert!(
1699                reason_str.contains("recipient"),
1700                "Reason should mention recipient"
1701            );
1702        }
1703
1704        #[test]
1705        fn should_generate_not_whitelisted_denial_reason_with_address() {
1706            // Arrange: Create not-whitelisted denial result
1707            let result = PolicyCheckResult::DeniedNotWhitelisted {
1708                address: "0x1234567890ABCDEF".to_string(),
1709            };
1710
1711            // Act: Get the reason message
1712            let reason = result.reason();
1713
1714            // Assert: Reason contains the address and clear explanation
1715            assert!(reason.is_some());
1716            let reason_str = reason.unwrap();
1717            assert!(
1718                reason_str.contains("0x1234567890ABCDEF"),
1719                "Reason should include the non-whitelisted address"
1720            );
1721            assert!(
1722                reason_str.contains("not in whitelist"),
1723                "Reason should mention whitelist rejection"
1724            );
1725        }
1726
1727        #[test]
1728        fn should_generate_transaction_limit_denial_reason_with_amounts() {
1729            // Arrange: Create transaction limit denial with specific amounts
1730            let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
1731                token: "USDC".to_string(),
1732                amount: U256::from(5_000_000u64),
1733                limit: U256::from(1_000_000u64),
1734            };
1735
1736            // Act: Get the reason message
1737            let reason = result.reason();
1738
1739            // Assert: Reason contains all relevant amounts and token
1740            assert!(reason.is_some());
1741            let reason_str = reason.unwrap();
1742            assert!(
1743                reason_str.contains("5000000"),
1744                "Reason should include the attempted amount"
1745            );
1746            assert!(
1747                reason_str.contains("1000000"),
1748                "Reason should include the limit"
1749            );
1750            assert!(reason_str.contains("USDC"), "Reason should include token");
1751            assert!(
1752                reason_str.contains("exceeds transaction limit"),
1753                "Reason should explain the violation"
1754            );
1755        }
1756
1757        #[test]
1758        fn should_generate_daily_limit_denial_reason_with_all_values() {
1759            // Arrange: Create daily limit denial with all values
1760            let result = PolicyCheckResult::DeniedExceedsDailyLimit {
1761                token: "ETH".to_string(),
1762                amount: U256::from(3_000_000_000_000_000_000u64), // 3 ETH
1763                daily_total: U256::from(8_000_000_000_000_000_000u64), // 8 ETH
1764                limit: U256::from(10_000_000_000_000_000_000u64), // 10 ETH limit
1765            };
1766
1767            // Act: Get the reason message
1768            let reason = result.reason();
1769
1770            // Assert: Reason contains amount, daily_total, limit, and token
1771            assert!(reason.is_some());
1772            let reason_str = reason.unwrap();
1773            assert!(
1774                reason_str.contains("3000000000000000000"),
1775                "Reason should include the requested amount"
1776            );
1777            assert!(
1778                reason_str.contains("8000000000000000000"),
1779                "Reason should include the daily total"
1780            );
1781            assert!(
1782                reason_str.contains("10000000000000000000"),
1783                "Reason should include the limit"
1784            );
1785            assert!(reason_str.contains("ETH"), "Reason should include token");
1786            assert!(
1787                reason_str.contains("exceeds daily limit"),
1788                "Reason should explain the violation"
1789            );
1790        }
1791
1792        // =====================================================================
1793        // Phase 2: None Recipient Handling in Policy Checks
1794        // =====================================================================
1795
1796        #[test]
1797        fn should_allow_transaction_with_none_recipient_when_no_whitelist() {
1798            // Arrange: Policy with no whitelist (allow all)
1799            let config = PolicyConfig::new();
1800            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1801            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1802
1803            // Act: Check transaction with None recipient (contract creation)
1804            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1805            let result = engine.check(&tx).unwrap();
1806
1807            // Assert: Should be allowed (no recipient to check)
1808            assert!(
1809                result.is_allowed(),
1810                "Transaction with None recipient should be allowed when no whitelist"
1811            );
1812        }
1813
1814        #[test]
1815        fn should_allow_none_recipient_when_whitelist_enabled() {
1816            // Arrange: Policy with whitelist enabled
1817            // Note: Whitelist/blacklist checks only apply when there IS a recipient
1818            let config = PolicyConfig::new().with_whitelist(vec!["0xALLOWED".to_string()]);
1819            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1820            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1821
1822            // Act: Check transaction with None recipient (e.g., contract creation)
1823            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1824            let result = engine.check(&tx).unwrap();
1825
1826            // Assert: Should be allowed - whitelist check is skipped for None recipient
1827            // This is correct behavior: you can't whitelist/blacklist a non-existent address
1828            assert!(
1829                result.is_allowed(),
1830                "None recipient should pass whitelist check (check is skipped)"
1831            );
1832        }
1833
1834        #[test]
1835        fn should_allow_none_recipient_when_not_blacklisted() {
1836            // Arrange: Policy with only blacklist (None can't be blacklisted)
1837            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
1838            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1839            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1840
1841            // Act: Check transaction with None recipient
1842            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1843            let result = engine.check(&tx).unwrap();
1844
1845            // Assert: Should be allowed (None recipient can't be blacklisted)
1846            assert!(
1847                result.is_allowed(),
1848                "None recipient should pass blacklist check"
1849            );
1850        }
1851
1852        #[test]
1853        fn should_enforce_amount_limits_on_none_recipient_transactions() {
1854            // Arrange: Policy with transaction limit but no address filters
1855            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(500u64));
1856            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1857            let engine = DefaultPolicyEngine::new(config, history).unwrap();
1858
1859            // Act: Check transaction with None recipient but exceeds limit
1860            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1861            let result = engine.check(&tx).unwrap();
1862
1863            // Assert: Should be denied (amount limit applies regardless of recipient)
1864            assert!(
1865                result.is_denied(),
1866                "Amount limits should apply to None recipient transactions"
1867            );
1868            if let PolicyResult::Denied { rule, .. } = result {
1869                assert_eq!(rule, "tx_limit");
1870            }
1871        }
1872
1873        #[test]
1874        fn should_enforce_daily_limits_on_none_recipient_transactions() {
1875            // Arrange: Policy with daily limit
1876            let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1877            let history = Arc::new(TransactionHistory::in_memory().unwrap());
1878            let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1879
1880            // Record a prior transaction
1881            engine
1882                .record(&create_test_tx(Some("0xSOME"), Some(U256::from(800u64))))
1883                .unwrap();
1884
1885            // Act: Check transaction with None recipient that would exceed daily limit
1886            let tx = create_test_tx(None, Some(U256::from(300u64)));
1887            let result = engine.check(&tx).unwrap();
1888
1889            // Assert: Should be denied (daily limit applies regardless of recipient)
1890            assert!(
1891                result.is_denied(),
1892                "Daily limits should apply to None recipient transactions"
1893            );
1894            if let PolicyResult::Denied { rule, .. } = result {
1895                assert_eq!(rule, "daily_limit");
1896            }
1897        }
1898    }
1899}