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. **Allow** - If all checks pass, the transaction is ALLOWED.
14//!
15//! # Thread Safety
16//!
17//! The [`DefaultPolicyEngine`] is `Send + Sync` and can be safely shared across threads.
18//!
19//! # Example
20//!
21//! ```no_run
22//! use txgate_policy::engine::{PolicyEngine, DefaultPolicyEngine};
23//! use txgate_policy::config::PolicyConfig;
24//! use txgate_core::types::{ParsedTx, PolicyResult};
25//! use alloy_primitives::U256;
26//!
27//! // Create a policy config
28//! let config = PolicyConfig::new()
29//!     .with_blacklist(vec!["0xBAD".to_string()])
30//!     .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64));
31//!
32//! // Create the engine
33//! let engine = DefaultPolicyEngine::new(config).unwrap();
34//!
35//! // Check a transaction
36//! let tx = ParsedTx::default();
37//! let result = engine.check(&tx);
38//! ```
39
40use crate::config::PolicyConfig;
41use alloy_primitives::U256;
42use txgate_core::error::PolicyError;
43use txgate_core::types::{ParsedTx, PolicyResult};
44
45/// Trait for policy engines that enforce transaction rules.
46///
47/// Implementors of this trait can check transactions against policy rules.
48///
49/// # Thread Safety
50///
51/// All implementations must be `Send + Sync` to allow concurrent access
52/// from multiple request handlers.
53///
54/// # Example
55///
56/// ```no_run
57/// use txgate_policy::engine::PolicyEngine;
58/// use txgate_core::types::{ParsedTx, PolicyResult};
59/// use txgate_core::error::PolicyError;
60///
61/// fn process_transaction(engine: &dyn PolicyEngine, tx: &ParsedTx) -> Result<(), PolicyError> {
62///     let result = engine.check(tx)?;
63///     if result.is_allowed() {
64///         // Transaction approved
65///     }
66///     Ok(())
67/// }
68/// ```
69pub trait PolicyEngine: Send + Sync {
70    /// Check if a transaction is allowed by policy rules.
71    ///
72    /// Evaluates the transaction against all configured policy rules in order
73    /// of priority (blacklist > whitelist > `tx_limit`).
74    ///
75    /// # Arguments
76    ///
77    /// * `tx` - The parsed transaction to check
78    ///
79    /// # Returns
80    ///
81    /// * `Ok(PolicyResult::Allowed)` - Transaction passes all policy checks
82    /// * `Ok(PolicyResult::Denied { rule, reason })` - Transaction denied by a rule
83    /// * `Err(PolicyError)` - Policy evaluation failed (e.g., database error)
84    ///
85    /// # Errors
86    ///
87    /// Returns [`PolicyError`] if policy evaluation fails due to:
88    /// - Invalid policy configuration
89    fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError>;
90}
91
92/// Detailed result of a policy check operation.
93///
94/// Provides specific information about why a transaction was allowed or denied,
95/// enabling detailed error messages and audit logging.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum PolicyCheckResult {
98    /// Transaction is allowed by all policy rules.
99    Allowed,
100
101    /// Transaction denied - recipient address is blacklisted.
102    DeniedBlacklisted {
103        /// The blacklisted address.
104        address: String,
105    },
106
107    /// Transaction denied - recipient address is not in the whitelist.
108    DeniedNotWhitelisted {
109        /// The address that is not whitelisted.
110        address: String,
111    },
112
113    /// Transaction denied - amount exceeds single transaction limit.
114    DeniedExceedsTransactionLimit {
115        /// Token identifier (e.g., "ETH" or contract address).
116        token: String,
117        /// The requested transaction amount.
118        amount: U256,
119        /// The configured limit.
120        limit: U256,
121    },
122}
123
124impl PolicyCheckResult {
125    /// Returns `true` if the transaction is allowed.
126    #[must_use]
127    pub const fn is_allowed(&self) -> bool {
128        matches!(self, Self::Allowed)
129    }
130
131    /// Returns `true` if the transaction is denied.
132    #[must_use]
133    pub const fn is_denied(&self) -> bool {
134        !self.is_allowed()
135    }
136
137    /// Returns the rule name that caused the denial, if any.
138    #[must_use]
139    pub const fn rule_name(&self) -> Option<&'static str> {
140        match self {
141            Self::Allowed => None,
142            Self::DeniedBlacklisted { .. } => Some("blacklist"),
143            Self::DeniedNotWhitelisted { .. } => Some("whitelist"),
144            Self::DeniedExceedsTransactionLimit { .. } => Some("tx_limit"),
145        }
146    }
147
148    /// Returns a human-readable reason for the denial, if any.
149    #[must_use]
150    pub fn reason(&self) -> Option<String> {
151        match self {
152            Self::Allowed => None,
153            Self::DeniedBlacklisted { address } => {
154                Some(format!("recipient address is blacklisted: {address}"))
155            }
156            Self::DeniedNotWhitelisted { address } => {
157                Some(format!("recipient address not in whitelist: {address}"))
158            }
159            Self::DeniedExceedsTransactionLimit {
160                token,
161                amount,
162                limit,
163            } => Some(format!(
164                "amount {amount} exceeds transaction limit {limit} for {token}"
165            )),
166        }
167    }
168}
169
170impl From<PolicyCheckResult> for PolicyResult {
171    fn from(result: PolicyCheckResult) -> Self {
172        if result == PolicyCheckResult::Allowed {
173            Self::Allowed
174        } else {
175            let rule = result.rule_name().unwrap_or("unknown").to_string();
176            let reason = result
177                .reason()
178                .unwrap_or_else(|| "policy denied".to_string());
179            Self::Denied { rule, reason }
180        }
181    }
182}
183
184/// Default policy engine implementation.
185///
186/// Evaluates transactions against configured whitelist, blacklist, and limit rules.
187///
188/// # Rule Evaluation Order
189///
190/// 1. Blacklist check (highest priority)
191/// 2. Whitelist check (if enabled)
192/// 3. Transaction limit check
193/// 4. Allow (if all checks pass)
194///
195/// # Thread Safety
196///
197/// This struct is `Send + Sync` and can be safely shared across threads.
198pub struct DefaultPolicyEngine {
199    /// Policy configuration with whitelist, blacklist, and limits.
200    config: PolicyConfig,
201}
202
203impl std::fmt::Debug for DefaultPolicyEngine {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        f.debug_struct("DefaultPolicyEngine")
206            .field("config", &self.config)
207            .finish()
208    }
209}
210
211impl DefaultPolicyEngine {
212    /// Creates a new policy engine with the given configuration.
213    ///
214    /// # Arguments
215    ///
216    /// * `config` - Policy configuration with whitelist, blacklist, and limits
217    ///
218    /// # Returns
219    ///
220    /// A new `DefaultPolicyEngine` instance.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`PolicyError::InvalidConfiguration`] if the configuration is invalid
225    /// (e.g., an address appears in both whitelist and blacklist).
226    ///
227    /// # Example
228    ///
229    /// ```no_run
230    /// use txgate_policy::engine::DefaultPolicyEngine;
231    /// use txgate_policy::config::PolicyConfig;
232    /// use alloy_primitives::U256;
233    ///
234    /// let config = PolicyConfig::new()
235    ///     .with_whitelist(vec!["0xAAA".to_string()])
236    ///     .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64));
237    ///
238    /// let engine = DefaultPolicyEngine::new(config).unwrap();
239    /// ```
240    pub fn new(config: PolicyConfig) -> Result<Self, PolicyError> {
241        // Validate configuration
242        config.validate()?;
243
244        Ok(Self { config })
245    }
246
247    /// Check recipient against blacklist.
248    ///
249    /// # Returns
250    ///
251    /// * `Some(DeniedBlacklisted)` - If the recipient is blacklisted
252    /// * `None` - If the recipient is not blacklisted or has no recipient
253    fn check_blacklist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
254        let recipient = tx.recipient.as_ref()?;
255
256        if self.config.is_blacklisted(recipient) {
257            return Some(PolicyCheckResult::DeniedBlacklisted {
258                address: recipient.clone(),
259            });
260        }
261
262        None
263    }
264
265    /// Check recipient against whitelist (if enabled).
266    ///
267    /// # Returns
268    ///
269    /// * `Some(DeniedNotWhitelisted)` - If whitelist is enabled and recipient is not whitelisted
270    /// * `None` - If whitelist is disabled, recipient is whitelisted, or transaction has no recipient
271    fn check_whitelist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
272        // Whitelist only applies if enabled
273        if !self.config.whitelist_enabled {
274            return None;
275        }
276
277        let recipient = tx.recipient.as_ref()?;
278
279        if !self.config.is_whitelisted(recipient) {
280            return Some(PolicyCheckResult::DeniedNotWhitelisted {
281                address: recipient.clone(),
282            });
283        }
284
285        None
286    }
287
288    /// Check transaction amount against per-transaction limit.
289    ///
290    /// # Returns
291    ///
292    /// * `Some(DeniedExceedsTransactionLimit)` - If amount exceeds the configured limit
293    /// * `None` - If no limit is configured, amount is within limit, or transaction has no amount
294    fn check_transaction_limit(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
295        let amount = tx.amount?;
296
297        // Determine token key: use token_address if present, otherwise "ETH"
298        let token = tx.token_address.as_deref().unwrap_or("ETH");
299
300        // Get the configured limit for this token
301        let limit = self.config.get_transaction_limit(token)?;
302
303        // Check if amount exceeds limit
304        if amount > limit {
305            return Some(PolicyCheckResult::DeniedExceedsTransactionLimit {
306                token: token.to_string(),
307                amount,
308                limit,
309            });
310        }
311
312        None
313    }
314}
315
316impl PolicyEngine for DefaultPolicyEngine {
317    fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError> {
318        // 1. Check blacklist (highest priority)
319        if let Some(result) = self.check_blacklist(tx) {
320            return Ok(result.into());
321        }
322
323        // 2. Check whitelist (if enabled)
324        if let Some(result) = self.check_whitelist(tx) {
325            return Ok(result.into());
326        }
327
328        // 3. Check transaction limit
329        if let Some(result) = self.check_transaction_limit(tx) {
330            return Ok(result.into());
331        }
332
333        // 4. All checks passed
334        Ok(PolicyResult::Allowed)
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    #![allow(
341        clippy::expect_used,
342        clippy::unwrap_used,
343        clippy::panic,
344        clippy::indexing_slicing,
345        clippy::similar_names,
346        clippy::redundant_clone,
347        clippy::manual_string_new,
348        clippy::needless_raw_string_hashes,
349        clippy::needless_collect,
350        clippy::unreadable_literal
351    )]
352
353    use super::*;
354    use std::collections::HashMap;
355    use std::sync::Arc;
356    use txgate_core::types::TxType;
357
358    /// Helper to create a basic test transaction.
359    fn create_test_tx(recipient: Option<&str>, amount: Option<U256>) -> ParsedTx {
360        ParsedTx {
361            hash: [0xab; 32],
362            recipient: recipient.map(String::from),
363            amount,
364            token: Some("ETH".to_string()),
365            token_address: None,
366            tx_type: TxType::Transfer,
367            chain: "ethereum".to_string(),
368            nonce: Some(1),
369            chain_id: Some(1),
370            metadata: HashMap::new(),
371        }
372    }
373
374    /// Helper to create a token transfer transaction.
375    fn create_token_tx(
376        recipient: Option<&str>,
377        amount: Option<U256>,
378        token_address: &str,
379    ) -> ParsedTx {
380        ParsedTx {
381            hash: [0xcd; 32],
382            recipient: recipient.map(String::from),
383            amount,
384            token: Some("USDC".to_string()),
385            token_address: Some(token_address.to_string()),
386            tx_type: TxType::TokenTransfer,
387            chain: "ethereum".to_string(),
388            nonce: Some(2),
389            chain_id: Some(1),
390            metadata: HashMap::new(),
391        }
392    }
393
394    // =========================================================================
395    // PolicyCheckResult tests
396    // =========================================================================
397
398    mod policy_check_result_tests {
399        use super::*;
400
401        #[test]
402        fn test_allowed_is_allowed() {
403            let result = PolicyCheckResult::Allowed;
404            assert!(result.is_allowed());
405            assert!(!result.is_denied());
406            assert!(result.rule_name().is_none());
407            assert!(result.reason().is_none());
408        }
409
410        #[test]
411        fn test_denied_blacklisted() {
412            let result = PolicyCheckResult::DeniedBlacklisted {
413                address: "0xBAD".to_string(),
414            };
415            assert!(!result.is_allowed());
416            assert!(result.is_denied());
417            assert_eq!(result.rule_name(), Some("blacklist"));
418            assert!(result.reason().unwrap().contains("blacklisted"));
419        }
420
421        #[test]
422        fn test_denied_not_whitelisted() {
423            let result = PolicyCheckResult::DeniedNotWhitelisted {
424                address: "0xUNKNOWN".to_string(),
425            };
426            assert!(!result.is_allowed());
427            assert!(result.is_denied());
428            assert_eq!(result.rule_name(), Some("whitelist"));
429            assert!(result.reason().unwrap().contains("not in whitelist"));
430        }
431
432        #[test]
433        fn test_denied_exceeds_transaction_limit() {
434            let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
435                token: "ETH".to_string(),
436                amount: U256::from(10u64),
437                limit: U256::from(5u64),
438            };
439            assert!(!result.is_allowed());
440            assert!(result.is_denied());
441            assert_eq!(result.rule_name(), Some("tx_limit"));
442            assert!(result
443                .reason()
444                .unwrap()
445                .contains("exceeds transaction limit"));
446        }
447
448        #[test]
449        fn test_conversion_to_policy_result_allowed() {
450            let check_result = PolicyCheckResult::Allowed;
451            let policy_result: PolicyResult = check_result.into();
452            assert!(policy_result.is_allowed());
453        }
454
455        #[test]
456        fn test_conversion_to_policy_result_denied() {
457            let check_result = PolicyCheckResult::DeniedBlacklisted {
458                address: "0xBAD".to_string(),
459            };
460            let policy_result: PolicyResult = check_result.into();
461            assert!(policy_result.is_denied());
462
463            if let PolicyResult::Denied { rule, reason } = policy_result {
464                assert_eq!(rule, "blacklist");
465                assert!(reason.contains("blacklisted"));
466            } else {
467                panic!("expected Denied variant");
468            }
469        }
470    }
471
472    // =========================================================================
473    // DefaultPolicyEngine creation tests
474    // =========================================================================
475
476    mod engine_creation_tests {
477        use super::*;
478
479        #[test]
480        fn test_create_engine_with_valid_config() {
481            let config = PolicyConfig::new()
482                .with_whitelist(vec!["0xAAA".to_string()])
483                .with_blacklist(vec!["0xBBB".to_string()]);
484
485            let engine = DefaultPolicyEngine::new(config);
486
487            assert!(engine.is_ok());
488        }
489
490        #[test]
491        fn test_create_engine_with_invalid_config() {
492            // Address in both whitelist and blacklist
493            let config = PolicyConfig::new()
494                .with_whitelist(vec!["0xAAA".to_string()])
495                .with_blacklist(vec!["0xAAA".to_string()]);
496
497            let engine = DefaultPolicyEngine::new(config);
498
499            assert!(engine.is_err());
500            let err = engine.unwrap_err();
501            assert!(matches!(err, PolicyError::InvalidConfiguration { .. }));
502        }
503
504        #[test]
505        fn test_create_engine_with_empty_config() {
506            let config = PolicyConfig::new();
507            let engine = DefaultPolicyEngine::new(config);
508
509            assert!(engine.is_ok());
510        }
511    }
512
513    // =========================================================================
514    // Blacklist tests
515    // =========================================================================
516
517    mod blacklist_tests {
518        use super::*;
519
520        #[test]
521        fn test_blacklist_blocks_transaction() {
522            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
523
524            let engine = DefaultPolicyEngine::new(config).unwrap();
525
526            let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
527            let result = engine.check(&tx).unwrap();
528
529            assert!(result.is_denied());
530            if let PolicyResult::Denied { rule, .. } = result {
531                assert_eq!(rule, "blacklist");
532            } else {
533                panic!("expected Denied variant");
534            }
535        }
536
537        #[test]
538        fn test_blacklist_case_insensitive() {
539            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
540
541            let engine = DefaultPolicyEngine::new(config).unwrap();
542
543            let tx = create_test_tx(Some("0xbad"), Some(U256::from(100u64)));
544            let result = engine.check(&tx).unwrap();
545
546            assert!(result.is_denied());
547        }
548
549        #[test]
550        fn test_non_blacklisted_address_allowed() {
551            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
552
553            let engine = DefaultPolicyEngine::new(config).unwrap();
554
555            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
556            let result = engine.check(&tx).unwrap();
557
558            assert!(result.is_allowed());
559        }
560
561        #[test]
562        fn test_no_recipient_skips_blacklist_check() {
563            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
564
565            let engine = DefaultPolicyEngine::new(config).unwrap();
566
567            let tx = create_test_tx(None, Some(U256::from(100u64)));
568            let result = engine.check(&tx).unwrap();
569
570            // Should pass since there's no recipient to check
571            assert!(result.is_allowed());
572        }
573    }
574
575    // =========================================================================
576    // Whitelist tests
577    // =========================================================================
578
579    mod whitelist_tests {
580        use super::*;
581
582        #[test]
583        fn test_whitelist_allows_only_whitelisted_when_enabled() {
584            let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
585
586            let engine = DefaultPolicyEngine::new(config).unwrap();
587
588            // Whitelisted address should be allowed
589            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
590            let result = engine.check(&tx).unwrap();
591            assert!(result.is_allowed());
592
593            // Non-whitelisted address should be denied
594            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
595            let result = engine.check(&tx).unwrap();
596            assert!(result.is_denied());
597            if let PolicyResult::Denied { rule, .. } = result {
598                assert_eq!(rule, "whitelist");
599            }
600        }
601
602        #[test]
603        fn test_whitelist_disabled_allows_all() {
604            let config = PolicyConfig::new()
605                .with_whitelist(vec!["0xGOOD".to_string()])
606                .with_whitelist_enabled(false);
607
608            let engine = DefaultPolicyEngine::new(config).unwrap();
609
610            // Any address should be allowed when whitelist is disabled
611            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
612            let result = engine.check(&tx).unwrap();
613            assert!(result.is_allowed());
614        }
615
616        #[test]
617        fn test_whitelist_case_insensitive() {
618            let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
619
620            let engine = DefaultPolicyEngine::new(config).unwrap();
621
622            let tx = create_test_tx(Some("0xgood"), Some(U256::from(100u64)));
623            let result = engine.check(&tx).unwrap();
624            assert!(result.is_allowed());
625        }
626
627        #[test]
628        fn test_no_recipient_skips_whitelist_check() {
629            let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
630
631            let engine = DefaultPolicyEngine::new(config).unwrap();
632
633            let tx = create_test_tx(None, Some(U256::from(100u64)));
634            let result = engine.check(&tx).unwrap();
635
636            // Should pass since there's no recipient to check
637            assert!(result.is_allowed());
638        }
639    }
640
641    // =========================================================================
642    // Transaction limit tests
643    // =========================================================================
644
645    mod transaction_limit_tests {
646        use super::*;
647
648        #[test]
649        fn test_transaction_limit_enforcement() {
650            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
651
652            let engine = DefaultPolicyEngine::new(config).unwrap();
653
654            // Within limit - should be allowed
655            let tx = create_test_tx(Some("0xREC"), Some(U256::from(50u64)));
656            let result = engine.check(&tx).unwrap();
657            assert!(result.is_allowed());
658
659            // At limit - should be allowed
660            let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
661            let result = engine.check(&tx).unwrap();
662            assert!(result.is_allowed());
663
664            // Above limit - should be denied
665            let tx = create_test_tx(Some("0xREC"), Some(U256::from(101u64)));
666            let result = engine.check(&tx).unwrap();
667            assert!(result.is_denied());
668            if let PolicyResult::Denied { rule, .. } = result {
669                assert_eq!(rule, "tx_limit");
670            }
671        }
672
673        #[test]
674        fn test_transaction_limit_for_token() {
675            let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
676            let config =
677                PolicyConfig::new().with_transaction_limit(token_address, U256::from(1000u64));
678
679            let engine = DefaultPolicyEngine::new(config).unwrap();
680
681            // Above limit for token - should be denied
682            let tx = create_token_tx(Some("0xREC"), Some(U256::from(1001u64)), token_address);
683            let result = engine.check(&tx).unwrap();
684            assert!(result.is_denied());
685
686            // ETH transfer should not be affected by token limit
687            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1001u64)));
688            let result = engine.check(&tx).unwrap();
689            assert!(result.is_allowed());
690        }
691
692        #[test]
693        fn test_no_transaction_limit_allows_any_amount() {
694            let config = PolicyConfig::new();
695
696            let engine = DefaultPolicyEngine::new(config).unwrap();
697
698            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
699            let result = engine.check(&tx).unwrap();
700            assert!(result.is_allowed());
701        }
702
703        #[test]
704        fn test_zero_transaction_limit_denies_everything() {
705            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::ZERO);
706
707            let engine = DefaultPolicyEngine::new(config).unwrap();
708
709            // Even a tiny amount should be denied
710            let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
711            let result = engine.check(&tx).unwrap();
712            assert!(result.is_denied());
713
714            // Zero amount should be allowed (not exceeding)
715            let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
716            let result = engine.check(&tx).unwrap();
717            assert!(result.is_allowed());
718        }
719
720        #[test]
721        fn test_no_amount_skips_transaction_limit_check() {
722            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
723
724            let engine = DefaultPolicyEngine::new(config).unwrap();
725
726            let tx = create_test_tx(Some("0xREC"), None);
727            let result = engine.check(&tx).unwrap();
728            assert!(result.is_allowed());
729        }
730    }
731
732    // =========================================================================
733    // Rule precedence tests
734    // =========================================================================
735
736    mod rule_precedence_tests {
737        use super::*;
738
739        #[test]
740        fn test_blacklist_takes_precedence_over_whitelist() {
741            // This test verifies that blacklist check happens before whitelist
742            // We can't have an address in both lists (validation prevents it),
743            // but we can test that a blacklisted address is denied even if
744            // whitelist is enabled with other addresses
745            let config = PolicyConfig::new()
746                .with_whitelist(vec!["0xGOOD".to_string()])
747                .with_blacklist(vec!["0xBAD".to_string()]);
748
749            let engine = DefaultPolicyEngine::new(config).unwrap();
750
751            // Blacklisted address should be denied
752            let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
753            let result = engine.check(&tx).unwrap();
754            assert!(result.is_denied());
755            if let PolicyResult::Denied { rule, .. } = result {
756                assert_eq!(rule, "blacklist");
757            }
758        }
759
760        #[test]
761        fn test_whitelist_takes_precedence_over_tx_limit() {
762            let config = PolicyConfig::new()
763                .with_whitelist(vec!["0xGOOD".to_string()])
764                .with_transaction_limit("ETH", U256::from(100u64));
765
766            let engine = DefaultPolicyEngine::new(config).unwrap();
767
768            // Non-whitelisted address should be denied by whitelist, not tx_limit
769            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(1000u64)));
770            let result = engine.check(&tx).unwrap();
771            assert!(result.is_denied());
772            if let PolicyResult::Denied { rule, .. } = result {
773                assert_eq!(rule, "whitelist");
774            }
775        }
776
777        #[test]
778        fn test_full_rule_evaluation_order() {
779            // Create a config with all rules
780            let config = PolicyConfig::new()
781                .with_whitelist(vec!["0xGOOD".to_string(), "0xALSO_GOOD".to_string()])
782                .with_blacklist(vec!["0xBAD".to_string()])
783                .with_transaction_limit("ETH", U256::from(100u64));
784
785            let engine = DefaultPolicyEngine::new(config).unwrap();
786
787            // 1. Blacklisted address - denied by blacklist
788            let tx = create_test_tx(Some("0xBAD"), Some(U256::from(50u64)));
789            let result = engine.check(&tx).unwrap();
790            if let PolicyResult::Denied { rule, .. } = result {
791                assert_eq!(rule, "blacklist");
792            }
793
794            // 2. Not whitelisted - denied by whitelist
795            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(50u64)));
796            let result = engine.check(&tx).unwrap();
797            if let PolicyResult::Denied { rule, .. } = result {
798                assert_eq!(rule, "whitelist");
799            }
800
801            // 3. Whitelisted but over tx limit - denied by tx_limit
802            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(150u64)));
803            let result = engine.check(&tx).unwrap();
804            if let PolicyResult::Denied { rule, .. } = result {
805                assert_eq!(rule, "tx_limit");
806            }
807
808            // 4. All checks pass - allowed
809            let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(10u64)));
810            let result = engine.check(&tx).unwrap();
811            assert!(result.is_allowed());
812        }
813    }
814
815    // =========================================================================
816    // Send + Sync tests
817    // =========================================================================
818
819    mod send_sync_tests {
820        use super::*;
821
822        #[test]
823        fn test_policy_engine_is_send_sync() {
824            fn assert_send_sync<T: Send + Sync>() {}
825            assert_send_sync::<DefaultPolicyEngine>();
826        }
827
828        #[test]
829        fn test_policy_check_result_is_send_sync() {
830            fn assert_send_sync<T: Send + Sync>() {}
831            assert_send_sync::<PolicyCheckResult>();
832        }
833
834        #[test]
835        fn test_engine_can_be_shared_across_threads() {
836            use std::thread;
837
838            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(1000u64));
839
840            let engine = Arc::new(DefaultPolicyEngine::new(config).unwrap());
841
842            let mut handles = vec![];
843
844            for i in 0..5 {
845                let engine_clone = Arc::clone(&engine);
846                let handle = thread::spawn(move || {
847                    let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64 * (i + 1))));
848                    engine_clone.check(&tx)
849                });
850                handles.push(handle);
851            }
852
853            for handle in handles {
854                let result = handle.join().unwrap().unwrap();
855                // All should be allowed since they're all under the limit
856                assert!(result.is_allowed());
857            }
858        }
859    }
860
861    // =========================================================================
862    // Edge case tests
863    // =========================================================================
864
865    mod edge_case_tests {
866        use super::*;
867
868        #[test]
869        fn test_empty_config_allows_everything() {
870            let config = PolicyConfig::new();
871            let engine = DefaultPolicyEngine::new(config).unwrap();
872
873            let tx = create_test_tx(Some("0xANYONE"), Some(U256::MAX));
874            let result = engine.check(&tx).unwrap();
875            assert!(result.is_allowed());
876        }
877
878        #[test]
879        fn test_empty_transaction() {
880            let config = PolicyConfig::new()
881                .with_whitelist(vec!["0xGOOD".to_string()])
882                .with_transaction_limit("ETH", U256::from(100u64));
883
884            let engine = DefaultPolicyEngine::new(config).unwrap();
885
886            // Transaction with no recipient and no amount
887            let tx = ParsedTx::default();
888            let result = engine.check(&tx).unwrap();
889
890            // Should be allowed because:
891            // - No recipient to blacklist/whitelist
892            // - No amount to check against limits
893            assert!(result.is_allowed());
894        }
895
896        #[test]
897        fn test_max_u256_amount() {
898            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::MAX);
899
900            let engine = DefaultPolicyEngine::new(config).unwrap();
901
902            let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
903            let result = engine.check(&tx).unwrap();
904            assert!(result.is_allowed());
905        }
906
907        #[test]
908        fn test_engine_debug_format() {
909            let config = PolicyConfig::new();
910            let engine = DefaultPolicyEngine::new(config).unwrap();
911
912            let debug_str = format!("{engine:?}");
913            assert!(debug_str.contains("DefaultPolicyEngine"));
914            assert!(debug_str.contains("config"));
915        }
916
917        #[test]
918        fn test_check_result_equality() {
919            let result1 = PolicyCheckResult::Allowed;
920            let result2 = PolicyCheckResult::Allowed;
921            assert_eq!(result1, result2);
922
923            let result3 = PolicyCheckResult::DeniedBlacklisted {
924                address: "0xBAD".to_string(),
925            };
926            let result4 = PolicyCheckResult::DeniedBlacklisted {
927                address: "0xBAD".to_string(),
928            };
929            assert_eq!(result3, result4);
930        }
931
932        #[test]
933        fn test_check_result_clone() {
934            let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
935                token: "ETH".to_string(),
936                amount: U256::from(100u64),
937                limit: U256::from(50u64),
938            };
939            let cloned = result.clone();
940            assert_eq!(result, cloned);
941        }
942    }
943
944    // =========================================================================
945    // Additional coverage tests for uncovered branches
946    // =========================================================================
947
948    mod additional_coverage_tests {
949        use super::*;
950
951        #[test]
952        fn test_transaction_with_token_address_no_limit() {
953            let config = PolicyConfig::new();
954            let engine = DefaultPolicyEngine::new(config).unwrap();
955
956            // Token transaction without any configured limits
957            let tx = create_token_tx(
958                Some("0xREC"),
959                Some(U256::MAX),
960                "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
961            );
962            let result = engine.check(&tx).unwrap();
963            assert!(result.is_allowed());
964        }
965
966        #[test]
967        fn test_whitelist_disabled_explicitly() {
968            // Test that whitelist is properly disabled even when addresses are present
969            let config = PolicyConfig::new()
970                .with_whitelist(vec!["0xGOOD".to_string()])
971                .with_whitelist_enabled(false);
972
973            let engine = DefaultPolicyEngine::new(config).unwrap();
974
975            // Non-whitelisted address should be allowed when whitelist is disabled
976            let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
977            let result = engine.check(&tx).unwrap();
978            assert!(result.is_allowed());
979        }
980
981        #[test]
982        fn test_policy_check_result_all_variants_have_rule_names() {
983            // Verify all denied variants return a rule name
984            let blacklisted = PolicyCheckResult::DeniedBlacklisted {
985                address: "0x1".to_string(),
986            };
987            assert_eq!(blacklisted.rule_name(), Some("blacklist"));
988
989            let not_whitelisted = PolicyCheckResult::DeniedNotWhitelisted {
990                address: "0x2".to_string(),
991            };
992            assert_eq!(not_whitelisted.rule_name(), Some("whitelist"));
993
994            let tx_limit = PolicyCheckResult::DeniedExceedsTransactionLimit {
995                token: "ETH".to_string(),
996                amount: U256::from(10u64),
997                limit: U256::from(5u64),
998            };
999            assert_eq!(tx_limit.rule_name(), Some("tx_limit"));
1000        }
1001
1002        #[test]
1003        fn test_conversion_edge_case_unknown_rule() {
1004            // This tests the unwrap_or fallback in the conversion
1005            // Though in practice this should never happen with current variants
1006            let allowed = PolicyCheckResult::Allowed;
1007            let policy_result: PolicyResult = allowed.into();
1008            assert!(policy_result.is_allowed());
1009        }
1010
1011        // =====================================================================
1012        // Phase 2: PolicyCheckResult Denial Reason Messages
1013        // =====================================================================
1014
1015        #[test]
1016        fn should_generate_blacklisted_denial_reason_with_address() {
1017            // Arrange: Create blacklisted denial result
1018            let result = PolicyCheckResult::DeniedBlacklisted {
1019                address: "0xBADDEADBEEF123456789".to_string(),
1020            };
1021
1022            // Act: Get the reason message
1023            let reason = result.reason();
1024
1025            // Assert: Reason contains the address and clear explanation
1026            assert!(reason.is_some());
1027            let reason_str = reason.unwrap();
1028            assert!(
1029                reason_str.contains("0xBADDEADBEEF123456789"),
1030                "Reason should include the blacklisted address"
1031            );
1032            assert!(
1033                reason_str.contains("blacklisted"),
1034                "Reason should mention blacklisting"
1035            );
1036            assert!(
1037                reason_str.contains("recipient"),
1038                "Reason should mention recipient"
1039            );
1040        }
1041
1042        #[test]
1043        fn should_generate_not_whitelisted_denial_reason_with_address() {
1044            // Arrange: Create not-whitelisted denial result
1045            let result = PolicyCheckResult::DeniedNotWhitelisted {
1046                address: "0x1234567890ABCDEF".to_string(),
1047            };
1048
1049            // Act: Get the reason message
1050            let reason = result.reason();
1051
1052            // Assert: Reason contains the address and clear explanation
1053            assert!(reason.is_some());
1054            let reason_str = reason.unwrap();
1055            assert!(
1056                reason_str.contains("0x1234567890ABCDEF"),
1057                "Reason should include the non-whitelisted address"
1058            );
1059            assert!(
1060                reason_str.contains("not in whitelist"),
1061                "Reason should mention whitelist rejection"
1062            );
1063        }
1064
1065        #[test]
1066        fn should_generate_transaction_limit_denial_reason_with_amounts() {
1067            // Arrange: Create transaction limit denial with specific amounts
1068            let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
1069                token: "USDC".to_string(),
1070                amount: U256::from(5_000_000u64),
1071                limit: U256::from(1_000_000u64),
1072            };
1073
1074            // Act: Get the reason message
1075            let reason = result.reason();
1076
1077            // Assert: Reason contains all relevant amounts and token
1078            assert!(reason.is_some());
1079            let reason_str = reason.unwrap();
1080            assert!(
1081                reason_str.contains("5000000"),
1082                "Reason should include the attempted amount"
1083            );
1084            assert!(
1085                reason_str.contains("1000000"),
1086                "Reason should include the limit"
1087            );
1088            assert!(reason_str.contains("USDC"), "Reason should include token");
1089            assert!(
1090                reason_str.contains("exceeds transaction limit"),
1091                "Reason should explain the violation"
1092            );
1093        }
1094
1095        // =====================================================================
1096        // Phase 2: None Recipient Handling in Policy Checks
1097        // =====================================================================
1098
1099        #[test]
1100        fn should_allow_transaction_with_none_recipient_when_no_whitelist() {
1101            // Arrange: Policy with no whitelist (allow all)
1102            let config = PolicyConfig::new();
1103            let engine = DefaultPolicyEngine::new(config).unwrap();
1104
1105            // Act: Check transaction with None recipient (contract creation)
1106            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1107            let result = engine.check(&tx).unwrap();
1108
1109            // Assert: Should be allowed (no recipient to check)
1110            assert!(
1111                result.is_allowed(),
1112                "Transaction with None recipient should be allowed when no whitelist"
1113            );
1114        }
1115
1116        #[test]
1117        fn should_allow_none_recipient_when_whitelist_enabled() {
1118            // Arrange: Policy with whitelist enabled
1119            // Note: Whitelist/blacklist checks only apply when there IS a recipient
1120            let config = PolicyConfig::new().with_whitelist(vec!["0xALLOWED".to_string()]);
1121            let engine = DefaultPolicyEngine::new(config).unwrap();
1122
1123            // Act: Check transaction with None recipient (e.g., contract creation)
1124            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1125            let result = engine.check(&tx).unwrap();
1126
1127            // Assert: Should be allowed - whitelist check is skipped for None recipient
1128            // This is correct behavior: you can't whitelist/blacklist a non-existent address
1129            assert!(
1130                result.is_allowed(),
1131                "None recipient should pass whitelist check (check is skipped)"
1132            );
1133        }
1134
1135        #[test]
1136        fn should_allow_none_recipient_when_not_blacklisted() {
1137            // Arrange: Policy with only blacklist (None can't be blacklisted)
1138            let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
1139            let engine = DefaultPolicyEngine::new(config).unwrap();
1140
1141            // Act: Check transaction with None recipient
1142            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1143            let result = engine.check(&tx).unwrap();
1144
1145            // Assert: Should be allowed (None recipient can't be blacklisted)
1146            assert!(
1147                result.is_allowed(),
1148                "None recipient should pass blacklist check"
1149            );
1150        }
1151
1152        #[test]
1153        fn should_enforce_amount_limits_on_none_recipient_transactions() {
1154            // Arrange: Policy with transaction limit but no address filters
1155            let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(500u64));
1156            let engine = DefaultPolicyEngine::new(config).unwrap();
1157
1158            // Act: Check transaction with None recipient but exceeds limit
1159            let tx = create_test_tx(None, Some(U256::from(1000u64)));
1160            let result = engine.check(&tx).unwrap();
1161
1162            // Assert: Should be denied (amount limit applies regardless of recipient)
1163            assert!(
1164                result.is_denied(),
1165                "Amount limits should apply to None recipient transactions"
1166            );
1167            if let PolicyResult::Denied { rule, .. } = result {
1168                assert_eq!(rule, "tx_limit");
1169            }
1170        }
1171    }
1172}