scope/web/api/
compliance.rs1use crate::compliance::datasource::{BlockchainDataClient, DataSources};
4use crate::compliance::risk::RiskEngine;
5use crate::web::AppState;
6use axum::Json;
7use axum::extract::State;
8use axum::http::StatusCode;
9use axum::response::IntoResponse;
10use serde::Deserialize;
11use std::sync::Arc;
12
13#[derive(Debug, Deserialize)]
15pub struct ComplianceRiskRequest {
16 pub address: String,
18 #[serde(default = "default_chain")]
20 pub chain: String,
21 #[serde(default)]
23 pub detailed: bool,
24}
25
26fn default_chain() -> String {
27 "ethereum".to_string()
28}
29
30pub async fn handle_risk(
32 State(_state): State<Arc<AppState>>,
33 Json(req): Json<ComplianceRiskRequest>,
34) -> impl IntoResponse {
35 let engine = if let Ok(key) = std::env::var("ETHERSCAN_API_KEY") {
37 let sources = DataSources::new(key);
38 let client = BlockchainDataClient::new(sources);
39 RiskEngine::with_data_client(client)
40 } else {
41 RiskEngine::new()
42 };
43
44 match engine.assess_address(&req.address, &req.chain).await {
45 Ok(assessment) => Json(serde_json::json!({
46 "address": assessment.address,
47 "chain": assessment.chain,
48 "overall_score": assessment.overall_score,
49 "risk_level": format!("{:?}", assessment.risk_level),
50 "factors": assessment.factors.iter().map(|f| {
51 serde_json::json!({
52 "name": f.name,
53 "weight": f.weight,
54 "score": f.score,
55 "description": f.description,
56 })
57 }).collect::<Vec<_>>(),
58 }))
59 .into_response(),
60 Err(e) => (
61 StatusCode::INTERNAL_SERVER_ERROR,
62 Json(serde_json::json!({ "error": e.to_string() })),
63 )
64 .into_response(),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn test_deserialize_full() {
74 let json = serde_json::json!({
75 "address": "0x1234567890123456789012345678901234567890",
76 "chain": "polygon",
77 "detailed": true
78 });
79 let req: ComplianceRiskRequest = serde_json::from_value(json).unwrap();
80 assert_eq!(req.address, "0x1234567890123456789012345678901234567890");
81 assert_eq!(req.chain, "polygon");
82 assert!(req.detailed);
83 }
84
85 #[test]
86 fn test_deserialize_minimal() {
87 let json = serde_json::json!({
88 "address": "0x1234567890123456789012345678901234567890"
89 });
90 let req: ComplianceRiskRequest = serde_json::from_value(json).unwrap();
91 assert_eq!(req.address, "0x1234567890123456789012345678901234567890");
92 assert_eq!(req.chain, "ethereum");
93 assert!(!req.detailed);
94 }
95
96 #[test]
97 fn test_default_chain() {
98 assert_eq!(default_chain(), "ethereum");
99 }
100
101 #[test]
102 fn test_detailed_flag() {
103 let json = serde_json::json!({
104 "address": "0x1234567890123456789012345678901234567890",
105 "detailed": true
106 });
107 let req: ComplianceRiskRequest = serde_json::from_value(json).unwrap();
108 assert!(req.detailed);
109
110 let json_false = serde_json::json!({
111 "address": "0x1234567890123456789012345678901234567890",
112 "detailed": false
113 });
114 let req_false: ComplianceRiskRequest = serde_json::from_value(json_false).unwrap();
115 assert!(!req_false.detailed);
116 }
117
118 #[tokio::test]
119 async fn test_handle_risk_direct() {
120 use crate::chains::DefaultClientFactory;
121 use crate::config::Config;
122 use crate::web::AppState;
123 use axum::extract::State;
124 use axum::response::IntoResponse;
125
126 let config = Config::default();
127 let factory = DefaultClientFactory {
128 chains_config: config.chains.clone(),
129 };
130 let state = std::sync::Arc::new(AppState { config, factory });
131 let req = ComplianceRiskRequest {
132 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
133 chain: "ethereum".to_string(),
134 detailed: true,
135 };
136 let response = handle_risk(State(state), axum::Json(req))
137 .await
138 .into_response();
139 let status = response.status();
140 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
141 }
142}