Skip to main content

scope/web/api/
compliance.rs

1//! Compliance risk assessment API handler.
2
3use 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/// Request body for compliance risk analysis.
14#[derive(Debug, Deserialize)]
15pub struct ComplianceRiskRequest {
16    /// Address to assess.
17    pub address: String,
18    /// Chain (default: "ethereum").
19    #[serde(default = "default_chain")]
20    pub chain: String,
21    /// Include detailed breakdown.
22    #[serde(default)]
23    pub detailed: bool,
24}
25
26fn default_chain() -> String {
27    "ethereum".to_string()
28}
29
30/// POST /api/compliance/risk — Risk assessment for an address.
31pub async fn handle_risk(
32    State(_state): State<Arc<AppState>>,
33    Json(req): Json<ComplianceRiskRequest>,
34) -> impl IntoResponse {
35    // Build risk engine (with Etherscan key if available)
36    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}