1use crate::config::Config;
4use crate::web::AppState;
5use axum::Json;
6use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use serde::Deserialize;
10use std::sync::Arc;
11
12pub async fn handle(State(state): State<Arc<AppState>>) -> impl IntoResponse {
14 let config = &state.config;
15 let config_path = Config::config_path()
16 .map(|p| p.display().to_string())
17 .unwrap_or_else(|| "unknown".to_string());
18
19 let config_exists = Config::config_path().map(|p| p.exists()).unwrap_or(false);
20
21 let api_keys_status: serde_json::Value = serde_json::json!({
23 "etherscan": config.chains.api_keys.contains_key("etherscan") ||
24 std::env::var("ETHERSCAN_API_KEY").is_ok(),
25 "polygonscan": config.chains.api_keys.contains_key("polygonscan"),
26 "bscscan": config.chains.api_keys.contains_key("bscscan"),
27 "solscan": config.chains.api_keys.contains_key("solscan"),
28 "tronscan": config.chains.api_keys.contains_key("tronscan"),
29 });
30
31 let rpc_status = serde_json::json!({
32 "ethereum_rpc": config.chains.ethereum_rpc.is_some(),
33 "bsc_rpc": config.chains.bsc_rpc.is_some(),
34 "solana_rpc": config.chains.solana_rpc.is_some(),
35 "tron_api": config.chains.tron_api.is_some(),
36 });
37
38 Json(serde_json::json!({
39 "config_path": config_path,
40 "config_exists": config_exists,
41 "output_format": format!("{:?}", config.output.format),
42 "color_enabled": config.output.color,
43 "api_keys": api_keys_status,
44 "rpc_endpoints": rpc_status,
45 "version": crate::VERSION,
46 }))
47 .into_response()
48}
49
50#[derive(Debug, Deserialize)]
52pub struct SaveConfigRequest {
53 #[serde(default)]
55 pub api_keys: std::collections::HashMap<String, String>,
56 #[serde(default)]
58 pub rpc_endpoints: std::collections::HashMap<String, String>,
59}
60
61pub async fn handle_save(
63 State(_state): State<Arc<AppState>>,
64 Json(req): Json<SaveConfigRequest>,
65) -> impl IntoResponse {
66 let mut config = Config::load(None).unwrap_or_default();
68
69 for (key, value) in &req.api_keys {
71 if !value.is_empty() {
72 config.chains.api_keys.insert(key.clone(), value.clone());
73 }
74 }
75
76 for (key, value) in &req.rpc_endpoints {
78 if !value.is_empty() {
79 match key.as_str() {
80 "ethereum_rpc" => config.chains.ethereum_rpc = Some(value.clone()),
81 "bsc_rpc" => config.chains.bsc_rpc = Some(value.clone()),
82 "solana_rpc" => config.chains.solana_rpc = Some(value.clone()),
83 "tron_api" => config.chains.tron_api = Some(value.clone()),
84 _ => {}
85 }
86 }
87 }
88
89 let config_path = match Config::config_path() {
91 Some(p) => p,
92 None => {
93 return (
94 StatusCode::INTERNAL_SERVER_ERROR,
95 Json(serde_json::json!({ "error": "Cannot determine config path" })),
96 )
97 .into_response();
98 }
99 };
100
101 if let Some(parent) = config_path.parent()
103 && let Err(e) = std::fs::create_dir_all(parent)
104 {
105 return (
106 StatusCode::INTERNAL_SERVER_ERROR,
107 Json(serde_json::json!({ "error": format!("Failed to create config dir: {}", e) })),
108 )
109 .into_response();
110 }
111
112 match serde_yaml::to_string(&config) {
113 Ok(yaml) => {
114 if let Err(e) = std::fs::write(&config_path, yaml) {
115 return (
116 StatusCode::INTERNAL_SERVER_ERROR,
117 Json(serde_json::json!({ "error": format!("Failed to write config: {}", e) })),
118 )
119 .into_response();
120 }
121 Json(serde_json::json!({
122 "status": "saved",
123 "path": config_path.display().to_string(),
124 }))
125 .into_response()
126 }
127 Err(e) => (
128 StatusCode::INTERNAL_SERVER_ERROR,
129 Json(serde_json::json!({ "error": format!("Failed to serialize config: {}", e) })),
130 )
131 .into_response(),
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_deserialize_save_config_full() {
141 let json = serde_json::json!({
142 "api_keys": {
143 "etherscan": "test_key_123",
144 "polygonscan": "test_key_456"
145 },
146 "rpc_endpoints": {
147 "ethereum_rpc": "https://eth.example.com",
148 "bsc_rpc": "https://bsc.example.com"
149 }
150 });
151 let req: SaveConfigRequest = serde_json::from_value(json).unwrap();
152 assert_eq!(req.api_keys.len(), 2);
153 assert_eq!(
154 req.api_keys.get("etherscan"),
155 Some(&"test_key_123".to_string())
156 );
157 assert_eq!(
158 req.api_keys.get("polygonscan"),
159 Some(&"test_key_456".to_string())
160 );
161 assert_eq!(req.rpc_endpoints.len(), 2);
162 assert_eq!(
163 req.rpc_endpoints.get("ethereum_rpc"),
164 Some(&"https://eth.example.com".to_string())
165 );
166 assert_eq!(
167 req.rpc_endpoints.get("bsc_rpc"),
168 Some(&"https://bsc.example.com".to_string())
169 );
170 }
171
172 #[test]
173 fn test_deserialize_save_config_empty() {
174 let json = serde_json::json!({});
175 let req: SaveConfigRequest = serde_json::from_value(json).unwrap();
176 assert_eq!(req.api_keys.len(), 0);
177 assert_eq!(req.rpc_endpoints.len(), 0);
178 }
179
180 #[test]
181 fn test_deserialize_save_config_partial() {
182 let json = serde_json::json!({
183 "api_keys": {
184 "etherscan": "test_key_123"
185 }
186 });
187 let req: SaveConfigRequest = serde_json::from_value(json).unwrap();
188 assert_eq!(req.api_keys.len(), 1);
189 assert_eq!(
190 req.api_keys.get("etherscan"),
191 Some(&"test_key_123".to_string())
192 );
193 assert_eq!(req.rpc_endpoints.len(), 0);
194 }
195
196 #[tokio::test]
197 async fn test_handle_config_status() {
198 use crate::chains::DefaultClientFactory;
199 use crate::config::Config;
200 use crate::web::AppState;
201 use axum::extract::State;
202 use axum::response::IntoResponse;
203
204 let config = Config::default();
205 let factory = DefaultClientFactory {
206 chains_config: config.chains.clone(),
207 };
208 let state = std::sync::Arc::new(AppState { config, factory });
209 let response = handle(State(state)).await.into_response();
210 assert_eq!(response.status(), axum::http::StatusCode::OK);
211
212 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
213 .await
214 .unwrap();
215 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
216 assert!(json.get("config_path").is_some());
217 assert!(json.get("api_keys").is_some());
218 assert!(json.get("rpc_endpoints").is_some());
219 assert!(json.get("version").is_some());
220 }
221
222 #[tokio::test]
223 async fn test_handle_save_config() {
224 use crate::chains::DefaultClientFactory;
225 use crate::config::Config;
226 use crate::web::AppState;
227 use axum::extract::State;
228 use axum::response::IntoResponse;
229
230 let config = Config::default();
231 let factory = DefaultClientFactory {
232 chains_config: config.chains.clone(),
233 };
234 let state = std::sync::Arc::new(AppState { config, factory });
235 let req = SaveConfigRequest {
236 api_keys: std::collections::HashMap::new(),
237 rpc_endpoints: std::collections::HashMap::new(),
238 };
239 let response = handle_save(State(state), axum::Json(req))
240 .await
241 .into_response();
242 let status = response.status();
244 assert!(
245 status == axum::http::StatusCode::OK
246 || status == axum::http::StatusCode::INTERNAL_SERVER_ERROR
247 );
248 }
249
250 #[tokio::test]
251 async fn test_handle_save_config_with_api_keys_and_rpc() {
252 use crate::chains::DefaultClientFactory;
253 use crate::config::Config;
254 use crate::web::AppState;
255 use axum::extract::State;
256 use axum::response::IntoResponse;
257
258 let config = Config::default();
259 let factory = DefaultClientFactory {
260 chains_config: config.chains.clone(),
261 };
262 let state = std::sync::Arc::new(AppState { config, factory });
263
264 let mut api_keys = std::collections::HashMap::new();
265 api_keys.insert("etherscan".to_string(), "test_key_abc".to_string());
266 api_keys.insert("polygonscan".to_string(), "test_key_def".to_string());
267 api_keys.insert("empty_key".to_string(), "".to_string()); let mut rpc_endpoints = std::collections::HashMap::new();
270 rpc_endpoints.insert(
271 "ethereum_rpc".to_string(),
272 "https://eth.example.com".to_string(),
273 );
274 rpc_endpoints.insert("bsc_rpc".to_string(), "https://bsc.example.com".to_string());
275 rpc_endpoints.insert(
276 "solana_rpc".to_string(),
277 "https://sol.example.com".to_string(),
278 );
279 rpc_endpoints.insert(
280 "tron_api".to_string(),
281 "https://tron.example.com".to_string(),
282 );
283 rpc_endpoints.insert(
284 "unknown_key".to_string(),
285 "https://unknown.example.com".to_string(),
286 );
287 rpc_endpoints.insert("empty_rpc".to_string(), "".to_string()); let req = SaveConfigRequest {
290 api_keys,
291 rpc_endpoints,
292 };
293
294 let response = handle_save(State(state), axum::Json(req))
295 .await
296 .into_response();
297 let status = response.status();
298 assert!(
300 status == axum::http::StatusCode::OK
301 || status == axum::http::StatusCode::INTERNAL_SERVER_ERROR
302 );
303 }
304}