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 None,
127 )
128 .await
129 {
130 Ok(analytics) => Json(serde_json::json!({
131 "target_info": target_type,
132 "data": analytics,
133 }))
134 .into_response(),
135 Err(e) => (
136 StatusCode::INTERNAL_SERVER_ERROR,
137 Json(serde_json::json!({ "error": e.to_string() })),
138 )
139 .into_response(),
140 }
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_deserialize_full() {
151 let json = serde_json::json!({
152 "target": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
153 "chain": "polygon",
154 "decode": true,
155 "trace": true
156 });
157 let req: InsightsRequest = serde_json::from_value(json).unwrap();
158 assert_eq!(req.target, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
159 assert_eq!(req.chain, Some("polygon".to_string()));
160 assert!(req.decode);
161 assert!(req.trace);
162 }
163
164 #[test]
165 fn test_deserialize_minimal() {
166 let json = serde_json::json!({
167 "target": "0x1234567890123456789012345678901234567890"
168 });
169 let req: InsightsRequest = serde_json::from_value(json).unwrap();
170 assert_eq!(req.target, "0x1234567890123456789012345678901234567890");
171 assert_eq!(req.chain, None);
172 assert!(!req.decode);
173 assert!(!req.trace);
174 }
175
176 #[test]
177 fn test_with_chain_override() {
178 let json = serde_json::json!({
179 "target": "USDC",
180 "chain": "ethereum"
181 });
182 let req: InsightsRequest = serde_json::from_value(json).unwrap();
183 assert_eq!(req.target, "USDC");
184 assert_eq!(req.chain, Some("ethereum".to_string()));
185 assert!(!req.decode);
186 assert!(!req.trace);
187 }
188
189 #[test]
190 fn test_flags() {
191 let json_decode = serde_json::json!({
192 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
193 "decode": true,
194 "trace": false
195 });
196 let req_decode: InsightsRequest = serde_json::from_value(json_decode).unwrap();
197 assert!(req_decode.decode);
198 assert!(!req_decode.trace);
199
200 let json_trace = serde_json::json!({
201 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
202 "decode": false,
203 "trace": true
204 });
205 let req_trace: InsightsRequest = serde_json::from_value(json_trace).unwrap();
206 assert!(!req_trace.decode);
207 assert!(req_trace.trace);
208
209 let json_both = serde_json::json!({
210 "target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
211 "decode": true,
212 "trace": true
213 });
214 let req_both: InsightsRequest = serde_json::from_value(json_both).unwrap();
215 assert!(req_both.decode);
216 assert!(req_both.trace);
217 }
218
219 #[tokio::test]
220 async fn test_handle_insights_address() {
221 use crate::chains::DefaultClientFactory;
222 use crate::config::Config;
223 use crate::web::AppState;
224 use axum::extract::State;
225 use axum::response::IntoResponse;
226
227 let config = Config::default();
228 let factory = DefaultClientFactory {
229 chains_config: config.chains.clone(),
230 };
231 let state = std::sync::Arc::new(AppState { config, factory });
232 let req = InsightsRequest {
233 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
234 chain: None,
235 decode: false,
236 trace: false,
237 };
238 let response = handle(State(state), axum::Json(req)).await.into_response();
239 let status = response.status();
240 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
241 }
242
243 #[tokio::test]
244 async fn test_handle_insights_token() {
245 use crate::chains::DefaultClientFactory;
246 use crate::config::Config;
247 use crate::web::AppState;
248 use axum::extract::State;
249 use axum::response::IntoResponse;
250
251 let config = Config::default();
252 let factory = DefaultClientFactory {
253 chains_config: config.chains.clone(),
254 };
255 let state = std::sync::Arc::new(AppState { config, factory });
256 let req = InsightsRequest {
257 target: "USDC".to_string(),
258 chain: Some("ethereum".to_string()),
259 decode: false,
260 trace: false,
261 };
262 let response = handle(State(state), axum::Json(req)).await.into_response();
263 let status = response.status();
264 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
265 }
266
267 #[tokio::test]
268 async fn test_handle_insights_tx() {
269 use crate::chains::DefaultClientFactory;
270 use crate::config::Config;
271 use crate::web::AppState;
272 use axum::extract::State;
273 use axum::response::IntoResponse;
274
275 let config = Config::default();
276 let factory = DefaultClientFactory {
277 chains_config: config.chains.clone(),
278 };
279 let state = std::sync::Arc::new(AppState { config, factory });
280 let req = InsightsRequest {
281 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
282 .to_string(),
283 chain: None,
284 decode: true,
285 trace: false,
286 };
287 let response = handle(State(state), axum::Json(req)).await.into_response();
288 let status = response.status();
289 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
290 }
291}