1use 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#[derive(Debug, Deserialize)]
15pub struct AddressRequest {
16 pub address: String,
18 #[serde(default = "default_chain")]
20 pub chain: String,
21 #[serde(default)]
23 pub include_txs: bool,
24 #[serde(default)]
26 pub include_tokens: bool,
27 #[serde(default = "default_limit")]
29 pub limit: u32,
30 #[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
43pub async fn handle(
49 State(state): State<Arc<AppState>>,
50 Json(req): Json<AddressRequest>,
51) -> impl IntoResponse {
52 let resolved = match super::resolve_address_book(&req.address, &state.config) {
54 Ok(r) => r,
55 Err(e) => {
56 return (
57 StatusCode::BAD_REQUEST,
58 Json(serde_json::json!({ "error": e })),
59 )
60 .into_response();
61 }
62 };
63 let address = resolved.value;
64 let chain = resolved.chain.unwrap_or(req.chain);
65
66 let args = AddressArgs {
67 address,
68 chain,
69 format: None,
70 include_txs: req.include_txs,
71 include_tokens: req.include_tokens,
72 limit: req.limit,
73 report: None,
74 dossier: req.dossier,
75 };
76
77 let client: Box<dyn crate::chains::ChainClient> =
78 match state.factory.create_chain_client(&args.chain) {
79 Ok(c) => c,
80 Err(e) => {
81 return (
82 StatusCode::BAD_REQUEST,
83 Json(serde_json::json!({ "error": e.to_string() })),
84 )
85 .into_response();
86 }
87 };
88
89 match address::analyze_address(&args, client.as_ref()).await {
90 Ok(report) => Json(serde_json::json!(report)).into_response(),
91 Err(e) => (
92 StatusCode::INTERNAL_SERVER_ERROR,
93 Json(serde_json::json!({ "error": e.to_string() })),
94 )
95 .into_response(),
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_deserialize_full() {
105 let json = serde_json::json!({
106 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
107 "chain": "polygon",
108 "include_txs": true,
109 "include_tokens": true,
110 "limit": 50,
111 "dossier": true
112 });
113 let req: AddressRequest = serde_json::from_value(json).unwrap();
114 assert_eq!(req.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
115 assert_eq!(req.chain, "polygon");
116 assert!(req.include_txs);
117 assert!(req.include_tokens);
118 assert_eq!(req.limit, 50);
119 assert!(req.dossier);
120 }
121
122 #[test]
123 fn test_deserialize_minimal() {
124 let json = serde_json::json!({
125 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"
126 });
127 let req: AddressRequest = serde_json::from_value(json).unwrap();
128 assert_eq!(req.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
129 assert_eq!(req.chain, "ethereum");
130 assert!(!req.include_txs);
131 assert!(!req.include_tokens);
132 assert_eq!(req.limit, 100);
133 assert!(!req.dossier);
134 }
135
136 #[test]
137 fn test_defaults() {
138 assert_eq!(default_chain(), "ethereum");
139 assert_eq!(default_limit(), 100);
140 }
141
142 #[test]
143 fn test_deserialize_with_options() {
144 let json = serde_json::json!({
145 "address": "0x1234567890123456789012345678901234567890",
146 "include_txs": true,
147 "dossier": true
148 });
149 let req: AddressRequest = serde_json::from_value(json).unwrap();
150 assert_eq!(req.address, "0x1234567890123456789012345678901234567890");
151 assert_eq!(req.chain, "ethereum");
152 assert!(req.include_txs);
153 assert!(!req.include_tokens);
154 assert_eq!(req.limit, 100);
155 assert!(req.dossier);
156 }
157
158 #[tokio::test]
159 async fn test_handle_address_direct() {
160 use crate::chains::DefaultClientFactory;
161 use crate::config::Config;
162 use crate::web::AppState;
163 use axum::extract::State;
164 use axum::response::IntoResponse;
165
166 let config = Config::default();
167 let factory = DefaultClientFactory {
168 chains_config: config.chains.clone(),
169 };
170 let state = std::sync::Arc::new(AppState { config, factory });
171 let req = AddressRequest {
172 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
173 chain: "ethereum".to_string(),
174 include_txs: false,
175 include_tokens: true,
176 limit: 10,
177 dossier: false,
178 };
179 let response = handle(State(state), axum::Json(req)).await.into_response();
180 let status = response.status();
182 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
183 }
184
185 #[tokio::test]
186 async fn test_handle_address_unsupported_chain_bad_request() {
187 use crate::chains::DefaultClientFactory;
188 use crate::config::Config;
189 use crate::web::AppState;
190 use axum::extract::State;
191 use axum::http::StatusCode;
192 use axum::response::IntoResponse;
193
194 let tmp = tempfile::tempdir().unwrap();
196 let config = Config {
197 address_book: crate::config::AddressBookConfig {
198 data_dir: Some(tmp.path().to_path_buf()),
199 },
200 ..Default::default()
201 };
202 let factory = DefaultClientFactory {
203 chains_config: config.chains.clone(),
204 };
205 let state = std::sync::Arc::new(AppState { config, factory });
206 let req = AddressRequest {
207 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
208 chain: "bitcoin".to_string(),
209 include_txs: false,
210 include_tokens: true,
211 limit: 10,
212 dossier: false,
213 };
214 let response = handle(State(state), axum::Json(req)).await.into_response();
215 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
216 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
217 .await
218 .unwrap();
219 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
220 assert!(
221 json["error"]
222 .as_str()
223 .unwrap()
224 .contains("Unsupported chain")
225 );
226 }
227
228 #[tokio::test]
229 async fn test_handle_address_book_label_not_found() {
230 use crate::chains::DefaultClientFactory;
231 use crate::config::Config;
232 use crate::web::AppState;
233 use axum::extract::State;
234 use axum::http::StatusCode;
235 use axum::response::IntoResponse;
236
237 let tmp = tempfile::tempdir().unwrap();
238 let config = Config {
239 address_book: crate::config::AddressBookConfig {
240 data_dir: Some(tmp.path().to_path_buf()),
241 },
242 ..Default::default()
243 };
244 let factory = DefaultClientFactory {
245 chains_config: config.chains.clone(),
246 };
247 let state = std::sync::Arc::new(AppState { config, factory });
248 let req = AddressRequest {
249 address: "@nonexistent-label".to_string(),
250 chain: "ethereum".to_string(),
251 include_txs: false,
252 include_tokens: false,
253 limit: 10,
254 dossier: false,
255 };
256 let response = handle(State(state), axum::Json(req)).await.into_response();
257 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
258 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
259 .await
260 .unwrap();
261 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
262 assert!(
263 json["error"]
264 .as_str()
265 .unwrap()
266 .contains("@nonexistent-label")
267 );
268 }
269}