scope/web/api/
contract.rs1use crate::chains::ChainClientFactory;
6use crate::contract;
7use crate::web::AppState;
8use axum::Json;
9use axum::extract::State;
10use axum::http::StatusCode;
11use axum::response::IntoResponse;
12use serde::Deserialize;
13use std::sync::Arc;
14
15#[derive(Debug, Deserialize)]
17pub struct ContractRequest {
18 pub address: String,
20 #[serde(default = "default_chain")]
22 pub chain: String,
23}
24
25fn default_chain() -> String {
26 "ethereum".to_string()
27}
28
29pub async fn handle(
34 State(state): State<Arc<AppState>>,
35 Json(req): Json<ContractRequest>,
36) -> impl IntoResponse {
37 let resolved = match super::resolve_address_book(&req.address, &state.config) {
39 Ok(r) => r,
40 Err(e) => {
41 return (
42 StatusCode::BAD_REQUEST,
43 Json(serde_json::json!({ "error": e })),
44 )
45 .into_response();
46 }
47 };
48 let address = resolved.value;
49 let chain = resolved.chain.unwrap_or(req.chain);
50
51 let client: Box<dyn crate::chains::ChainClient> =
52 match state.factory.create_chain_client(&chain) {
53 Ok(c) => c,
54 Err(e) => {
55 let err_msg = e.to_string();
56 return (
57 StatusCode::BAD_REQUEST,
58 Json(serde_json::json!({ "error": err_msg })),
59 )
60 .into_response();
61 }
62 };
63
64 let http_client = reqwest::Client::new();
65
66 match contract::analyze_contract(&address, &chain, client.as_ref(), &http_client).await {
67 Ok(analysis) => (StatusCode::OK, Json(serde_json::json!(analysis))).into_response(),
68 Err(e) => {
69 let err_msg = e.to_string();
70 (
71 StatusCode::INTERNAL_SERVER_ERROR,
72 Json(serde_json::json!({ "error": err_msg })),
73 )
74 .into_response()
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn test_contract_deserialize_full() {
85 let json = serde_json::json!({
86 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
87 "chain": "polygon"
88 });
89 let req: ContractRequest = serde_json::from_value(json).unwrap();
90 assert_eq!(req.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
91 assert_eq!(req.chain, "polygon");
92 }
93
94 #[test]
95 fn test_contract_deserialize_minimal() {
96 let json = serde_json::json!({
97 "address": "0x1234567890123456789012345678901234567890"
98 });
99 let req: ContractRequest = serde_json::from_value(json).unwrap();
100 assert_eq!(req.address, "0x1234567890123456789012345678901234567890");
101 assert_eq!(req.chain, "ethereum");
102 }
103
104 #[test]
105 fn test_contract_default_chain() {
106 assert_eq!(default_chain(), "ethereum");
107 }
108
109 #[test]
110 fn test_contract_request_debug() {
111 let req = ContractRequest {
112 address: "0xabc".to_string(),
113 chain: "ethereum".to_string(),
114 };
115 let debug = format!("{:?}", req);
116 assert!(debug.contains("ContractRequest"));
117 }
118
119 #[tokio::test]
120 async fn test_handle_contract_direct() {
121 use crate::chains::DefaultClientFactory;
122 use crate::config::Config;
123 use crate::web::AppState;
124 use axum::extract::State;
125 use axum::response::IntoResponse;
126
127 let config = Config::default();
128 let http: std::sync::Arc<dyn crate::http::HttpClient> =
129 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
130 let factory = DefaultClientFactory {
131 chains_config: config.chains.clone(),
132 http,
133 };
134 let state = std::sync::Arc::new(AppState { config, factory });
135 let req = ContractRequest {
136 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
137 chain: "ethereum".to_string(),
138 };
139 let response = handle(State(state), axum::Json(req)).await.into_response();
140 let status = response.status();
141 assert!(
142 status.is_success()
143 || status == axum::http::StatusCode::BAD_REQUEST
144 || status.is_server_error()
145 );
146 }
147
148 #[tokio::test]
149 async fn test_handle_contract_solana_chain() {
150 use crate::chains::DefaultClientFactory;
151 use crate::config::Config;
152 use crate::web::AppState;
153 use axum::extract::State;
154 use axum::response::IntoResponse;
155
156 let config = Config::default();
157 let http: std::sync::Arc<dyn crate::http::HttpClient> =
158 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
159 let factory = DefaultClientFactory {
160 chains_config: config.chains.clone(),
161 http,
162 };
163 let state = std::sync::Arc::new(AppState { config, factory });
164 let req = ContractRequest {
165 address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(),
166 chain: "solana".to_string(),
167 };
168 let response = handle(State(state), axum::Json(req)).await.into_response();
169 let status = response.status();
170 assert!(
171 status.is_success()
172 || status == axum::http::StatusCode::BAD_REQUEST
173 || status.is_server_error()
174 );
175 }
176
177 #[tokio::test]
178 async fn test_handle_contract_label_not_found() {
179 use crate::chains::DefaultClientFactory;
180 use crate::config::Config;
181 use crate::web::AppState;
182 use axum::extract::State;
183 use axum::http::StatusCode;
184 use axum::response::IntoResponse;
185
186 let tmp = tempfile::tempdir().unwrap();
187 let config = Config {
188 address_book: crate::config::AddressBookConfig {
189 data_dir: Some(tmp.path().to_path_buf()),
190 },
191 ..Default::default()
192 };
193 let http: std::sync::Arc<dyn crate::http::HttpClient> =
194 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
195 let factory = DefaultClientFactory {
196 chains_config: config.chains.clone(),
197 http,
198 };
199 let state = std::sync::Arc::new(AppState { config, factory });
200 let req = ContractRequest {
201 address: "@missing-label".to_string(),
202 chain: "ethereum".to_string(),
203 };
204 let response = handle(State(state), axum::Json(req)).await.into_response();
205 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
206 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
207 .await
208 .unwrap();
209 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
210 assert!(json["error"].as_str().unwrap().contains("@missing-label"));
211 }
212}