1pub mod datasource;
7pub mod etherscan;
8pub mod risk;
9pub mod token_risk;
10
11use risk::{RiskAssessment, RiskEngine};
12
13pub struct ComplianceAnalyzer {
15 risk_engine: RiskEngine,
16}
17
18impl Default for ComplianceAnalyzer {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl ComplianceAnalyzer {
25 pub fn new() -> Self {
27 Self {
28 risk_engine: RiskEngine::new(),
29 }
30 }
31
32 pub async fn analyze_address(
34 &self,
35 address: &str,
36 chain: &str,
37 ) -> anyhow::Result<RiskAssessment> {
38 self.risk_engine.assess_address(address, chain).await
39 }
40
41 pub fn check_sanctions(&self, _address: &str) -> SanctionsCheckResult {
46 SanctionsCheckResult {
48 is_sanctioned: false,
49 lists_checked: vec![],
50 matches: vec![],
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct SanctionsCheckResult {
58 pub is_sanctioned: bool,
59 pub lists_checked: Vec<String>,
60 pub matches: Vec<SanctionsMatch>,
61}
62
63#[derive(Debug, Clone)]
65pub struct SanctionsMatch {
66 pub list_name: String,
67 pub entity_name: String,
68 pub match_type: MatchType,
69 pub confidence: f32,
70}
71
72#[derive(Debug, Clone)]
74pub enum MatchType {
75 Exact,
76 Partial,
77 Associated,
78}
79
80impl SanctionsCheckResult {
81 pub fn has_matches(&self) -> bool {
83 !self.matches.is_empty()
84 }
85
86 pub fn summary(&self) -> String {
88 if self.is_sanctioned {
89 format!(
90 "⚠️ SANCTIONS MATCH FOUND! Checked {} lists, found {} matches.",
91 self.lists_checked.len(),
92 self.matches.len()
93 )
94 } else {
95 format!(
96 "✅ No sanctions matches. Checked: {}",
97 self.lists_checked.join(", ")
98 )
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_sanctions_check_result() {
109 let result = SanctionsCheckResult {
110 is_sanctioned: false,
111 lists_checked: vec!["OFAC".to_string()],
112 matches: vec![],
113 };
114
115 assert!(!result.has_matches());
116 }
117
118 #[test]
119 fn test_sanctions_match_found() {
120 let result = SanctionsCheckResult {
121 is_sanctioned: true,
122 lists_checked: vec!["OFAC".to_string()],
123 matches: vec![SanctionsMatch {
124 list_name: "OFAC".to_string(),
125 entity_name: "Test Entity".to_string(),
126 match_type: MatchType::Exact,
127 confidence: 1.0,
128 }],
129 };
130
131 assert!(result.has_matches());
132 assert!(result.summary().contains("SANCTIONS MATCH"));
133 }
134
135 #[test]
136 fn test_sanctions_no_match_summary() {
137 let result = SanctionsCheckResult {
138 is_sanctioned: false,
139 lists_checked: vec!["OFAC".to_string(), "EU".to_string()],
140 matches: vec![],
141 };
142
143 let summary = result.summary();
144 assert!(summary.contains("No sanctions matches"));
145 assert!(summary.contains("OFAC"));
146 assert!(summary.contains("EU"));
147 }
148
149 #[test]
150 fn test_compliance_analyzer_new() {
151 let analyzer = ComplianceAnalyzer::new();
152 let result = analyzer.check_sanctions("0xtest");
153 assert!(!result.is_sanctioned);
154 assert!(result.lists_checked.is_empty());
155 assert!(result.matches.is_empty());
156 }
157
158 #[test]
159 fn test_compliance_analyzer_default() {
160 let analyzer = ComplianceAnalyzer::default();
161 let result = analyzer.check_sanctions("0xtest");
162 assert!(!result.has_matches());
163 }
164
165 #[tokio::test]
166 async fn test_compliance_analyze_address() {
167 let analyzer = ComplianceAnalyzer::new();
168 let assessment = analyzer
169 .analyze_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "ethereum")
170 .await
171 .unwrap();
172 assert_eq!(
173 assessment.address,
174 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"
175 );
176 assert_eq!(assessment.chain, "ethereum");
177 assert_eq!(assessment.factors.len() + assessment.not_assessed.len(), 4);
179 }
180
181 #[test]
182 fn test_match_type_variants() {
183 let exact = MatchType::Exact;
185 let partial = MatchType::Partial;
186 let associated = MatchType::Associated;
187 assert!(format!("{:?}", exact).contains("Exact"));
188 assert!(format!("{:?}", partial).contains("Partial"));
189 assert!(format!("{:?}", associated).contains("Associated"));
190 }
191
192 #[test]
193 fn test_sanctions_check_result_debug() {
194 let result = SanctionsCheckResult {
195 is_sanctioned: false,
196 lists_checked: vec![],
197 matches: vec![],
198 };
199 let debug = format!("{:?}", result);
200 assert!(debug.contains("SanctionsCheckResult"));
201 }
202
203 #[test]
204 fn test_sanctions_match_debug() {
205 let m = SanctionsMatch {
206 list_name: "OFAC".to_string(),
207 entity_name: "Test".to_string(),
208 match_type: MatchType::Partial,
209 confidence: 0.85,
210 };
211 let debug = format!("{:?}", m);
212 assert!(debug.contains("OFAC"));
213 assert!(debug.contains("0.85"));
214 }
215
216 #[test]
217 fn test_sanctions_check_result_empty_lists() {
218 let result = SanctionsCheckResult {
219 is_sanctioned: false,
220 lists_checked: vec![],
221 matches: vec![],
222 };
223 let summary = result.summary();
224 assert!(summary.contains("No sanctions matches"));
225 assert!(summary.contains("Checked: "));
226 }
227
228 #[test]
229 fn test_sanctions_check_result_sanctioned_summary() {
230 let result = SanctionsCheckResult {
231 is_sanctioned: true,
232 lists_checked: vec!["OFAC".to_string(), "EU".to_string()],
233 matches: vec![
234 SanctionsMatch {
235 list_name: "OFAC".to_string(),
236 entity_name: "Entity A".to_string(),
237 match_type: MatchType::Exact,
238 confidence: 1.0,
239 },
240 SanctionsMatch {
241 list_name: "EU".to_string(),
242 entity_name: "Entity B".to_string(),
243 match_type: MatchType::Partial,
244 confidence: 0.9,
245 },
246 ],
247 };
248 let summary = result.summary();
249 assert!(summary.contains("SANCTIONS MATCH"));
250 assert!(summary.contains("2 lists"));
251 assert!(summary.contains("2 matches"));
252 }
253
254 #[test]
255 fn test_sanctions_match_clone() {
256 let m = SanctionsMatch {
257 list_name: "OFAC".to_string(),
258 entity_name: "Test".to_string(),
259 match_type: MatchType::Associated,
260 confidence: 0.75,
261 };
262 let cloned = m.clone();
263 assert_eq!(cloned.list_name, "OFAC");
264 assert_eq!(cloned.confidence, 0.75);
265 }
266}