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 http: std::sync::Arc<dyn crate::http::HttpClient> =
168 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
169 let factory = DefaultClientFactory {
170 chains_config: config.chains.clone(),
171 http,
172 };
173 let state = std::sync::Arc::new(AppState { config, factory });
174 let req = AddressRequest {
175 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
176 chain: "ethereum".to_string(),
177 include_txs: false,
178 include_tokens: true,
179 limit: 10,
180 dossier: false,
181 };
182 let response = handle(State(state), axum::Json(req)).await.into_response();
183 let status = response.status();
185 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
186 }
187
188 #[tokio::test]
189 async fn test_handle_address_unsupported_chain_bad_request() {
190 use crate::chains::DefaultClientFactory;
191 use crate::config::Config;
192 use crate::web::AppState;
193 use axum::extract::State;
194 use axum::http::StatusCode;
195 use axum::response::IntoResponse;
196
197 let tmp = tempfile::tempdir().unwrap();
199 let config = Config {
200 address_book: crate::config::AddressBookConfig {
201 data_dir: Some(tmp.path().to_path_buf()),
202 },
203 ..Default::default()
204 };
205 let http: std::sync::Arc<dyn crate::http::HttpClient> =
206 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
207 let factory = DefaultClientFactory {
208 chains_config: config.chains.clone(),
209 http,
210 };
211 let state = std::sync::Arc::new(AppState { config, factory });
212 let req = AddressRequest {
213 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
214 chain: "bitcoin".to_string(),
215 include_txs: false,
216 include_tokens: true,
217 limit: 10,
218 dossier: false,
219 };
220 let response = handle(State(state), axum::Json(req)).await.into_response();
221 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
222 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
223 .await
224 .unwrap();
225 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
226 assert!(
227 json["error"]
228 .as_str()
229 .unwrap()
230 .contains("Unsupported chain")
231 );
232 }
233
234 #[tokio::test]
235 async fn test_handle_address_book_label_not_found() {
236 use crate::chains::DefaultClientFactory;
237 use crate::config::Config;
238 use crate::web::AppState;
239 use axum::extract::State;
240 use axum::http::StatusCode;
241 use axum::response::IntoResponse;
242
243 let tmp = tempfile::tempdir().unwrap();
244 let config = Config {
245 address_book: crate::config::AddressBookConfig {
246 data_dir: Some(tmp.path().to_path_buf()),
247 },
248 ..Default::default()
249 };
250 let http: std::sync::Arc<dyn crate::http::HttpClient> =
251 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
252 let factory = DefaultClientFactory {
253 chains_config: config.chains.clone(),
254 http,
255 };
256 let state = std::sync::Arc::new(AppState { config, factory });
257 let req = AddressRequest {
258 address: "@nonexistent-label".to_string(),
259 chain: "ethereum".to_string(),
260 include_txs: false,
261 include_tokens: false,
262 limit: 10,
263 dossier: false,
264 };
265 let response = handle(State(state), axum::Json(req)).await.into_response();
266 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
267 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
268 .await
269 .unwrap();
270 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
271 assert!(
272 json["error"]
273 .as_str()
274 .unwrap()
275 .contains("@nonexistent-label")
276 );
277 }
278}