Skip to main content

scope/web/api/
address.rs

1//! Address analysis API handler.
2
3use crate::chains::ChainClientFactory;
4use crate::cli::address::{self, AddressArgs};
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 address analysis.
14#[derive(Debug, Deserialize)]
15pub struct AddressRequest {
16    /// Blockchain address to analyze.
17    pub address: String,
18    /// Target chain (default: "ethereum").
19    #[serde(default = "default_chain")]
20    pub chain: String,
21    /// Include transaction history.
22    #[serde(default)]
23    pub include_txs: bool,
24    /// Include token balances.
25    #[serde(default)]
26    pub include_tokens: bool,
27    /// Max transactions to retrieve.
28    #[serde(default = "default_limit")]
29    pub limit: u32,
30    /// Generate dossier (address + risk).
31    #[serde(default)]
32    pub dossier: bool,
33}
34
35fn default_chain() -> String {
36    "ethereum".to_string()
37}
38
39fn default_limit() -> u32 {
40    100
41}
42
43/// POST /api/address — Analyze a blockchain address.
44pub async fn handle(
45    State(state): State<Arc<AppState>>,
46    Json(req): Json<AddressRequest>,
47) -> impl IntoResponse {
48    let args = AddressArgs {
49        address: req.address,
50        chain: req.chain,
51        format: None,
52        include_txs: req.include_txs,
53        include_tokens: req.include_tokens,
54        limit: req.limit,
55        report: None,
56        dossier: req.dossier,
57    };
58
59    let client: Box<dyn crate::chains::ChainClient> =
60        match state.factory.create_chain_client(&args.chain) {
61            Ok(c) => c,
62            Err(e) => {
63                return (
64                    StatusCode::BAD_REQUEST,
65                    Json(serde_json::json!({ "error": e.to_string() })),
66                )
67                    .into_response();
68            }
69        };
70
71    match address::analyze_address(&args, client.as_ref()).await {
72        Ok(report) => Json(serde_json::json!(report)).into_response(),
73        Err(e) => (
74            StatusCode::INTERNAL_SERVER_ERROR,
75            Json(serde_json::json!({ "error": e.to_string() })),
76        )
77            .into_response(),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_deserialize_full() {
87        let json = serde_json::json!({
88            "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
89            "chain": "polygon",
90            "include_txs": true,
91            "include_tokens": true,
92            "limit": 50,
93            "dossier": true
94        });
95        let req: AddressRequest = serde_json::from_value(json).unwrap();
96        assert_eq!(req.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
97        assert_eq!(req.chain, "polygon");
98        assert!(req.include_txs);
99        assert!(req.include_tokens);
100        assert_eq!(req.limit, 50);
101        assert!(req.dossier);
102    }
103
104    #[test]
105    fn test_deserialize_minimal() {
106        let json = serde_json::json!({
107            "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"
108        });
109        let req: AddressRequest = serde_json::from_value(json).unwrap();
110        assert_eq!(req.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
111        assert_eq!(req.chain, "ethereum");
112        assert!(!req.include_txs);
113        assert!(!req.include_tokens);
114        assert_eq!(req.limit, 100);
115        assert!(!req.dossier);
116    }
117
118    #[test]
119    fn test_defaults() {
120        assert_eq!(default_chain(), "ethereum");
121        assert_eq!(default_limit(), 100);
122    }
123
124    #[test]
125    fn test_deserialize_with_options() {
126        let json = serde_json::json!({
127            "address": "0x1234567890123456789012345678901234567890",
128            "include_txs": true,
129            "dossier": true
130        });
131        let req: AddressRequest = serde_json::from_value(json).unwrap();
132        assert_eq!(req.address, "0x1234567890123456789012345678901234567890");
133        assert_eq!(req.chain, "ethereum");
134        assert!(req.include_txs);
135        assert!(!req.include_tokens);
136        assert_eq!(req.limit, 100);
137        assert!(req.dossier);
138    }
139
140    #[tokio::test]
141    async fn test_handle_address_direct() {
142        use crate::chains::DefaultClientFactory;
143        use crate::config::Config;
144        use crate::web::AppState;
145        use axum::extract::State;
146        use axum::response::IntoResponse;
147
148        let config = Config::default();
149        let factory = DefaultClientFactory {
150            chains_config: config.chains.clone(),
151        };
152        let state = std::sync::Arc::new(AppState { config, factory });
153        let req = AddressRequest {
154            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
155            chain: "ethereum".to_string(),
156            include_txs: false,
157            include_tokens: true,
158            limit: 10,
159            dossier: false,
160        };
161        let response = handle(State(state), axum::Json(req)).await.into_response();
162        // Will likely return error (no API key) or success
163        let status = response.status();
164        assert!(status.is_success() || status.is_client_error() || status.is_server_error());
165    }
166}