Skip to main content

scope/compliance/
mod.rs

1//! Compliance module for Scope
2//!
3//! Provides risk scoring, transaction taint analysis, pattern detection,
4//! and compliance reporting for blockchain addresses and transactions.
5
6pub mod datasource;
7pub mod risk;
8
9use risk::{RiskAssessment, RiskEngine};
10
11/// Main compliance analyzer
12pub struct ComplianceAnalyzer {
13    risk_engine: RiskEngine,
14}
15
16impl Default for ComplianceAnalyzer {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl ComplianceAnalyzer {
23    /// Create a new compliance analyzer
24    pub fn new() -> Self {
25        Self {
26            risk_engine: RiskEngine::new(),
27        }
28    }
29
30    /// Analyze an address for compliance risks
31    pub async fn analyze_address(
32        &self,
33        address: &str,
34        chain: &str,
35    ) -> anyhow::Result<RiskAssessment> {
36        self.risk_engine.assess_address(address, chain).await
37    }
38
39    /// Check if an address matches known sanctions lists
40    ///
41    /// Note: Sanctions checking requires external API integration.
42    /// This function returns a structure ready for future implementation.
43    pub fn check_sanctions(&self, _address: &str) -> SanctionsCheckResult {
44        // Future: Integrate with OFAC, EU, UN sanctions databases
45        SanctionsCheckResult {
46            is_sanctioned: false,
47            lists_checked: vec![],
48            matches: vec![],
49        }
50    }
51}
52
53/// Result of sanctions list check
54#[derive(Debug, Clone)]
55pub struct SanctionsCheckResult {
56    pub is_sanctioned: bool,
57    pub lists_checked: Vec<String>,
58    pub matches: Vec<SanctionsMatch>,
59}
60
61/// Individual sanctions list match
62#[derive(Debug, Clone)]
63pub struct SanctionsMatch {
64    pub list_name: String,
65    pub entity_name: String,
66    pub match_type: MatchType,
67    pub confidence: f32,
68}
69
70/// Type of sanctions match
71#[derive(Debug, Clone)]
72pub enum MatchType {
73    Exact,
74    Partial,
75    Associated,
76}
77
78impl SanctionsCheckResult {
79    /// Check if any sanctions list matches were found
80    pub fn has_matches(&self) -> bool {
81        !self.matches.is_empty()
82    }
83
84    /// Get formatted summary of the check
85    pub fn summary(&self) -> String {
86        if self.is_sanctioned {
87            format!(
88                "⚠️  SANCTIONS MATCH FOUND! Checked {} lists, found {} matches.",
89                self.lists_checked.len(),
90                self.matches.len()
91            )
92        } else {
93            format!(
94                "✅ No sanctions matches. Checked: {}",
95                self.lists_checked.join(", ")
96            )
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_sanctions_check_result() {
107        let result = SanctionsCheckResult {
108            is_sanctioned: false,
109            lists_checked: vec!["OFAC".to_string()],
110            matches: vec![],
111        };
112
113        assert!(!result.has_matches());
114    }
115
116    #[test]
117    fn test_sanctions_match_found() {
118        let result = SanctionsCheckResult {
119            is_sanctioned: true,
120            lists_checked: vec!["OFAC".to_string()],
121            matches: vec![SanctionsMatch {
122                list_name: "OFAC".to_string(),
123                entity_name: "Test Entity".to_string(),
124                match_type: MatchType::Exact,
125                confidence: 1.0,
126            }],
127        };
128
129        assert!(result.has_matches());
130        assert!(result.summary().contains("SANCTIONS MATCH"));
131    }
132
133    #[test]
134    fn test_sanctions_no_match_summary() {
135        let result = SanctionsCheckResult {
136            is_sanctioned: false,
137            lists_checked: vec!["OFAC".to_string(), "EU".to_string()],
138            matches: vec![],
139        };
140
141        let summary = result.summary();
142        assert!(summary.contains("No sanctions matches"));
143        assert!(summary.contains("OFAC"));
144        assert!(summary.contains("EU"));
145    }
146
147    #[test]
148    fn test_compliance_analyzer_new() {
149        let analyzer = ComplianceAnalyzer::new();
150        let result = analyzer.check_sanctions("0xtest");
151        assert!(!result.is_sanctioned);
152        assert!(result.lists_checked.is_empty());
153        assert!(result.matches.is_empty());
154    }
155
156    #[test]
157    fn test_compliance_analyzer_default() {
158        let analyzer = ComplianceAnalyzer::default();
159        let result = analyzer.check_sanctions("0xtest");
160        assert!(!result.has_matches());
161    }
162
163    #[tokio::test]
164    async fn test_compliance_analyze_address() {
165        let analyzer = ComplianceAnalyzer::new();
166        let assessment = analyzer
167            .analyze_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "ethereum")
168            .await
169            .unwrap();
170        assert_eq!(
171            assessment.address,
172            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"
173        );
174        assert_eq!(assessment.chain, "ethereum");
175        assert!(!assessment.factors.is_empty());
176    }
177
178    #[test]
179    fn test_match_type_variants() {
180        // Test all match type variants
181        let exact = MatchType::Exact;
182        let partial = MatchType::Partial;
183        let associated = MatchType::Associated;
184        assert!(format!("{:?}", exact).contains("Exact"));
185        assert!(format!("{:?}", partial).contains("Partial"));
186        assert!(format!("{:?}", associated).contains("Associated"));
187    }
188
189    #[test]
190    fn test_sanctions_check_result_debug() {
191        let result = SanctionsCheckResult {
192            is_sanctioned: false,
193            lists_checked: vec![],
194            matches: vec![],
195        };
196        let debug = format!("{:?}", result);
197        assert!(debug.contains("SanctionsCheckResult"));
198    }
199
200    #[test]
201    fn test_sanctions_match_debug() {
202        let m = SanctionsMatch {
203            list_name: "OFAC".to_string(),
204            entity_name: "Test".to_string(),
205            match_type: MatchType::Partial,
206            confidence: 0.85,
207        };
208        let debug = format!("{:?}", m);
209        assert!(debug.contains("OFAC"));
210        assert!(debug.contains("0.85"));
211    }
212}