1use crate::compliance::datasource::{BlockchainDataClient, DataSources, analyze_patterns};
4use crate::compliance::risk::RiskEngine;
5use crate::display::{OutputFormat, format_risk_report};
6use clap::{Args, Subcommand};
7
8#[derive(Debug, Subcommand)]
9pub enum ComplianceCommands {
10 #[command(name = "risk")]
12 Risk(RiskArgs),
13
14 #[command(name = "trace")]
16 Trace(TraceArgs),
17
18 #[command(name = "analyze")]
20 Analyze(AnalyzeArgs),
21
22 #[command(name = "compliance-report")]
24 ComplianceReport(ComplianceReportArgs),
25}
26
27#[derive(Debug, Args)]
28pub struct RiskArgs {
29 #[arg(value_name = "ADDRESS")]
31 pub address: String,
32
33 #[arg(short, long)]
35 pub chain: Option<String>,
36
37 #[arg(short, long, value_enum, default_value = "table")]
39 pub format: OutputFormat,
40
41 #[arg(long)]
43 pub detailed: bool,
44
45 #[arg(short, long)]
47 pub output: Option<String>,
48}
49
50#[derive(Debug, Args)]
51pub struct TraceArgs {
52 #[arg(value_name = "TX_HASH")]
54 pub tx_hash: String,
55
56 #[arg(short, long, default_value = "3")]
58 pub depth: u32,
59
60 #[arg(long)]
62 pub flag_suspicious: bool,
63
64 #[arg(short, long, value_enum, default_value = "table")]
66 pub format: OutputFormat,
67}
68
69#[derive(Debug, Args)]
70pub struct AnalyzeArgs {
71 #[arg(value_name = "ADDRESS")]
73 pub address: String,
74
75 #[arg(long, value_enum, default_values = &["structuring", "layering", "integration"])]
77 pub patterns: Vec<PatternType>,
78
79 #[arg(short, long, default_value = "30d")]
81 pub range: String,
82
83 #[arg(short, long, value_enum, default_value = "table")]
85 pub format: OutputFormat,
86}
87
88#[derive(Debug, Args)]
89pub struct ComplianceReportArgs {
90 #[arg(value_name = "TARGET")]
92 pub target: String,
93
94 #[arg(short, long, value_enum)]
96 pub jurisdiction: Jurisdiction,
97
98 #[arg(short, long, value_enum, default_value = "summary")]
100 pub report_type: ReportType,
101
102 #[arg(short, long, required = true)]
104 pub output: String,
105}
106
107#[derive(Clone, Copy, Debug, clap::ValueEnum)]
108pub enum PatternType {
109 Structuring,
110 Layering,
111 Integration,
112 Velocity,
113 RoundNumbers,
114}
115
116#[derive(Clone, Copy, Debug, clap::ValueEnum)]
117pub enum Jurisdiction {
118 US,
119 EU,
120 UK,
121 Switzerland,
122 Singapore,
123}
124
125#[derive(Clone, Copy, Debug, clap::ValueEnum)]
126pub enum ReportType {
127 Summary,
128 Detailed,
129 SAR, TravelRule,
131}
132
133pub async fn handle_risk(args: RiskArgs) -> anyhow::Result<()> {
135 handle_risk_with_client(args, None).await
136}
137
138pub async fn handle_risk_with_client(
140 args: RiskArgs,
141 client: Option<BlockchainDataClient>,
142) -> anyhow::Result<()> {
143 let chain = match args.chain {
145 Some(c) => c,
146 None => detect_chain(&args.address)?,
147 };
148
149 println!("Assessing risk for {} on {}...", args.address, chain);
150
151 let engine = if let Some(c) = client {
152 println!("Using Etherscan API for enhanced analysis");
153 RiskEngine::with_data_client(c)
154 } else {
155 let etherscan_key = std::env::var("ETHERSCAN_API_KEY").ok();
157
158 if let Some(key) = etherscan_key {
159 let sources = DataSources::new(key);
160 let client = BlockchainDataClient::new(sources);
161 println!("Using Etherscan API for enhanced analysis");
162 RiskEngine::with_data_client(client)
163 } else {
164 println!("Note: Set ETHERSCAN_API_KEY for enhanced analysis");
165 RiskEngine::new()
166 }
167 };
168
169 let assessment = engine.assess_address(&args.address, &chain).await?;
170
171 let output = format_risk_report(&assessment, args.format, args.detailed);
173 println!("{}", output);
174
175 if let Some(path) = args.output {
177 let json = serde_json::to_string_pretty(&assessment)?;
178 std::fs::write(&path, json)?;
179 println!("\nReport exported to: {}", path);
180 }
181
182 Ok(())
183}
184
185pub async fn handle_trace(args: TraceArgs) -> anyhow::Result<()> {
187 handle_trace_with_client(args, None).await
188}
189
190pub async fn handle_trace_with_client(
192 args: TraceArgs,
193 client: Option<BlockchainDataClient>,
194) -> anyhow::Result<()> {
195 println!("Tracing transaction {}...", args.tx_hash);
196 println!("Depth: {} hops", args.depth);
197
198 if args.flag_suspicious {
199 println!("Flagging suspicious addresses enabled");
200 }
201
202 let resolved_client = if let Some(c) = client {
203 Some(c)
204 } else {
205 std::env::var("ETHERSCAN_API_KEY").ok().map(|key| {
206 let sources = DataSources::new(key);
207 BlockchainDataClient::new(sources)
208 })
209 };
210
211 if let Some(client) = resolved_client {
212 match client.trace_transaction(&args.tx_hash, args.depth).await {
213 Ok(trace) => {
214 println!("\nTransaction Trace");
215 println!("=================");
216 println!("Root: {}", trace.root_hash);
217 println!("Hops: {}", trace.hops.len());
218
219 for hop in &trace.hops {
220 println!(
221 " Depth {}: {} ({} ETH)",
222 hop.depth, hop.address, hop.amount
223 );
224 }
225 }
226 Err(e) => {
227 eprintln!("Error tracing transaction: {}", e);
228 }
229 }
230 } else {
231 println!("Set ETHERSCAN_API_KEY to enable transaction tracing");
232 }
233
234 Ok(())
235}
236
237pub async fn handle_analyze(args: AnalyzeArgs) -> anyhow::Result<()> {
239 handle_analyze_with_client(args, None).await
240}
241
242pub async fn handle_analyze_with_client(
244 args: AnalyzeArgs,
245 client: Option<BlockchainDataClient>,
246) -> anyhow::Result<()> {
247 println!("Analyzing patterns for {}...", args.address);
248 println!("Patterns: {:?}", args.patterns);
249 println!("Time range: {}", args.range);
250
251 let resolved_client = if let Some(c) = client {
252 Some(c)
253 } else {
254 std::env::var("ETHERSCAN_API_KEY").ok().map(|key| {
255 let sources = DataSources::new(key);
256 BlockchainDataClient::new(sources)
257 })
258 };
259
260 if let Some(client) = resolved_client {
261 let chain = match detect_chain(&args.address) {
263 Ok(c) => c,
264 Err(_) => "ethereum".to_string(),
265 };
266
267 match client.get_transactions(&args.address, &chain).await {
268 Ok(txs) => {
269 let analysis = analyze_patterns(&txs);
270
271 println!("\nPattern Analysis Results");
272 println!("========================");
273 println!("Total transactions: {}", analysis.total_transactions);
274 println!("Velocity: {:.2} tx/day", analysis.velocity_score);
275 println!("Structuring detected: {}", analysis.structuring_detected);
276 println!("Round number pattern: {}", analysis.round_number_pattern);
277 println!("Unusual hour transactions: {}", analysis.unusual_hours);
278 }
279 Err(e) => {
280 eprintln!("Error fetching transactions: {}", e);
281 }
282 }
283 } else {
284 println!("Set ETHERSCAN_API_KEY to enable pattern analysis");
285 }
286
287 Ok(())
288}
289
290pub async fn handle_compliance_report(args: ComplianceReportArgs) -> anyhow::Result<()> {
292 println!("Compliance report generation is not yet implemented.");
293 println!(
294 "Planned features: {} report for {:?} jurisdiction",
295 format!("{:?}", args.report_type).to_lowercase(),
296 args.jurisdiction
297 );
298 println!("\nFor now, use 'scope risk' and 'scope analyze' for compliance checks.");
299
300 Ok(())
301}
302
303fn detect_chain(address: &str) -> anyhow::Result<String> {
305 if address.starts_with("0x") && address.len() == 42 {
306 Ok("ethereum".to_string())
308 } else if address.len() == 32 || address.len() == 44 {
309 Ok("solana".to_string())
311 } else if address.starts_with("T") && address.len() == 34 {
312 Ok("tron".to_string())
314 } else if address.starts_with("bc1") || address.starts_with("1") || address.starts_with("3") {
315 Ok("bitcoin".to_string())
317 } else {
318 anyhow::bail!("Could not auto-detect chain from address: {}", address)
319 }
320}
321
322#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_detect_chain_ethereum() {
332 let result = detect_chain("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
333 assert!(result.is_ok());
334 assert_eq!(result.unwrap(), "ethereum");
335 }
336
337 #[test]
338 fn test_detect_chain_solana_short() {
339 let result = detect_chain("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC");
341 assert!(result.is_ok());
342 assert_eq!(result.unwrap(), "solana");
343 }
344
345 #[test]
346 fn test_detect_chain_solana_long() {
347 let result = detect_chain("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy");
349 assert!(result.is_ok());
350 assert_eq!(result.unwrap(), "solana");
351 }
352
353 #[test]
354 fn test_detect_chain_tron() {
355 let result = detect_chain("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf");
356 assert!(result.is_ok());
357 assert_eq!(result.unwrap(), "tron");
358 }
359
360 #[test]
361 fn test_detect_chain_bitcoin_bech32() {
362 let result = detect_chain("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
363 assert!(result.is_ok());
364 assert_eq!(result.unwrap(), "bitcoin");
365 }
366
367 #[test]
368 fn test_detect_chain_bitcoin_p2pkh() {
369 let result = detect_chain("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2");
370 assert!(result.is_ok());
371 assert_eq!(result.unwrap(), "bitcoin");
372 }
373
374 #[test]
375 fn test_detect_chain_bitcoin_p2sh() {
376 let result = detect_chain("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy");
377 assert!(result.is_ok());
378 assert_eq!(result.unwrap(), "bitcoin");
379 }
380
381 #[test]
382 fn test_detect_chain_unknown() {
383 let result = detect_chain("unknown_address_format_xyz");
384 assert!(result.is_err());
385 }
386
387 #[tokio::test]
388 async fn test_handle_risk_no_api_key() {
389 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
391 let args = RiskArgs {
392 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
393 chain: Some("ethereum".to_string()),
394 format: OutputFormat::Table,
395 detailed: false,
396 output: None,
397 };
398 let result = handle_risk(args).await;
399 assert!(result.is_ok());
400 }
401
402 #[tokio::test]
403 async fn test_handle_risk_json_format() {
404 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
405 let args = RiskArgs {
406 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
407 chain: Some("ethereum".to_string()),
408 format: OutputFormat::Json,
409 detailed: true,
410 output: None,
411 };
412 let result = handle_risk(args).await;
413 assert!(result.is_ok());
414 }
415
416 #[tokio::test]
417 async fn test_handle_risk_with_export() {
418 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
419 let temp = tempfile::NamedTempFile::new().unwrap();
420 let path = temp.path().to_string_lossy().to_string();
421 let args = RiskArgs {
422 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
423 chain: Some("ethereum".to_string()),
424 format: OutputFormat::Table,
425 detailed: false,
426 output: Some(path.clone()),
427 };
428 let result = handle_risk(args).await;
429 assert!(result.is_ok());
430 assert!(std::path::Path::new(&path).exists());
431 }
432
433 #[tokio::test]
434 async fn test_handle_risk_auto_detect_chain() {
435 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
436 let args = RiskArgs {
437 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
438 chain: None, format: OutputFormat::Table,
440 detailed: false,
441 output: None,
442 };
443 let result = handle_risk(args).await;
444 assert!(result.is_ok());
445 }
446
447 #[tokio::test]
448 async fn test_handle_trace_no_api_key() {
449 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
450 let args = TraceArgs {
451 tx_hash: "0xabc123".to_string(),
452 depth: 3,
453 flag_suspicious: true,
454 format: OutputFormat::Table,
455 };
456 let result = handle_trace(args).await;
457 assert!(result.is_ok()); }
459
460 #[tokio::test]
461 async fn test_handle_analyze_no_api_key() {
462 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
463 let args = AnalyzeArgs {
464 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
465 patterns: vec![PatternType::Structuring, PatternType::Layering],
466 range: "30d".to_string(),
467 format: OutputFormat::Table,
468 };
469 let result = handle_analyze(args).await;
470 assert!(result.is_ok());
471 }
472
473 #[tokio::test]
474 async fn test_handle_compliance_report() {
475 let args = ComplianceReportArgs {
476 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
477 jurisdiction: Jurisdiction::US,
478 report_type: ReportType::Summary,
479 output: "/tmp/test_compliance.json".to_string(),
480 };
481 let result = handle_compliance_report(args).await;
482 assert!(result.is_ok()); }
484
485 #[tokio::test]
486 async fn test_handle_compliance_report_eu_detailed() {
487 let args = ComplianceReportArgs {
488 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
489 jurisdiction: Jurisdiction::EU,
490 report_type: ReportType::Detailed,
491 output: "/tmp/test_compliance_eu.json".to_string(),
492 };
493 let result = handle_compliance_report(args).await;
494 assert!(result.is_ok());
495 }
496
497 #[tokio::test]
498 async fn test_handle_compliance_report_uk_sar() {
499 let args = ComplianceReportArgs {
500 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
501 jurisdiction: Jurisdiction::UK,
502 report_type: ReportType::SAR,
503 output: "/tmp/test_compliance_uk.json".to_string(),
504 };
505 let result = handle_compliance_report(args).await;
506 assert!(result.is_ok());
507 }
508
509 #[tokio::test]
510 async fn test_handle_compliance_report_singapore_travel_rule() {
511 let args = ComplianceReportArgs {
512 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
513 jurisdiction: Jurisdiction::Singapore,
514 report_type: ReportType::TravelRule,
515 output: "/tmp/test_compliance_sg.json".to_string(),
516 };
517 let result = handle_compliance_report(args).await;
518 assert!(result.is_ok());
519 }
520
521 #[tokio::test]
522 async fn test_handle_compliance_report_switzerland() {
523 let args = ComplianceReportArgs {
524 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
525 jurisdiction: Jurisdiction::Switzerland,
526 report_type: ReportType::Summary,
527 output: "/tmp/test_compliance_ch.json".to_string(),
528 };
529 let result = handle_compliance_report(args).await;
530 assert!(result.is_ok());
531 }
532
533 #[tokio::test]
534 async fn test_handle_risk_yaml_format() {
535 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
536 let args = RiskArgs {
537 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
538 chain: Some("ethereum".to_string()),
539 format: OutputFormat::Yaml,
540 detailed: false,
541 output: None,
542 };
543 let result = handle_risk(args).await;
544 assert!(result.is_ok());
545 }
546
547 fn mock_etherscan_response(txs: &[serde_json::Value]) -> String {
552 serde_json::json!({
553 "status": "1",
554 "message": "OK",
555 "result": txs
556 })
557 .to_string()
558 }
559
560 fn make_mock_client(base_url: &str) -> BlockchainDataClient {
561 let sources = DataSources::new("test_api_key".to_string());
562 BlockchainDataClient::with_base_url(sources, base_url)
563 }
564
565 #[tokio::test]
566 async fn test_handle_risk_with_api_client() {
567 let mut server = mockito::Server::new_async().await;
568 let _mock = server
569 .mock("GET", mockito::Matcher::Any)
570 .with_status(200)
571 .with_body(mock_etherscan_response(&[]))
572 .create_async()
573 .await;
574
575 let client = make_mock_client(&server.url());
576 let args = RiskArgs {
577 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
578 chain: Some("ethereum".to_string()),
579 format: OutputFormat::Table,
580 detailed: true,
581 output: None,
582 };
583 let result = handle_risk_with_client(args, Some(client)).await;
584 assert!(result.is_ok());
585 }
586
587 #[tokio::test]
588 async fn test_handle_risk_with_api_client_json_export() {
589 let mut server = mockito::Server::new_async().await;
590 let _mock = server
591 .mock("GET", mockito::Matcher::Any)
592 .with_status(200)
593 .with_body(mock_etherscan_response(&[]))
594 .create_async()
595 .await;
596
597 let client = make_mock_client(&server.url());
598 let tmp = tempfile::NamedTempFile::new().unwrap();
599 let path = tmp.path().to_string_lossy().to_string();
600
601 let args = RiskArgs {
602 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
603 chain: Some("ethereum".to_string()),
604 format: OutputFormat::Table,
605 detailed: false,
606 output: Some(path.clone()),
607 };
608 let result = handle_risk_with_client(args, Some(client)).await;
609 assert!(result.is_ok());
610 assert!(std::path::Path::new(&path).exists());
611 }
612
613 #[tokio::test]
614 async fn test_handle_trace_with_api_client() {
615 let mut server = mockito::Server::new_async().await;
616 let _mock = server
617 .mock("GET", mockito::Matcher::Any)
618 .with_status(200)
619 .with_body(mock_etherscan_response(&[serde_json::json!({
620 "hash": "0xabc",
621 "from": "0x111",
622 "to": "0x222",
623 "value": "1000000000000000000",
624 "timeStamp": "1700000000",
625 "blockNumber": "18000000",
626 "gasUsed": "21000",
627 "gasPrice": "50000000000",
628 "isError": "0",
629 "input": "0x"
630 })]))
631 .create_async()
632 .await;
633
634 let client = make_mock_client(&server.url());
635 let args = TraceArgs {
636 tx_hash: "0xabc123def456".to_string(),
637 depth: 2,
638 flag_suspicious: true,
639 format: OutputFormat::Table,
640 };
641 let result = handle_trace_with_client(args, Some(client)).await;
642 assert!(result.is_ok());
643 }
644
645 #[tokio::test]
646 async fn test_handle_trace_with_api_client_error() {
647 let mut server = mockito::Server::new_async().await;
648 let _mock = server
649 .mock("GET", mockito::Matcher::Any)
650 .with_status(200)
651 .with_body(r#"{"status":"0","message":"NOTOK","result":"Error"}"#)
652 .create_async()
653 .await;
654
655 let client = make_mock_client(&server.url());
656 let args = TraceArgs {
657 tx_hash: "0xabc123def456".to_string(),
658 depth: 3,
659 flag_suspicious: false,
660 format: OutputFormat::Table,
661 };
662 let result = handle_trace_with_client(args, Some(client)).await;
664 assert!(result.is_ok());
665 }
666
667 #[tokio::test]
668 async fn test_handle_analyze_with_api_client() {
669 let mut server = mockito::Server::new_async().await;
670 let _mock = server
671 .mock("GET", mockito::Matcher::Any)
672 .with_status(200)
673 .with_body(mock_etherscan_response(&[serde_json::json!({
674 "hash": "0xabc",
675 "from": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
676 "to": "0x222",
677 "value": "1000000000000000000",
678 "timeStamp": "1700000000",
679 "blockNumber": "18000000",
680 "gasUsed": "21000",
681 "gasPrice": "50000000000",
682 "isError": "0",
683 "input": "0x"
684 })]))
685 .create_async()
686 .await;
687
688 let client = make_mock_client(&server.url());
689 let args = AnalyzeArgs {
690 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
691 patterns: vec![PatternType::Structuring, PatternType::Velocity],
692 range: "30d".to_string(),
693 format: OutputFormat::Table,
694 };
695 let result = handle_analyze_with_client(args, Some(client)).await;
696 assert!(result.is_ok());
697 }
698
699 #[tokio::test]
700 async fn test_handle_analyze_with_api_client_error() {
701 let mut server = mockito::Server::new_async().await;
702 let _mock = server
703 .mock("GET", mockito::Matcher::Any)
704 .with_status(200)
705 .with_body(r#"{"status":"0","message":"NOTOK","result":"Error"}"#)
706 .create_async()
707 .await;
708
709 let client = make_mock_client(&server.url());
710 let args = AnalyzeArgs {
711 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
712 patterns: vec![PatternType::Layering],
713 range: "7d".to_string(),
714 format: OutputFormat::Table,
715 };
716 let result = handle_analyze_with_client(args, Some(client)).await;
718 assert!(result.is_ok());
719 }
720
721 #[tokio::test]
722 async fn test_handle_analyze_with_detect_chain_failure() {
723 let mut server = mockito::Server::new_async().await;
724 let _mock = server
725 .mock("GET", mockito::Matcher::Any)
726 .with_status(200)
727 .with_body(mock_etherscan_response(&[]))
728 .create_async()
729 .await;
730
731 let client = make_mock_client(&server.url());
732 let args = AnalyzeArgs {
734 address: "unknown_format_addr".to_string(),
735 patterns: vec![PatternType::Integration],
736 range: "1y".to_string(),
737 format: OutputFormat::Json,
738 };
739 let result = handle_analyze_with_client(args, Some(client)).await;
740 assert!(result.is_ok());
741 }
742
743 #[tokio::test]
744 async fn test_handle_risk_markdown_detailed() {
745 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
746 let args = RiskArgs {
747 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
748 chain: Some("ethereum".to_string()),
749 format: OutputFormat::Markdown,
750 detailed: true,
751 output: None,
752 };
753 let result = handle_risk(args).await;
754 assert!(result.is_ok());
755 }
756
757 #[tokio::test]
758 async fn test_handle_trace_no_flag_suspicious() {
759 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
760 let args = TraceArgs {
761 tx_hash: "0xdef456".to_string(),
762 depth: 5,
763 flag_suspicious: false,
764 format: OutputFormat::Json,
765 };
766 let result = handle_trace(args).await;
767 assert!(result.is_ok());
768 }
769
770 #[tokio::test]
771 async fn test_handle_analyze_all_patterns() {
772 unsafe { std::env::remove_var("ETHERSCAN_API_KEY") };
773 let args = AnalyzeArgs {
774 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
775 patterns: vec![
776 PatternType::Structuring,
777 PatternType::Layering,
778 PatternType::Integration,
779 PatternType::Velocity,
780 PatternType::RoundNumbers,
781 ],
782 range: "6m".to_string(),
783 format: OutputFormat::Json,
784 };
785 let result = handle_analyze(args).await;
786 assert!(result.is_ok());
787 }
788
789 #[test]
790 fn test_pattern_type_debug() {
791 let patterns = [
792 PatternType::Structuring,
793 PatternType::Layering,
794 PatternType::Integration,
795 PatternType::Velocity,
796 PatternType::RoundNumbers,
797 ];
798 for p in &patterns {
799 let debug = format!("{:?}", p);
800 assert!(!debug.is_empty());
801 }
802 }
803
804 #[test]
805 fn test_jurisdiction_debug() {
806 let jurisdictions = [
807 Jurisdiction::US,
808 Jurisdiction::EU,
809 Jurisdiction::UK,
810 Jurisdiction::Switzerland,
811 Jurisdiction::Singapore,
812 ];
813 for j in &jurisdictions {
814 let debug = format!("{:?}", j);
815 assert!(!debug.is_empty());
816 }
817 }
818
819 #[test]
820 fn test_report_type_debug() {
821 let types = [
822 ReportType::Summary,
823 ReportType::Detailed,
824 ReportType::SAR,
825 ReportType::TravelRule,
826 ];
827 for t in &types {
828 let debug = format!("{:?}", t);
829 assert!(!debug.is_empty());
830 }
831 }
832}