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