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(
33 State(state): State<Arc<AppState>>,
34 Json(req): Json<InsightsRequest>,
35) -> impl IntoResponse {
36 let target = insights::infer_target(&req.target, req.chain.as_deref());
37
38 let target_type = match &target {
39 insights::InferredTarget::Address { chain } => {
40 serde_json::json!({ "type": "address", "chain": chain })
41 }
42 insights::InferredTarget::Transaction { chain } => {
43 serde_json::json!({ "type": "transaction", "chain": chain })
44 }
45 insights::InferredTarget::Token { chain } => {
46 serde_json::json!({ "type": "token", "chain": chain })
47 }
48 };
49
50 let args = InsightsArgs {
53 target: req.target.clone(),
54 chain: req.chain,
55 decode: req.decode,
56 trace: req.trace,
57 };
58
59 match &target {
62 insights::InferredTarget::Address { chain } => {
63 let addr_args = crate::cli::address::AddressArgs {
64 address: req.target,
65 chain: chain.clone(),
66 format: None,
67 include_txs: false,
68 include_tokens: true,
69 limit: 10,
70 report: None,
71 dossier: false,
72 };
73 let client: Box<dyn crate::chains::ChainClient> =
74 match state.factory.create_chain_client(chain) {
75 Ok(c) => c,
76 Err(e) => {
77 return (
78 StatusCode::BAD_REQUEST,
79 Json(serde_json::json!({ "error": e.to_string() })),
80 )
81 .into_response();
82 }
83 };
84 match crate::cli::address::analyze_address(&addr_args, client.as_ref()).await {
85 Ok(report) => Json(serde_json::json!({
86 "target_info": target_type,
87 "data": report,
88 }))
89 .into_response(),
90 Err(e) => (
91 StatusCode::INTERNAL_SERVER_ERROR,
92 Json(serde_json::json!({ "error": e.to_string() })),
93 )
94 .into_response(),
95 }
96 }
97 insights::InferredTarget::Transaction { chain } => {
98 match crate::cli::tx::fetch_transaction_report(
99 &req.target,
100 chain,
101 args.decode,
102 args.trace,
103 &state.factory,
104 )
105 .await
106 {
107 Ok(report) => Json(serde_json::json!({
108 "target_info": target_type,
109 "data": report,
110 }))
111 .into_response(),
112 Err(e) => (
113 StatusCode::INTERNAL_SERVER_ERROR,
114 Json(serde_json::json!({ "error": e.to_string() })),
115 )
116 .into_response(),
117 }
118 }
119 insights::InferredTarget::Token { chain } => {
120 match crate::cli::crawl::fetch_analytics_for_input(
121 &req.target,
122 chain,
123 crate::cli::crawl::Period::Hour24,
124 10,
125 &state.factory,
126 )
127 .await
128 {
129 Ok(analytics) => Json(serde_json::json!({
130 "target_info": target_type,
131 "data": analytics,
132 }))
133 .into_response(),
134 Err(e) => (
135 StatusCode::INTERNAL_SERVER_ERROR,
136 Json(serde_json::json!({ "error": e.to_string() })),
137 )
138 .into_response(),
139 }
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_deserialize_full() {
150 let json = serde_json::json!({
151 "target": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
152 "chain": "polygon",
153 "decode": true,
154 "trace": true
155 });
156 let req: InsightsRequest = serde_json::from_value(json).unwrap();
157 assert_eq!(req.target, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
158 assert_eq!(req.chain, Some("polygon".to_string()));
159 assert!(req.decode);
160 assert!(req.trace);
161 }
162
163 #[test]
164 fn test_deserialize_minimal() {
165 let json = serde_json::json!({
166 "target": "0x1234567890123456789012345678901234567890"
167 });
168 let req: InsightsRequest = serde_json::from_value(json).unwrap();
169 assert_eq!(req.target, "0x1234567890123456789012345678901234567890");
170 assert_eq!(req.chain, None);
171 assert!(!req.decode);
172 assert!(!req.trace);
173 }
174
175 #[test]
176 fn test_with_chain_override() {
177 let json = serde_json::json!({
178 "target": "USDC",
179 "chain": "ethereum"
180 });
181 let req: InsightsRequest = serde_json::from_value(json).unwrap();
182 assert_eq!(req.target, "USDC");
183 assert_eq!(req.chain, Some("ethereum".to_string()));
184 assert!(!req.decode);
185 assert!(!req.trace);
186 }
187
188 #[test]
189 fn test_flags() {
190 let json_decode = serde_json::json!({
191 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
192 "decode": true,
193 "trace": false
194 });
195 let req_decode: InsightsRequest = serde_json::from_value(json_decode).unwrap();
196 assert!(req_decode.decode);
197 assert!(!req_decode.trace);
198
199 let json_trace = serde_json::json!({
200 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
201 "decode": false,
202 "trace": true
203 });
204 let req_trace: InsightsRequest = serde_json::from_value(json_trace).unwrap();
205 assert!(!req_trace.decode);
206 assert!(req_trace.trace);
207
208 let json_both = serde_json::json!({
209 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
210 "decode": true,
211 "trace": true
212 });
213 let req_both: InsightsRequest = serde_json::from_value(json_both).unwrap();
214 assert!(req_both.decode);
215 assert!(req_both.trace);
216 }
217
218 #[tokio::test]
219 async fn test_handle_insights_address() {
220 use crate::chains::DefaultClientFactory;
221 use crate::config::Config;
222 use crate::web::AppState;
223 use axum::extract::State;
224 use axum::response::IntoResponse;
225
226 let config = Config::default();
227 let factory = DefaultClientFactory {
228 chains_config: config.chains.clone(),
229 };
230 let state = std::sync::Arc::new(AppState { config, factory });
231 let req = InsightsRequest {
232 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
233 chain: None,
234 decode: false,
235 trace: false,
236 };
237 let response = handle(State(state), axum::Json(req)).await.into_response();
238 let status = response.status();
239 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
240 }
241
242 #[tokio::test]
243 async fn test_handle_insights_token() {
244 use crate::chains::DefaultClientFactory;
245 use crate::config::Config;
246 use crate::web::AppState;
247 use axum::extract::State;
248 use axum::response::IntoResponse;
249
250 let config = Config::default();
251 let factory = DefaultClientFactory {
252 chains_config: config.chains.clone(),
253 };
254 let state = std::sync::Arc::new(AppState { config, factory });
255 let req = InsightsRequest {
256 target: "USDC".to_string(),
257 chain: Some("ethereum".to_string()),
258 decode: false,
259 trace: false,
260 };
261 let response = handle(State(state), axum::Json(req)).await.into_response();
262 let status = response.status();
263 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
264 }
265
266 #[tokio::test]
267 async fn test_handle_insights_tx() {
268 use crate::chains::DefaultClientFactory;
269 use crate::config::Config;
270 use crate::web::AppState;
271 use axum::extract::State;
272 use axum::response::IntoResponse;
273
274 let config = Config::default();
275 let factory = DefaultClientFactory {
276 chains_config: config.chains.clone(),
277 };
278 let state = std::sync::Arc::new(AppState { config, factory });
279 let req = InsightsRequest {
280 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
281 .to_string(),
282 chain: None,
283 decode: true,
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}