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 factory = DefaultClientFactory {
129 chains_config: config.chains.clone(),
130 };
131 let state = std::sync::Arc::new(AppState { config, factory });
132 let req = ContractRequest {
133 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
134 chain: "ethereum".to_string(),
135 };
136 let response = handle(State(state), axum::Json(req)).await.into_response();
137 let status = response.status();
138 assert!(
139 status.is_success()
140 || status == axum::http::StatusCode::BAD_REQUEST
141 || status.is_server_error()
142 );
143 }
144
145 #[tokio::test]
146 async fn test_handle_contract_solana_chain() {
147 use crate::chains::DefaultClientFactory;
148 use crate::config::Config;
149 use crate::web::AppState;
150 use axum::extract::State;
151 use axum::response::IntoResponse;
152
153 let config = Config::default();
154 let factory = DefaultClientFactory {
155 chains_config: config.chains.clone(),
156 };
157 let state = std::sync::Arc::new(AppState { config, factory });
158 let req = ContractRequest {
159 address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(),
160 chain: "solana".to_string(),
161 };
162 let response = handle(State(state), axum::Json(req)).await.into_response();
163 let status = response.status();
164 assert!(
165 status.is_success()
166 || status == axum::http::StatusCode::BAD_REQUEST
167 || status.is_server_error()
168 );
169 }
170
171 #[tokio::test]
172 async fn test_handle_contract_label_not_found() {
173 use crate::chains::DefaultClientFactory;
174 use crate::config::Config;
175 use crate::web::AppState;
176 use axum::extract::State;
177 use axum::http::StatusCode;
178 use axum::response::IntoResponse;
179
180 let tmp = tempfile::tempdir().unwrap();
181 let config = Config {
182 address_book: crate::config::AddressBookConfig {
183 data_dir: Some(tmp.path().to_path_buf()),
184 },
185 ..Default::default()
186 };
187 let factory = DefaultClientFactory {
188 chains_config: config.chains.clone(),
189 };
190 let state = std::sync::Arc::new(AppState { config, factory });
191 let req = ContractRequest {
192 address: "@missing-label".to_string(),
193 chain: "ethereum".to_string(),
194 };
195 let response = handle(State(state), axum::Json(req)).await.into_response();
196 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
197 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
198 .await
199 .unwrap();
200 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
201 assert!(json["error"].as_str().unwrap().contains("@missing-label"));
202 }
203}