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 factory = DefaultClientFactory {
247 chains_config: config.chains.clone(),
248 };
249 let state = std::sync::Arc::new(AppState { config, factory });
250 let req = InsightsRequest {
251 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
252 chain: None,
253 decode: false,
254 trace: false,
255 };
256 let response = handle(State(state), axum::Json(req)).await.into_response();
257 let status = response.status();
258 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
259 }
260
261 #[tokio::test]
262 async fn test_handle_insights_token() {
263 use crate::chains::DefaultClientFactory;
264 use crate::config::Config;
265 use crate::web::AppState;
266 use axum::extract::State;
267 use axum::response::IntoResponse;
268
269 let config = Config::default();
270 let factory = DefaultClientFactory {
271 chains_config: config.chains.clone(),
272 };
273 let state = std::sync::Arc::new(AppState { config, factory });
274 let req = InsightsRequest {
275 target: "USDC".to_string(),
276 chain: Some("ethereum".to_string()),
277 decode: false,
278 trace: false,
279 };
280 let response = handle(State(state), axum::Json(req)).await.into_response();
281 let status = response.status();
282 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
283 }
284
285 #[tokio::test]
286 async fn test_handle_insights_tx() {
287 use crate::chains::DefaultClientFactory;
288 use crate::config::Config;
289 use crate::web::AppState;
290 use axum::extract::State;
291 use axum::response::IntoResponse;
292
293 let config = Config::default();
294 let factory = DefaultClientFactory {
295 chains_config: config.chains.clone(),
296 };
297 let state = std::sync::Arc::new(AppState { config, factory });
298 let req = InsightsRequest {
299 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
300 .to_string(),
301 chain: None,
302 decode: true,
303 trace: false,
304 };
305 let response = handle(State(state), axum::Json(req)).await.into_response();
306 let status = response.status();
307 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
308 }
309
310 #[tokio::test]
311 async fn test_handle_insights_address_analyze_error() {
312 use crate::chains::DefaultClientFactory;
313 use crate::config::Config;
314 use crate::web::AppState;
315 use axum::extract::State;
316 use axum::http::StatusCode;
317 use axum::response::IntoResponse;
318
319 let config = Config::default();
320 let factory = DefaultClientFactory {
321 chains_config: config.chains.clone(),
322 };
323 let state = std::sync::Arc::new(AppState { config, factory });
324 let req = InsightsRequest {
325 target: "0x0000000000000000000000000000000000000000".to_string(),
326 chain: Some("ethereum".to_string()),
327 decode: false,
328 trace: false,
329 };
330 let response = handle(State(state), axum::Json(req)).await.into_response();
331 if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
332 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
333 .await
334 .unwrap();
335 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
336 assert!(json.get("error").is_some());
337 }
338 }
339
340 #[tokio::test]
341 async fn test_handle_insights_tx_error() {
342 use crate::chains::DefaultClientFactory;
343 use crate::config::Config;
344 use crate::web::AppState;
345 use axum::extract::State;
346 use axum::http::StatusCode;
347 use axum::response::IntoResponse;
348
349 let config = Config::default();
350 let factory = DefaultClientFactory {
351 chains_config: config.chains.clone(),
352 };
353 let state = std::sync::Arc::new(AppState { config, factory });
354 let req = InsightsRequest {
355 target: "0x0000000000000000000000000000000000000000000000000000000000000000"
356 .to_string(),
357 chain: Some("ethereum".to_string()),
358 decode: false,
359 trace: false,
360 };
361 let response = handle(State(state), axum::Json(req)).await.into_response();
362 if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
363 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
364 .await
365 .unwrap();
366 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
367 assert!(json.get("error").is_some());
368 }
369 }
370
371 #[tokio::test]
372 async fn test_handle_insights_token_error() {
373 use crate::chains::DefaultClientFactory;
374 use crate::config::Config;
375 use crate::web::AppState;
376 use axum::extract::State;
377 use axum::http::StatusCode;
378 use axum::response::IntoResponse;
379
380 let config = Config::default();
381 let factory = DefaultClientFactory {
382 chains_config: config.chains.clone(),
383 };
384 let state = std::sync::Arc::new(AppState { config, factory });
385 let req = InsightsRequest {
386 target: "NONEXISTENT_TOKEN_XYZ_123".to_string(),
387 chain: Some("ethereum".to_string()),
388 decode: false,
389 trace: false,
390 };
391 let response = handle(State(state), axum::Json(req)).await.into_response();
392 if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
393 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
394 .await
395 .unwrap();
396 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
397 assert!(json.get("error").is_some());
398 }
399 }
400
401 #[tokio::test]
402 async fn test_handle_insights_unsupported_chain_bad_request() {
403 use crate::chains::DefaultClientFactory;
404 use crate::config::Config;
405 use crate::web::AppState;
406 use axum::extract::State;
407 use axum::http::StatusCode;
408 use axum::response::IntoResponse;
409
410 let tmp = tempfile::tempdir().unwrap();
412 let config = Config {
413 address_book: crate::config::AddressBookConfig {
414 data_dir: Some(tmp.path().to_path_buf()),
415 },
416 ..Default::default()
417 };
418 let factory = DefaultClientFactory {
419 chains_config: config.chains.clone(),
420 };
421 let state = std::sync::Arc::new(AppState { config, factory });
422 let req = InsightsRequest {
423 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
424 chain: Some("bitcoin".to_string()), decode: false,
426 trace: false,
427 };
428 let response = handle(State(state), axum::Json(req)).await.into_response();
429 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
430 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
431 .await
432 .unwrap();
433 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
434 assert!(
435 json["error"]
436 .as_str()
437 .unwrap()
438 .contains("Unsupported chain")
439 );
440 }
441
442 #[tokio::test]
443 async fn test_handle_insights_label_not_found() {
444 use crate::chains::DefaultClientFactory;
445 use crate::config::Config;
446 use crate::web::AppState;
447 use axum::extract::State;
448 use axum::http::StatusCode;
449 use axum::response::IntoResponse;
450
451 let tmp = tempfile::tempdir().unwrap();
452 let config = Config {
453 address_book: crate::config::AddressBookConfig {
454 data_dir: Some(tmp.path().to_path_buf()),
455 },
456 ..Default::default()
457 };
458 let factory = DefaultClientFactory {
459 chains_config: config.chains.clone(),
460 };
461 let state = std::sync::Arc::new(AppState { config, factory });
462 let req = InsightsRequest {
463 target: "@no-such-label".to_string(),
464 chain: None,
465 decode: false,
466 trace: false,
467 };
468 let response = handle(State(state), axum::Json(req)).await.into_response();
469 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
470 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
471 .await
472 .unwrap();
473 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
474 assert!(json["error"].as_str().unwrap().contains("@no-such-label"));
475 }
476}