1use crate::chains::ChainClientFactory;
4use crate::cli::insights::{self, InsightsArgs};
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 InsightsRequest {
16 pub target: String,
18 pub chain: Option<String>,
20 #[serde(default)]
22 pub decode: bool,
23 #[serde(default)]
25 pub trace: bool,
26}
27
28pub async fn handle(
37 State(state): State<Arc<AppState>>,
38 Json(req): Json<InsightsRequest>,
39) -> impl IntoResponse {
40 let resolved = match super::resolve_address_book(&req.target, &state.config) {
42 Ok(r) => r,
43 Err(e) => {
44 return (
45 StatusCode::BAD_REQUEST,
46 Json(serde_json::json!({ "error": e })),
47 )
48 .into_response();
49 }
50 };
51 let target_str = resolved.value;
52 let chain_override = req.chain.or(resolved.chain);
53
54 let target = insights::infer_target(&target_str, chain_override.as_deref());
55
56 let target_type = match &target {
57 insights::InferredTarget::Address { chain } => {
58 serde_json::json!({ "type": "address", "chain": chain })
59 }
60 insights::InferredTarget::Transaction { chain } => {
61 serde_json::json!({ "type": "transaction", "chain": chain })
62 }
63 insights::InferredTarget::Token { chain } => {
64 serde_json::json!({ "type": "token", "chain": chain })
65 }
66 };
67
68 let args = InsightsArgs {
71 target: target_str.clone(),
72 chain: chain_override,
73 decode: req.decode,
74 trace: req.trace,
75 };
76
77 match &target {
80 insights::InferredTarget::Address { chain } => {
81 let addr_args = crate::cli::address::AddressArgs {
82 address: target_str.clone(),
83 chain: chain.clone(),
84 format: None,
85 include_txs: false,
86 include_tokens: true,
87 limit: 10,
88 report: None,
89 dossier: false,
90 };
91 let client: Box<dyn crate::chains::ChainClient> =
92 match state.factory.create_chain_client(chain) {
93 Ok(c) => c,
94 Err(e) => {
95 return (
96 StatusCode::BAD_REQUEST,
97 Json(serde_json::json!({ "error": e.to_string() })),
98 )
99 .into_response();
100 }
101 };
102 match crate::cli::address::analyze_address(&addr_args, client.as_ref()).await {
103 Ok(report) => Json(serde_json::json!({
104 "target_info": target_type,
105 "data": report,
106 }))
107 .into_response(),
108 Err(e) => (
109 StatusCode::INTERNAL_SERVER_ERROR,
110 Json(serde_json::json!({ "error": e.to_string() })),
111 )
112 .into_response(),
113 }
114 }
115 insights::InferredTarget::Transaction { chain } => {
116 match crate::cli::tx::fetch_transaction_report(
117 &target_str,
118 chain,
119 args.decode,
120 args.trace,
121 &state.factory,
122 )
123 .await
124 {
125 Ok(report) => Json(serde_json::json!({
126 "target_info": target_type,
127 "data": report,
128 }))
129 .into_response(),
130 Err(e) => (
131 StatusCode::INTERNAL_SERVER_ERROR,
132 Json(serde_json::json!({ "error": e.to_string() })),
133 )
134 .into_response(),
135 }
136 }
137 insights::InferredTarget::Token { chain } => {
138 match crate::cli::crawl::fetch_analytics_for_input(
139 &target_str,
140 chain,
141 crate::cli::crawl::Period::Hour24,
142 10,
143 &state.factory,
144 None,
145 )
146 .await
147 {
148 Ok(analytics) => Json(serde_json::json!({
149 "target_info": target_type,
150 "data": analytics,
151 }))
152 .into_response(),
153 Err(e) => (
154 StatusCode::INTERNAL_SERVER_ERROR,
155 Json(serde_json::json!({ "error": e.to_string() })),
156 )
157 .into_response(),
158 }
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_deserialize_full() {
169 let json = serde_json::json!({
170 "target": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
171 "chain": "polygon",
172 "decode": true,
173 "trace": true
174 });
175 let req: InsightsRequest = serde_json::from_value(json).unwrap();
176 assert_eq!(req.target, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
177 assert_eq!(req.chain, Some("polygon".to_string()));
178 assert!(req.decode);
179 assert!(req.trace);
180 }
181
182 #[test]
183 fn test_deserialize_minimal() {
184 let json = serde_json::json!({
185 "target": "0x1234567890123456789012345678901234567890"
186 });
187 let req: InsightsRequest = serde_json::from_value(json).unwrap();
188 assert_eq!(req.target, "0x1234567890123456789012345678901234567890");
189 assert_eq!(req.chain, None);
190 assert!(!req.decode);
191 assert!(!req.trace);
192 }
193
194 #[test]
195 fn test_with_chain_override() {
196 let json = serde_json::json!({
197 "target": "USDC",
198 "chain": "ethereum"
199 });
200 let req: InsightsRequest = serde_json::from_value(json).unwrap();
201 assert_eq!(req.target, "USDC");
202 assert_eq!(req.chain, Some("ethereum".to_string()));
203 assert!(!req.decode);
204 assert!(!req.trace);
205 }
206
207 #[test]
208 fn test_flags() {
209 let json_decode = serde_json::json!({
210 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
211 "decode": true,
212 "trace": false
213 });
214 let req_decode: InsightsRequest = serde_json::from_value(json_decode).unwrap();
215 assert!(req_decode.decode);
216 assert!(!req_decode.trace);
217
218 let json_trace = serde_json::json!({
219 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
220 "decode": false,
221 "trace": true
222 });
223 let req_trace: InsightsRequest = serde_json::from_value(json_trace).unwrap();
224 assert!(!req_trace.decode);
225 assert!(req_trace.trace);
226
227 let json_both = serde_json::json!({
228 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
229 "decode": true,
230 "trace": true
231 });
232 let req_both: InsightsRequest = serde_json::from_value(json_both).unwrap();
233 assert!(req_both.decode);
234 assert!(req_both.trace);
235 }
236
237 #[tokio::test]
238 async fn test_handle_insights_address() {
239 use crate::chains::DefaultClientFactory;
240 use crate::config::Config;
241 use crate::web::AppState;
242 use axum::extract::State;
243 use axum::response::IntoResponse;
244
245 let config = Config::default();
246 let http: std::sync::Arc<dyn crate::http::HttpClient> =
247 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
248 let factory = DefaultClientFactory {
249 chains_config: config.chains.clone(),
250 http,
251 };
252 let state = std::sync::Arc::new(AppState { config, factory });
253 let req = InsightsRequest {
254 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
255 chain: None,
256 decode: false,
257 trace: false,
258 };
259 let response = handle(State(state), axum::Json(req)).await.into_response();
260 let status = response.status();
261 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
262 }
263
264 #[tokio::test]
265 async fn test_handle_insights_token() {
266 use crate::chains::DefaultClientFactory;
267 use crate::config::Config;
268 use crate::web::AppState;
269 use axum::extract::State;
270 use axum::response::IntoResponse;
271
272 let config = Config::default();
273 let http: std::sync::Arc<dyn crate::http::HttpClient> =
274 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
275 let factory = DefaultClientFactory {
276 chains_config: config.chains.clone(),
277 http,
278 };
279 let state = std::sync::Arc::new(AppState { config, factory });
280 let req = InsightsRequest {
281 target: "USDC".to_string(),
282 chain: Some("ethereum".to_string()),
283 decode: false,
284 trace: false,
285 };
286 let response = handle(State(state), axum::Json(req)).await.into_response();
287 let status = response.status();
288 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
289 }
290
291 #[tokio::test]
292 async fn test_handle_insights_tx() {
293 use crate::chains::DefaultClientFactory;
294 use crate::config::Config;
295 use crate::web::AppState;
296 use axum::extract::State;
297 use axum::response::IntoResponse;
298
299 let config = Config::default();
300 let http: std::sync::Arc<dyn crate::http::HttpClient> =
301 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
302 let factory = DefaultClientFactory {
303 chains_config: config.chains.clone(),
304 http,
305 };
306 let state = std::sync::Arc::new(AppState { config, factory });
307 let req = InsightsRequest {
308 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
309 .to_string(),
310 chain: None,
311 decode: true,
312 trace: false,
313 };
314 let response = handle(State(state), axum::Json(req)).await.into_response();
315 let status = response.status();
316 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
317 }
318
319 #[tokio::test]
320 async fn test_handle_insights_address_analyze_error() {
321 use crate::chains::DefaultClientFactory;
322 use crate::config::Config;
323 use crate::web::AppState;
324 use axum::extract::State;
325 use axum::http::StatusCode;
326 use axum::response::IntoResponse;
327
328 let config = Config::default();
329 let http: std::sync::Arc<dyn crate::http::HttpClient> =
330 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
331 let factory = DefaultClientFactory {
332 chains_config: config.chains.clone(),
333 http,
334 };
335 let state = std::sync::Arc::new(AppState { config, factory });
336 let req = InsightsRequest {
337 target: "0x0000000000000000000000000000000000000000".to_string(),
338 chain: Some("ethereum".to_string()),
339 decode: false,
340 trace: false,
341 };
342 let response = handle(State(state), axum::Json(req)).await.into_response();
343 if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
344 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
345 .await
346 .unwrap();
347 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
348 assert!(json.get("error").is_some());
349 }
350 }
351
352 #[tokio::test]
353 async fn test_handle_insights_tx_error() {
354 use crate::chains::DefaultClientFactory;
355 use crate::config::Config;
356 use crate::web::AppState;
357 use axum::extract::State;
358 use axum::http::StatusCode;
359 use axum::response::IntoResponse;
360
361 let config = Config::default();
362 let http: std::sync::Arc<dyn crate::http::HttpClient> =
363 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
364 let factory = DefaultClientFactory {
365 chains_config: config.chains.clone(),
366 http,
367 };
368 let state = std::sync::Arc::new(AppState { config, factory });
369 let req = InsightsRequest {
370 target: "0x0000000000000000000000000000000000000000000000000000000000000000"
371 .to_string(),
372 chain: Some("ethereum".to_string()),
373 decode: false,
374 trace: false,
375 };
376 let response = handle(State(state), axum::Json(req)).await.into_response();
377 if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
378 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
379 .await
380 .unwrap();
381 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
382 assert!(json.get("error").is_some());
383 }
384 }
385
386 #[tokio::test]
387 async fn test_handle_insights_token_error() {
388 use crate::chains::DefaultClientFactory;
389 use crate::config::Config;
390 use crate::web::AppState;
391 use axum::extract::State;
392 use axum::http::StatusCode;
393 use axum::response::IntoResponse;
394
395 let config = Config::default();
396 let http: std::sync::Arc<dyn crate::http::HttpClient> =
397 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
398 let factory = DefaultClientFactory {
399 chains_config: config.chains.clone(),
400 http,
401 };
402 let state = std::sync::Arc::new(AppState { config, factory });
403 let req = InsightsRequest {
404 target: "NONEXISTENT_TOKEN_XYZ_123".to_string(),
405 chain: Some("ethereum".to_string()),
406 decode: false,
407 trace: false,
408 };
409 let response = handle(State(state), axum::Json(req)).await.into_response();
410 if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
411 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
412 .await
413 .unwrap();
414 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
415 assert!(json.get("error").is_some());
416 }
417 }
418
419 #[tokio::test]
420 async fn test_handle_insights_unsupported_chain_bad_request() {
421 use crate::chains::DefaultClientFactory;
422 use crate::config::Config;
423 use crate::web::AppState;
424 use axum::extract::State;
425 use axum::http::StatusCode;
426 use axum::response::IntoResponse;
427
428 let tmp = tempfile::tempdir().unwrap();
430 let config = Config {
431 address_book: crate::config::AddressBookConfig {
432 data_dir: Some(tmp.path().to_path_buf()),
433 },
434 ..Default::default()
435 };
436 let http: std::sync::Arc<dyn crate::http::HttpClient> =
437 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
438 let factory = DefaultClientFactory {
439 chains_config: config.chains.clone(),
440 http,
441 };
442 let state = std::sync::Arc::new(AppState { config, factory });
443 let req = InsightsRequest {
444 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
445 chain: Some("bitcoin".to_string()), decode: false,
447 trace: false,
448 };
449 let response = handle(State(state), axum::Json(req)).await.into_response();
450 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
451 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
452 .await
453 .unwrap();
454 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
455 assert!(
456 json["error"]
457 .as_str()
458 .unwrap()
459 .contains("Unsupported chain")
460 );
461 }
462
463 #[tokio::test]
464 async fn test_handle_insights_label_not_found() {
465 use crate::chains::DefaultClientFactory;
466 use crate::config::Config;
467 use crate::web::AppState;
468 use axum::extract::State;
469 use axum::http::StatusCode;
470 use axum::response::IntoResponse;
471
472 let tmp = tempfile::tempdir().unwrap();
473 let config = Config {
474 address_book: crate::config::AddressBookConfig {
475 data_dir: Some(tmp.path().to_path_buf()),
476 },
477 ..Default::default()
478 };
479 let http: std::sync::Arc<dyn crate::http::HttpClient> =
480 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
481 let factory = DefaultClientFactory {
482 chains_config: config.chains.clone(),
483 http,
484 };
485 let state = std::sync::Arc::new(AppState { config, factory });
486 let req = InsightsRequest {
487 target: "@no-such-label".to_string(),
488 chain: None,
489 decode: false,
490 trace: false,
491 };
492 let response = handle(State(state), axum::Json(req)).await.into_response();
493 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
494 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
495 .await
496 .unwrap();
497 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
498 assert!(json["error"].as_str().unwrap().contains("@no-such-label"));
499 }
500}