1use crate::config::{Config, OutputFormat};
21use crate::error::{ConfigError, Result, ScopeError};
22use clap::Args;
23use std::io::{self, BufRead, Write};
24use std::path::{Path, PathBuf};
25
26#[derive(Debug, Args)]
28pub struct SetupArgs {
29 #[arg(long, short)]
31 pub status: bool,
32
33 #[arg(long, short, value_name = "PROVIDER")]
35 pub key: Option<String>,
36
37 #[arg(long)]
39 pub reset: bool,
40}
41
42#[allow(dead_code)]
44struct ConfigItem {
45 name: &'static str,
46 description: &'static str,
47 env_var: &'static str,
48 is_set: bool,
49 value_hint: Option<String>,
50}
51
52pub async fn run(args: SetupArgs, config: &Config) -> Result<()> {
54 if args.status {
55 show_status(config);
56 return Ok(());
57 }
58
59 if args.reset {
60 return reset_config();
61 }
62
63 if let Some(ref key_name) = args.key {
64 return configure_single_key(key_name, config).await;
65 }
66
67 run_setup_wizard(config).await
69}
70
71fn show_status(config: &Config) {
73 println!();
74 println!("Scope Configuration Status");
75 println!("{}", "=".repeat(60));
76 println!();
77
78 let config_path = Config::config_path()
80 .map(|p| p.display().to_string())
81 .unwrap_or_else(|| "Not found".to_string());
82 println!("Config file: {}", config_path);
83 println!();
84
85 println!("API Keys:");
87 println!("{}", "-".repeat(60));
88
89 let api_keys = get_api_key_items(config);
90 let mut missing_keys = Vec::new();
91
92 for item in &api_keys {
93 let status = if item.is_set {
94 "✓ Set"
95 } else {
96 missing_keys.push(item.name);
97 "✗ Not set"
98 };
99 let hint = item.value_hint.as_deref().unwrap_or("");
100 let info = get_api_key_info(item.name);
101 println!(
102 " {:<15} {} {}",
103 item.name,
104 status,
105 if item.is_set { hint } else { "" }
106 );
107 println!(" Chain: {}", info.chain);
108 }
109
110 if !missing_keys.is_empty() {
112 println!();
113 println!("Where to get API keys:");
114 println!("{}", "-".repeat(60));
115 for key_name in missing_keys {
116 let info = get_api_key_info(key_name);
117 println!(" {}: {}", key_name, info.url);
118 }
119 }
120
121 println!();
122 println!("Defaults:");
123 println!("{}", "-".repeat(40));
124 println!(
125 " Chain: {}",
126 config.chains.ethereum_rpc.as_deref().unwrap_or("ethereum")
127 );
128 println!(" Output format: {:?}", config.output.format);
129 println!(
130 " Color output: {}",
131 if config.output.color {
132 "enabled"
133 } else {
134 "disabled"
135 }
136 );
137
138 println!();
139 println!("Run 'scope setup' to configure missing settings.");
140 println!("Run 'scope setup --key <provider>' to configure a specific key.");
141 println!();
142}
143
144fn get_api_key_items(config: &Config) -> Vec<ConfigItem> {
146 vec![
147 ConfigItem {
148 name: "etherscan",
149 description: "Ethereum mainnet block explorer",
150 env_var: "SCOPE_ETHERSCAN_API_KEY",
151 is_set: config.chains.api_keys.contains_key("etherscan"),
152 value_hint: config.chains.api_keys.get("etherscan").map(|k| mask_key(k)),
153 },
154 ConfigItem {
155 name: "bscscan",
156 description: "BNB Smart Chain block explorer",
157 env_var: "SCOPE_BSCSCAN_API_KEY",
158 is_set: config.chains.api_keys.contains_key("bscscan"),
159 value_hint: config.chains.api_keys.get("bscscan").map(|k| mask_key(k)),
160 },
161 ConfigItem {
162 name: "polygonscan",
163 description: "Polygon block explorer",
164 env_var: "SCOPE_POLYGONSCAN_API_KEY",
165 is_set: config.chains.api_keys.contains_key("polygonscan"),
166 value_hint: config
167 .chains
168 .api_keys
169 .get("polygonscan")
170 .map(|k| mask_key(k)),
171 },
172 ConfigItem {
173 name: "arbiscan",
174 description: "Arbitrum block explorer",
175 env_var: "SCOPE_ARBISCAN_API_KEY",
176 is_set: config.chains.api_keys.contains_key("arbiscan"),
177 value_hint: config.chains.api_keys.get("arbiscan").map(|k| mask_key(k)),
178 },
179 ConfigItem {
180 name: "basescan",
181 description: "Base block explorer",
182 env_var: "SCOPE_BASESCAN_API_KEY",
183 is_set: config.chains.api_keys.contains_key("basescan"),
184 value_hint: config.chains.api_keys.get("basescan").map(|k| mask_key(k)),
185 },
186 ConfigItem {
187 name: "optimism",
188 description: "Optimism block explorer",
189 env_var: "SCOPE_OPTIMISM_API_KEY",
190 is_set: config.chains.api_keys.contains_key("optimism"),
191 value_hint: config.chains.api_keys.get("optimism").map(|k| mask_key(k)),
192 },
193 ]
194}
195
196fn mask_key(key: &str) -> String {
198 if key.len() <= 8 {
199 return "*".repeat(key.len());
200 }
201 format!("({}...{})", &key[..4], &key[key.len() - 4..])
202}
203
204fn reset_config() -> Result<()> {
206 let config_path = Config::config_path().ok_or_else(|| {
207 ScopeError::Config(ConfigError::NotFound {
208 path: PathBuf::from("~/.config/scope/config.yaml"),
209 })
210 })?;
211 let stdin = io::stdin();
212 let stdout = io::stdout();
213 reset_config_impl(&mut stdin.lock(), &mut stdout.lock(), &config_path)
214}
215
216fn reset_config_impl(
218 reader: &mut impl BufRead,
219 writer: &mut impl Write,
220 config_path: &Path,
221) -> Result<()> {
222 if config_path.exists() {
223 write!(
224 writer,
225 "This will delete your current configuration. Continue? [y/N]: "
226 )
227 .map_err(|e| ScopeError::Io(e.to_string()))?;
228 writer.flush().map_err(|e| ScopeError::Io(e.to_string()))?;
229
230 let mut input = String::new();
231 reader
232 .read_line(&mut input)
233 .map_err(|e| ScopeError::Io(e.to_string()))?;
234
235 if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
236 writeln!(writer, "Cancelled.").map_err(|e| ScopeError::Io(e.to_string()))?;
237 return Ok(());
238 }
239
240 std::fs::remove_file(config_path).map_err(|e| ScopeError::Io(e.to_string()))?;
241 writeln!(writer, "Configuration reset to defaults.")
242 .map_err(|e| ScopeError::Io(e.to_string()))?;
243 } else {
244 writeln!(
245 writer,
246 "No configuration file found. Already using defaults."
247 )
248 .map_err(|e| ScopeError::Io(e.to_string()))?;
249 }
250
251 Ok(())
252}
253
254async fn configure_single_key(key_name: &str, config: &Config) -> Result<()> {
256 let config_path = Config::config_path().ok_or_else(|| {
257 ScopeError::Config(ConfigError::NotFound {
258 path: PathBuf::from("~/.config/scope/config.yaml"),
259 })
260 })?;
261 let stdin = io::stdin();
262 let stdout = io::stdout();
263 configure_single_key_impl(
264 &mut stdin.lock(),
265 &mut stdout.lock(),
266 key_name,
267 config,
268 &config_path,
269 )
270}
271
272fn configure_single_key_impl(
274 reader: &mut impl BufRead,
275 writer: &mut impl Write,
276 key_name: &str,
277 config: &Config,
278 config_path: &Path,
279) -> Result<()> {
280 let valid_keys = [
281 "etherscan",
282 "bscscan",
283 "polygonscan",
284 "arbiscan",
285 "basescan",
286 "optimism",
287 ];
288
289 if !valid_keys.contains(&key_name) {
290 writeln!(writer, "Unknown API key: {}", key_name)
291 .map_err(|e| ScopeError::Io(e.to_string()))?;
292 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
293 writeln!(writer, "Valid options:").map_err(|e| ScopeError::Io(e.to_string()))?;
294 for key in valid_keys {
295 let info = get_api_key_info(key);
296 writeln!(writer, " {:<15} - {}", key, info.chain)
297 .map_err(|e| ScopeError::Io(e.to_string()))?;
298 }
299 return Ok(());
300 }
301
302 let info = get_api_key_info(key_name);
303 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
304 writeln!(
305 writer,
306 "╔══════════════════════════════════════════════════════════════╗"
307 )
308 .map_err(|e| ScopeError::Io(e.to_string()))?;
309 writeln!(writer, "║ Configure {} API Key", key_name.to_uppercase())
310 .map_err(|e| ScopeError::Io(e.to_string()))?;
311 writeln!(
312 writer,
313 "╚══════════════════════════════════════════════════════════════╝"
314 )
315 .map_err(|e| ScopeError::Io(e.to_string()))?;
316 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
317 writeln!(writer, "Chain: {}", info.chain).map_err(|e| ScopeError::Io(e.to_string()))?;
318 writeln!(writer, "Enables: {}", info.features).map_err(|e| ScopeError::Io(e.to_string()))?;
319 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
320 writeln!(writer, "How to get your free API key:").map_err(|e| ScopeError::Io(e.to_string()))?;
321 writeln!(writer, " {}", info.signup_steps).map_err(|e| ScopeError::Io(e.to_string()))?;
322 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
323 writeln!(writer, "URL: {}", info.url).map_err(|e| ScopeError::Io(e.to_string()))?;
324 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
325
326 let key = prompt_api_key_impl(reader, writer, key_name)?;
327
328 if key.is_empty() {
329 writeln!(writer, "Skipped.").map_err(|e| ScopeError::Io(e.to_string()))?;
330 return Ok(());
331 }
332
333 let mut new_config = config.clone();
335 new_config.chains.api_keys.insert(key_name.to_string(), key);
336
337 save_config_to_path(&new_config, config_path)?;
338 writeln!(writer, "✓ {} API key saved.", key_name).map_err(|e| ScopeError::Io(e.to_string()))?;
339
340 Ok(())
341}
342
343struct ApiKeyInfo {
345 url: &'static str,
346 chain: &'static str,
347 features: &'static str,
348 signup_steps: &'static str,
349}
350
351fn get_api_key_info(key_name: &str) -> ApiKeyInfo {
353 match key_name {
354 "etherscan" => ApiKeyInfo {
355 url: "https://etherscan.io/apis",
356 chain: "Ethereum Mainnet",
357 features: "token balances, transactions, holders, contract verification",
358 signup_steps: "1. Visit etherscan.io/register\n 2. Create a free account\n 3. Go to API-Keys in your account\n 4. Click 'Add' to generate a new key",
359 },
360 "bscscan" => ApiKeyInfo {
361 url: "https://bscscan.com/apis",
362 chain: "BNB Smart Chain (BSC)",
363 features: "BSC token data, BEP-20 holders, transactions",
364 signup_steps: "1. Visit bscscan.com/register\n 2. Create a free account\n 3. Go to API-Keys in your account\n 4. Click 'Add' to generate a new key",
365 },
366 "polygonscan" => ApiKeyInfo {
367 url: "https://polygonscan.com/apis",
368 chain: "Polygon (MATIC)",
369 features: "Polygon token data, transactions, holders",
370 signup_steps: "1. Visit polygonscan.com/register\n 2. Create a free account\n 3. Go to API-Keys in your account\n 4. Click 'Add' to generate a new key",
371 },
372 "arbiscan" => ApiKeyInfo {
373 url: "https://arbiscan.io/apis",
374 chain: "Arbitrum One",
375 features: "Arbitrum token data, L2 transactions, holders",
376 signup_steps: "1. Visit arbiscan.io/register\n 2. Create a free account\n 3. Go to API-Keys in your account\n 4. Click 'Add' to generate a new key",
377 },
378 "basescan" => ApiKeyInfo {
379 url: "https://basescan.org/apis",
380 chain: "Base (Coinbase L2)",
381 features: "Base token data, transactions, holders",
382 signup_steps: "1. Visit basescan.org/register\n 2. Create a free account\n 3. Go to API-Keys in your account\n 4. Click 'Add' to generate a new key",
383 },
384 "optimism" => ApiKeyInfo {
385 url: "https://optimistic.etherscan.io/apis",
386 chain: "Optimism (OP Mainnet)",
387 features: "Optimism token data, L2 transactions, holders",
388 signup_steps: "1. Visit optimistic.etherscan.io/register\n 2. Create a free account\n 3. Go to API-Keys in your account\n 4. Click 'Add' to generate a new key",
389 },
390 _ => ApiKeyInfo {
391 url: "https://etherscan.io/apis",
392 chain: "Ethereum",
393 features: "blockchain data",
394 signup_steps: "Visit the provider's website to register",
395 },
396 }
397}
398
399#[cfg(test)]
401fn get_api_key_url(key_name: &str) -> &'static str {
402 get_api_key_info(key_name).url
403}
404
405async fn run_setup_wizard(config: &Config) -> Result<()> {
407 let config_path = Config::config_path().ok_or_else(|| {
408 ScopeError::Config(ConfigError::NotFound {
409 path: PathBuf::from("~/.config/scope/config.yaml"),
410 })
411 })?;
412 let stdin = io::stdin();
413 let stdout = io::stdout();
414 run_setup_wizard_impl(&mut stdin.lock(), &mut stdout.lock(), config, &config_path)
415}
416
417fn run_setup_wizard_impl(
419 reader: &mut impl BufRead,
420 writer: &mut impl Write,
421 config: &Config,
422 config_path: &Path,
423) -> Result<()> {
424 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
425 writeln!(
426 writer,
427 "╔══════════════════════════════════════════════════════════════╗"
428 )
429 .map_err(|e| ScopeError::Io(e.to_string()))?;
430 writeln!(
431 writer,
432 "║ Scope Setup Wizard ║"
433 )
434 .map_err(|e| ScopeError::Io(e.to_string()))?;
435 writeln!(
436 writer,
437 "╚══════════════════════════════════════════════════════════════╝"
438 )
439 .map_err(|e| ScopeError::Io(e.to_string()))?;
440 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
441 writeln!(
442 writer,
443 "This wizard will help you configure Scope (Blockchain Crawler CLI)."
444 )
445 .map_err(|e| ScopeError::Io(e.to_string()))?;
446 writeln!(writer, "Press Enter to skip any optional setting.")
447 .map_err(|e| ScopeError::Io(e.to_string()))?;
448 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
449
450 let mut new_config = config.clone();
451 let mut changes_made = false;
452
453 writeln!(writer, "Step 1: API Keys").map_err(|e| ScopeError::Io(e.to_string()))?;
455 writeln!(writer, "{}", "=".repeat(60)).map_err(|e| ScopeError::Io(e.to_string()))?;
456 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
457 writeln!(
458 writer,
459 "API keys enable access to block explorer data including:"
460 )
461 .map_err(|e| ScopeError::Io(e.to_string()))?;
462 writeln!(writer, " • Token balances and holder information")
463 .map_err(|e| ScopeError::Io(e.to_string()))?;
464 writeln!(writer, " • Transaction history and details")
465 .map_err(|e| ScopeError::Io(e.to_string()))?;
466 writeln!(writer, " • Contract verification status")
467 .map_err(|e| ScopeError::Io(e.to_string()))?;
468 writeln!(writer, " • Token analytics and metrics")
469 .map_err(|e| ScopeError::Io(e.to_string()))?;
470 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
471 writeln!(
472 writer,
473 "All API keys are FREE and take just a minute to obtain."
474 )
475 .map_err(|e| ScopeError::Io(e.to_string()))?;
476 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
477
478 if !config.chains.api_keys.contains_key("etherscan") {
480 let info = get_api_key_info("etherscan");
481 writeln!(
482 writer,
483 "┌────────────────────────────────────────────────────────────┐"
484 )
485 .map_err(|e| ScopeError::Io(e.to_string()))?;
486 writeln!(
487 writer,
488 "│ ETHERSCAN API KEY (Recommended) │"
489 )
490 .map_err(|e| ScopeError::Io(e.to_string()))?;
491 writeln!(
492 writer,
493 "└────────────────────────────────────────────────────────────┘"
494 )
495 .map_err(|e| ScopeError::Io(e.to_string()))?;
496 writeln!(writer, " Chain: {}", info.chain).map_err(|e| ScopeError::Io(e.to_string()))?;
497 writeln!(writer, " Enables: {}", info.features)
498 .map_err(|e| ScopeError::Io(e.to_string()))?;
499 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
500 writeln!(writer, " How to get your free API key:")
501 .map_err(|e| ScopeError::Io(e.to_string()))?;
502 writeln!(writer, " {}", info.signup_steps).map_err(|e| ScopeError::Io(e.to_string()))?;
503 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
504 writeln!(writer, " URL: {}", info.url).map_err(|e| ScopeError::Io(e.to_string()))?;
505 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
506 if let Some(key) = prompt_optional_key_impl(reader, writer, "etherscan")? {
507 new_config
508 .chains
509 .api_keys
510 .insert("etherscan".to_string(), key);
511 changes_made = true;
512 }
513 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
514 } else {
515 writeln!(writer, "✓ Etherscan API key already configured")
516 .map_err(|e| ScopeError::Io(e.to_string()))?;
517 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
518 }
519
520 write!(
522 writer,
523 "Configure API keys for other chains (BSC, Polygon, Arbitrum, etc.)? [y/N]: "
524 )
525 .map_err(|e| ScopeError::Io(e.to_string()))?;
526 writer.flush().map_err(|e| ScopeError::Io(e.to_string()))?;
527
528 let mut input = String::new();
529 reader
530 .read_line(&mut input)
531 .map_err(|e| ScopeError::Io(e.to_string()))?;
532
533 if matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
534 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
535
536 let other_chains = ["bscscan", "polygonscan", "arbiscan", "basescan", "optimism"];
537
538 for key_name in other_chains {
539 if !config.chains.api_keys.contains_key(key_name) {
540 let info = get_api_key_info(key_name);
541 writeln!(
542 writer,
543 "┌────────────────────────────────────────────────────────────┐"
544 )
545 .map_err(|e| ScopeError::Io(e.to_string()))?;
546 writeln!(writer, "│ {} API KEY", key_name.to_uppercase())
547 .map_err(|e| ScopeError::Io(e.to_string()))?;
548 writeln!(
549 writer,
550 "└────────────────────────────────────────────────────────────┘"
551 )
552 .map_err(|e| ScopeError::Io(e.to_string()))?;
553 writeln!(writer, " Chain: {}", info.chain)
554 .map_err(|e| ScopeError::Io(e.to_string()))?;
555 writeln!(writer, " Enables: {}", info.features)
556 .map_err(|e| ScopeError::Io(e.to_string()))?;
557 writeln!(writer, " URL: {}", info.url)
558 .map_err(|e| ScopeError::Io(e.to_string()))?;
559 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
560 if let Some(key) = prompt_optional_key_impl(reader, writer, key_name)? {
561 new_config.chains.api_keys.insert(key_name.to_string(), key);
562 changes_made = true;
563 }
564 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
565 }
566 }
567 }
568
569 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
571 writeln!(writer, "Step 2: Preferences").map_err(|e| ScopeError::Io(e.to_string()))?;
572 writeln!(writer, "{}", "=".repeat(60)).map_err(|e| ScopeError::Io(e.to_string()))?;
573 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
574
575 writeln!(writer, "Default output format:").map_err(|e| ScopeError::Io(e.to_string()))?;
577 writeln!(writer, " 1. table (default)").map_err(|e| ScopeError::Io(e.to_string()))?;
578 writeln!(writer, " 2. json").map_err(|e| ScopeError::Io(e.to_string()))?;
579 writeln!(writer, " 3. csv").map_err(|e| ScopeError::Io(e.to_string()))?;
580 write!(writer, "Select [1-3, Enter for default]: ")
581 .map_err(|e| ScopeError::Io(e.to_string()))?;
582 writer.flush().map_err(|e| ScopeError::Io(e.to_string()))?;
583
584 input.clear();
585 reader
586 .read_line(&mut input)
587 .map_err(|e| ScopeError::Io(e.to_string()))?;
588
589 match input.trim() {
590 "2" => {
591 new_config.output.format = OutputFormat::Json;
592 changes_made = true;
593 }
594 "3" => {
595 new_config.output.format = OutputFormat::Csv;
596 changes_made = true;
597 }
598 _ => {} }
600
601 if changes_made {
603 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
604 writeln!(writer, "Saving configuration...").map_err(|e| ScopeError::Io(e.to_string()))?;
605 save_config_to_path(&new_config, config_path)?;
606 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
607 writeln!(
608 writer,
609 "✓ Configuration saved to ~/.config/scope/config.yaml"
610 )
611 .map_err(|e| ScopeError::Io(e.to_string()))?;
612 } else {
613 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
614 writeln!(writer, "No changes made.").map_err(|e| ScopeError::Io(e.to_string()))?;
615 }
616
617 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
618 writeln!(writer, "Setup complete! You can now use Scope.")
619 .map_err(|e| ScopeError::Io(e.to_string()))?;
620 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
621 writeln!(writer, "Quick start:").map_err(|e| ScopeError::Io(e.to_string()))?;
622 writeln!(writer, " scope crawl USDC # Analyze a token")
623 .map_err(|e| ScopeError::Io(e.to_string()))?;
624 writeln!(
625 writer,
626 " scope address 0x... # Analyze an address"
627 )
628 .map_err(|e| ScopeError::Io(e.to_string()))?;
629 writeln!(
630 writer,
631 " scope insights <target> # Auto-detect and analyze"
632 )
633 .map_err(|e| ScopeError::Io(e.to_string()))?;
634 writeln!(
635 writer,
636 " scope monitor USDC # Live TUI dashboard"
637 )
638 .map_err(|e| ScopeError::Io(e.to_string()))?;
639 writeln!(writer, " scope interactive # Interactive mode")
640 .map_err(|e| ScopeError::Io(e.to_string()))?;
641 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
642 writeln!(
643 writer,
644 "Run 'scope setup --status' to view your configuration."
645 )
646 .map_err(|e| ScopeError::Io(e.to_string()))?;
647 writeln!(
648 writer,
649 "Run 'scope completions zsh > _scope' for shell tab-completion."
650 )
651 .map_err(|e| ScopeError::Io(e.to_string()))?;
652 writeln!(writer).map_err(|e| ScopeError::Io(e.to_string()))?;
653
654 Ok(())
655}
656
657fn prompt_optional_key_impl(
659 reader: &mut impl BufRead,
660 writer: &mut impl Write,
661 name: &str,
662) -> Result<Option<String>> {
663 write!(writer, " {} API key (or Enter to skip): ", name)
664 .map_err(|e| ScopeError::Io(e.to_string()))?;
665 writer.flush().map_err(|e| ScopeError::Io(e.to_string()))?;
666
667 let mut input = String::new();
668 reader
669 .read_line(&mut input)
670 .map_err(|e| ScopeError::Io(e.to_string()))?;
671
672 let key = input.trim().to_string();
673 if key.is_empty() {
674 Ok(None)
675 } else {
676 Ok(Some(key))
677 }
678}
679
680fn prompt_api_key_impl(
682 reader: &mut impl BufRead,
683 writer: &mut impl Write,
684 name: &str,
685) -> Result<String> {
686 write!(writer, "Enter {} API key: ", name).map_err(|e| ScopeError::Io(e.to_string()))?;
687 writer.flush().map_err(|e| ScopeError::Io(e.to_string()))?;
688
689 let mut input = String::new();
690 reader
691 .read_line(&mut input)
692 .map_err(|e| ScopeError::Io(e.to_string()))?;
693
694 Ok(input.trim().to_string())
695}
696
697fn save_config_to_path(config: &Config, config_path: &Path) -> Result<()> {
699 if let Some(parent) = config_path.parent() {
701 std::fs::create_dir_all(parent).map_err(|e| ScopeError::Io(e.to_string()))?;
702 }
703
704 let mut yaml = String::new();
706 yaml.push_str("# Scope Configuration\n");
707 yaml.push_str("# Generated by 'scope setup'\n\n");
708
709 yaml.push_str("chains:\n");
711
712 if !config.chains.api_keys.is_empty() {
714 yaml.push_str(" api_keys:\n");
715 for (name, key) in &config.chains.api_keys {
716 yaml.push_str(&format!(" {}: \"{}\"\n", name, key));
717 }
718 }
719
720 if let Some(ref rpc) = config.chains.ethereum_rpc {
722 yaml.push_str(&format!(" ethereum_rpc: \"{}\"\n", rpc));
723 }
724
725 yaml.push_str("\noutput:\n");
727 yaml.push_str(&format!(" format: {}\n", config.output.format));
728 yaml.push_str(&format!(" color: {}\n", config.output.color));
729
730 std::fs::write(config_path, yaml).map_err(|e| ScopeError::Io(e.to_string()))?;
731
732 Ok(())
733}
734
735#[cfg(test)]
740mod tests {
741 use super::*;
742
743 #[test]
744 fn test_mask_key_long() {
745 let masked = mask_key("ABCDEFGHIJKLMNOP");
746 assert_eq!(masked, "(ABCD...MNOP)");
747 }
748
749 #[test]
750 fn test_mask_key_short() {
751 let masked = mask_key("SHORT");
752 assert_eq!(masked, "*****");
753 }
754
755 #[test]
756 fn test_mask_key_exactly_8() {
757 let masked = mask_key("ABCDEFGH");
758 assert_eq!(masked, "********");
759 }
760
761 #[test]
762 fn test_mask_key_9_chars() {
763 let masked = mask_key("ABCDEFGHI");
764 assert_eq!(masked, "(ABCD...FGHI)");
765 }
766
767 #[test]
768 fn test_mask_key_empty() {
769 let masked = mask_key("");
770 assert_eq!(masked, "");
771 }
772
773 #[test]
774 fn test_get_api_key_url() {
775 assert!(get_api_key_url("etherscan").contains("etherscan.io"));
776 assert!(get_api_key_url("bscscan").contains("bscscan.com"));
777 }
778
779 #[test]
784 fn test_get_api_key_info_all_providers() {
785 let providers = [
786 "etherscan",
787 "bscscan",
788 "polygonscan",
789 "arbiscan",
790 "basescan",
791 "optimism",
792 ];
793 for provider in providers {
794 let info = get_api_key_info(provider);
795 assert!(
796 !info.url.is_empty(),
797 "URL should not be empty for {}",
798 provider
799 );
800 assert!(
801 !info.chain.is_empty(),
802 "Chain should not be empty for {}",
803 provider
804 );
805 assert!(
806 !info.features.is_empty(),
807 "Features should not be empty for {}",
808 provider
809 );
810 assert!(
811 !info.signup_steps.is_empty(),
812 "Signup steps should not be empty for {}",
813 provider
814 );
815 }
816 }
817
818 #[test]
819 fn test_get_api_key_info_unknown() {
820 let info = get_api_key_info("unknown_provider");
821 assert!(!info.url.is_empty());
823 }
824
825 #[test]
826 fn test_get_api_key_info_urls_correct() {
827 assert!(get_api_key_info("etherscan").url.contains("etherscan.io"));
828 assert!(get_api_key_info("bscscan").url.contains("bscscan.com"));
829 assert!(
830 get_api_key_info("polygonscan")
831 .url
832 .contains("polygonscan.com")
833 );
834 assert!(get_api_key_info("arbiscan").url.contains("arbiscan.io"));
835 assert!(get_api_key_info("basescan").url.contains("basescan.org"));
836 assert!(
837 get_api_key_info("optimism")
838 .url
839 .contains("optimistic.etherscan.io")
840 );
841 }
842
843 #[test]
848 fn test_get_api_key_items_default_config() {
849 let config = Config::default();
850 let items = get_api_key_items(&config);
851 assert_eq!(items.len(), 6);
852 for item in &items {
854 assert!(
855 !item.is_set,
856 "{} should not be set in default config",
857 item.name
858 );
859 assert!(item.value_hint.is_none());
860 }
861 }
862
863 #[test]
864 fn test_get_api_key_items_with_set_key() {
865 let mut config = Config::default();
866 config
867 .chains
868 .api_keys
869 .insert("etherscan".to_string(), "ABCDEFGHIJKLMNOP".to_string());
870 let items = get_api_key_items(&config);
871 let etherscan_item = items.iter().find(|i| i.name == "etherscan").unwrap();
872 assert!(etherscan_item.is_set);
873 assert!(etherscan_item.value_hint.is_some());
874 assert_eq!(etherscan_item.value_hint.as_ref().unwrap(), "(ABCD...MNOP)");
875 }
876
877 #[test]
882 fn test_setup_args_defaults() {
883 use clap::Parser;
884
885 #[derive(Parser)]
886 struct TestCli {
887 #[command(flatten)]
888 setup: SetupArgs,
889 }
890
891 let cli = TestCli::try_parse_from(["test"]).unwrap();
892 assert!(!cli.setup.status);
893 assert!(cli.setup.key.is_none());
894 assert!(!cli.setup.reset);
895 }
896
897 #[test]
898 fn test_setup_args_status() {
899 use clap::Parser;
900
901 #[derive(Parser)]
902 struct TestCli {
903 #[command(flatten)]
904 setup: SetupArgs,
905 }
906
907 let cli = TestCli::try_parse_from(["test", "--status"]).unwrap();
908 assert!(cli.setup.status);
909 }
910
911 #[test]
912 fn test_setup_args_key() {
913 use clap::Parser;
914
915 #[derive(Parser)]
916 struct TestCli {
917 #[command(flatten)]
918 setup: SetupArgs,
919 }
920
921 let cli = TestCli::try_parse_from(["test", "--key", "etherscan"]).unwrap();
922 assert_eq!(cli.setup.key.as_deref(), Some("etherscan"));
923 }
924
925 #[test]
926 fn test_setup_args_reset() {
927 use clap::Parser;
928
929 #[derive(Parser)]
930 struct TestCli {
931 #[command(flatten)]
932 setup: SetupArgs,
933 }
934
935 let cli = TestCli::try_parse_from(["test", "--reset"]).unwrap();
936 assert!(cli.setup.reset);
937 }
938
939 #[test]
944 fn test_show_status_no_panic() {
945 let config = Config::default();
946 show_status(&config);
947 }
948
949 #[test]
950 fn test_show_status_with_keys_no_panic() {
951 let mut config = Config::default();
952 config
953 .chains
954 .api_keys
955 .insert("etherscan".to_string(), "abc123def456".to_string());
956 config
957 .chains
958 .api_keys
959 .insert("bscscan".to_string(), "xyz".to_string());
960 show_status(&config);
961 }
962
963 #[tokio::test]
968 async fn test_run_status_mode() {
969 let config = Config::default();
970 let args = SetupArgs {
971 status: true,
972 key: None,
973 reset: false,
974 };
975 let result = run(args, &config).await;
976 assert!(result.is_ok());
977 }
978
979 #[tokio::test]
980 async fn test_run_key_unknown() {
981 let config = Config::default();
982 let args = SetupArgs {
983 status: false,
984 key: Some("nonexistent".to_string()),
985 reset: false,
986 };
987 let result = run(args, &config).await;
989 assert!(result.is_ok());
990 }
991
992 #[test]
997 fn test_show_status_with_multiple_keys() {
998 let mut config = Config::default();
999 config
1000 .chains
1001 .api_keys
1002 .insert("etherscan".to_string(), "abc123def456789".to_string());
1003 config
1004 .chains
1005 .api_keys
1006 .insert("polygonscan".to_string(), "poly_key_12345".to_string());
1007 config
1008 .chains
1009 .api_keys
1010 .insert("bscscan".to_string(), "bsc".to_string()); show_status(&config);
1012 }
1013
1014 #[test]
1015 fn test_show_status_with_all_keys() {
1016 let mut config = Config::default();
1017 for key in [
1018 "etherscan",
1019 "bscscan",
1020 "polygonscan",
1021 "arbiscan",
1022 "basescan",
1023 "optimism",
1024 ] {
1025 config
1026 .chains
1027 .api_keys
1028 .insert(key.to_string(), format!("{}_key_12345678", key));
1029 }
1030 show_status(&config);
1032 }
1033
1034 #[test]
1035 fn test_show_status_with_custom_rpc() {
1036 let mut config = Config::default();
1037 config.chains.ethereum_rpc = Some("https://custom.rpc.example.com".to_string());
1038 config.output.format = OutputFormat::Json;
1039 config.output.color = false;
1040 show_status(&config);
1041 }
1042
1043 #[test]
1044 fn test_get_api_key_items_all_set() {
1045 let mut config = Config::default();
1046 for key in [
1047 "etherscan",
1048 "bscscan",
1049 "polygonscan",
1050 "arbiscan",
1051 "basescan",
1052 "optimism",
1053 ] {
1054 config
1055 .chains
1056 .api_keys
1057 .insert(key.to_string(), format!("{}_key_12345678", key));
1058 }
1059 let items = get_api_key_items(&config);
1060 assert_eq!(items.len(), 6);
1061 for item in &items {
1062 assert!(item.is_set, "{} should be set", item.name);
1063 assert!(item.value_hint.is_some());
1064 }
1065 }
1066
1067 #[test]
1068 fn test_get_api_key_info_features_not_empty() {
1069 for key in [
1070 "etherscan",
1071 "bscscan",
1072 "polygonscan",
1073 "arbiscan",
1074 "basescan",
1075 "optimism",
1076 ] {
1077 let info = get_api_key_info(key);
1078 assert!(!info.features.is_empty());
1079 assert!(!info.signup_steps.is_empty());
1080 }
1081 }
1082
1083 #[test]
1084 fn test_save_config_creates_file() {
1085 let tmp_dir = std::env::temp_dir().join("scope_test_setup");
1086 let _ = std::fs::create_dir_all(&tmp_dir);
1087 let tmp_file = tmp_dir.join("config.yaml");
1088
1089 let mut config = Config::default();
1092 config
1093 .chains
1094 .api_keys
1095 .insert("etherscan".to_string(), "test_key_12345".to_string());
1096 config.output.format = OutputFormat::Json;
1097
1098 let mut yaml = String::new();
1100 yaml.push_str("# Scope Configuration\n");
1101 yaml.push_str("# Generated by 'scope setup'\n\n");
1102 yaml.push_str("chains:\n");
1103 if !config.chains.api_keys.is_empty() {
1104 yaml.push_str(" api_keys:\n");
1105 for (name, key) in &config.chains.api_keys {
1106 yaml.push_str(&format!(" {}: \"{}\"\n", name, key));
1107 }
1108 }
1109 yaml.push_str("\noutput:\n");
1110 yaml.push_str(&format!(" format: {}\n", config.output.format));
1111 yaml.push_str(&format!(" color: {}\n", config.output.color));
1112
1113 std::fs::write(&tmp_file, &yaml).unwrap();
1114 let content = std::fs::read_to_string(&tmp_file).unwrap();
1115 assert!(content.contains("etherscan"));
1116 assert!(content.contains("test_key_12345"));
1117 assert!(content.contains("json") || content.contains("Json"));
1118
1119 let _ = std::fs::remove_dir_all(&tmp_dir);
1120 }
1121
1122 #[test]
1123 fn test_save_config_to_temp_dir() {
1124 let temp_dir = tempfile::tempdir().unwrap();
1125 let config_path = temp_dir.path().join("scope").join("config.yaml");
1126
1127 std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
1129
1130 let config = Config::default();
1131 let yaml = serde_yaml::to_string(&config.chains).unwrap();
1132 std::fs::write(&config_path, yaml).unwrap();
1133
1134 assert!(config_path.exists());
1135 let contents = std::fs::read_to_string(&config_path).unwrap();
1136 assert!(!contents.is_empty());
1137 }
1138
1139 #[test]
1140 fn test_setup_args_reset_flag() {
1141 let args = SetupArgs {
1142 status: false,
1143 key: None,
1144 reset: true,
1145 };
1146 assert!(args.reset);
1147 }
1148
1149 #[test]
1154 fn test_prompt_api_key_impl_with_input() {
1155 let input = b"MY_SECRET_API_KEY_123\n";
1156 let mut reader = std::io::Cursor::new(&input[..]);
1157 let mut writer = Vec::new();
1158
1159 let result = prompt_api_key_impl(&mut reader, &mut writer, "etherscan").unwrap();
1160 assert_eq!(result, "MY_SECRET_API_KEY_123");
1161 let output = String::from_utf8(writer).unwrap();
1162 assert!(output.contains("Enter etherscan API key"));
1163 }
1164
1165 #[test]
1166 fn test_prompt_api_key_impl_empty_input() {
1167 let input = b"\n";
1168 let mut reader = std::io::Cursor::new(&input[..]);
1169 let mut writer = Vec::new();
1170
1171 let result = prompt_api_key_impl(&mut reader, &mut writer, "bscscan").unwrap();
1172 assert_eq!(result, "");
1173 }
1174
1175 #[test]
1176 fn test_prompt_optional_key_impl_with_key() {
1177 let input = b"my_key_12345\n";
1178 let mut reader = std::io::Cursor::new(&input[..]);
1179 let mut writer = Vec::new();
1180
1181 let result = prompt_optional_key_impl(&mut reader, &mut writer, "polygonscan").unwrap();
1182 assert_eq!(result, Some("my_key_12345".to_string()));
1183 }
1184
1185 #[test]
1186 fn test_prompt_optional_key_impl_skip() {
1187 let input = b"\n";
1188 let mut reader = std::io::Cursor::new(&input[..]);
1189 let mut writer = Vec::new();
1190
1191 let result = prompt_optional_key_impl(&mut reader, &mut writer, "arbiscan").unwrap();
1192 assert_eq!(result, None);
1193 let output = String::from_utf8(writer).unwrap();
1194 assert!(output.contains("arbiscan API key"));
1195 }
1196
1197 #[test]
1198 fn test_save_config_to_path_creates_file_and_dirs() {
1199 let tmp = tempfile::tempdir().unwrap();
1200 let config_path = tmp.path().join("subdir").join("config.yaml");
1201 let mut config = Config::default();
1202 config
1203 .chains
1204 .api_keys
1205 .insert("etherscan".to_string(), "test_key_abc".to_string());
1206 config.output.format = OutputFormat::Json;
1207 config.output.color = false;
1208
1209 save_config_to_path(&config, &config_path).unwrap();
1210
1211 assert!(config_path.exists());
1212 let content = std::fs::read_to_string(&config_path).unwrap();
1213 assert!(content.contains("etherscan"));
1214 assert!(content.contains("test_key_abc"));
1215 assert!(content.contains("json"));
1216 assert!(content.contains("color: false"));
1217 assert!(content.contains("# Scope Configuration"));
1218 }
1219
1220 #[test]
1221 fn test_save_config_to_path_with_rpc() {
1222 let tmp = tempfile::tempdir().unwrap();
1223 let config_path = tmp.path().join("config.yaml");
1224 let mut config = Config::default();
1225 config.chains.ethereum_rpc = Some("https://my-rpc.example.com".to_string());
1226
1227 save_config_to_path(&config, &config_path).unwrap();
1228
1229 let content = std::fs::read_to_string(&config_path).unwrap();
1230 assert!(content.contains("ethereum_rpc"));
1231 assert!(content.contains("https://my-rpc.example.com"));
1232 }
1233
1234 #[test]
1235 fn test_reset_config_impl_confirm_yes() {
1236 let tmp = tempfile::tempdir().unwrap();
1237 let config_path = tmp.path().join("config.yaml");
1238 std::fs::write(&config_path, "test: data").unwrap();
1239
1240 let input = b"y\n";
1241 let mut reader = std::io::Cursor::new(&input[..]);
1242 let mut writer = Vec::new();
1243
1244 reset_config_impl(&mut reader, &mut writer, &config_path).unwrap();
1245 assert!(!config_path.exists());
1246 let output = String::from_utf8(writer).unwrap();
1247 assert!(output.contains("Configuration reset to defaults"));
1248 }
1249
1250 #[test]
1251 fn test_reset_config_impl_confirm_yes_full() {
1252 let tmp = tempfile::tempdir().unwrap();
1253 let config_path = tmp.path().join("config.yaml");
1254 std::fs::write(&config_path, "test: data").unwrap();
1255
1256 let input = b"yes\n";
1257 let mut reader = std::io::Cursor::new(&input[..]);
1258 let mut writer = Vec::new();
1259
1260 reset_config_impl(&mut reader, &mut writer, &config_path).unwrap();
1261 assert!(!config_path.exists());
1262 }
1263
1264 #[test]
1265 fn test_reset_config_impl_cancel() {
1266 let tmp = tempfile::tempdir().unwrap();
1267 let config_path = tmp.path().join("config.yaml");
1268 std::fs::write(&config_path, "test: data").unwrap();
1269
1270 let input = b"n\n";
1271 let mut reader = std::io::Cursor::new(&input[..]);
1272 let mut writer = Vec::new();
1273
1274 reset_config_impl(&mut reader, &mut writer, &config_path).unwrap();
1275 assert!(config_path.exists()); let output = String::from_utf8(writer).unwrap();
1277 assert!(output.contains("Cancelled"));
1278 }
1279
1280 #[test]
1281 fn test_reset_config_impl_no_file() {
1282 let tmp = tempfile::tempdir().unwrap();
1283 let config_path = tmp.path().join("nonexistent.yaml");
1284
1285 let input = b"";
1286 let mut reader = std::io::Cursor::new(&input[..]);
1287 let mut writer = Vec::new();
1288
1289 reset_config_impl(&mut reader, &mut writer, &config_path).unwrap();
1290 let output = String::from_utf8(writer).unwrap();
1291 assert!(output.contains("No configuration file found"));
1292 }
1293
1294 #[test]
1295 fn test_configure_single_key_impl_valid_key() {
1296 let tmp = tempfile::tempdir().unwrap();
1297 let config_path = tmp.path().join("config.yaml");
1298 let config = Config::default();
1299
1300 let input = b"MY_ETH_KEY_12345678\n";
1301 let mut reader = std::io::Cursor::new(&input[..]);
1302 let mut writer = Vec::new();
1303
1304 configure_single_key_impl(&mut reader, &mut writer, "etherscan", &config, &config_path)
1305 .unwrap();
1306
1307 let output = String::from_utf8(writer).unwrap();
1308 assert!(output.contains("Configure ETHERSCAN API Key"));
1309 assert!(output.contains("Ethereum Mainnet"));
1310 assert!(output.contains("etherscan API key saved"));
1311
1312 assert!(config_path.exists());
1314 let content = std::fs::read_to_string(&config_path).unwrap();
1315 assert!(content.contains("MY_ETH_KEY_12345678"));
1316 }
1317
1318 #[test]
1319 fn test_configure_single_key_impl_empty_skips() {
1320 let tmp = tempfile::tempdir().unwrap();
1321 let config_path = tmp.path().join("config.yaml");
1322 let config = Config::default();
1323
1324 let input = b"\n";
1325 let mut reader = std::io::Cursor::new(&input[..]);
1326 let mut writer = Vec::new();
1327
1328 configure_single_key_impl(&mut reader, &mut writer, "etherscan", &config, &config_path)
1329 .unwrap();
1330
1331 let output = String::from_utf8(writer).unwrap();
1332 assert!(output.contains("Skipped"));
1333 assert!(!config_path.exists()); }
1335
1336 #[test]
1337 fn test_configure_single_key_impl_invalid_key_name() {
1338 let tmp = tempfile::tempdir().unwrap();
1339 let config_path = tmp.path().join("config.yaml");
1340 let config = Config::default();
1341
1342 let input = b"";
1343 let mut reader = std::io::Cursor::new(&input[..]);
1344 let mut writer = Vec::new();
1345
1346 configure_single_key_impl(&mut reader, &mut writer, "invalid", &config, &config_path)
1347 .unwrap();
1348
1349 let output = String::from_utf8(writer).unwrap();
1350 assert!(output.contains("Unknown API key: invalid"));
1351 assert!(output.contains("Valid options"));
1352 }
1353
1354 #[test]
1355 fn test_configure_single_key_impl_bscscan() {
1356 let tmp = tempfile::tempdir().unwrap();
1357 let config_path = tmp.path().join("config.yaml");
1358 let config = Config::default();
1359
1360 let input = b"BSC_KEY_ABCDEF\n";
1361 let mut reader = std::io::Cursor::new(&input[..]);
1362 let mut writer = Vec::new();
1363
1364 configure_single_key_impl(&mut reader, &mut writer, "bscscan", &config, &config_path)
1365 .unwrap();
1366
1367 let output = String::from_utf8(writer).unwrap();
1368 assert!(output.contains("Configure BSCSCAN API Key"));
1369 assert!(output.contains("BNB Smart Chain"));
1370 assert!(config_path.exists());
1371 }
1372
1373 #[test]
1374 fn test_wizard_no_changes() {
1375 let tmp = tempfile::tempdir().unwrap();
1376 let config_path = tmp.path().join("config.yaml");
1377 let config = Config::default();
1378
1379 let input = b"\nn\n\n";
1381 let mut reader = std::io::Cursor::new(&input[..]);
1382 let mut writer = Vec::new();
1383
1384 run_setup_wizard_impl(&mut reader, &mut writer, &config, &config_path).unwrap();
1385
1386 let output = String::from_utf8(writer).unwrap();
1387 assert!(output.contains("Scope Setup Wizard"));
1388 assert!(output.contains("Step 1: API Keys"));
1389 assert!(output.contains("Step 2: Preferences"));
1390 assert!(output.contains("No changes made"));
1391 assert!(output.contains("Setup complete"));
1392 assert!(!config_path.exists()); }
1394
1395 #[test]
1396 fn test_wizard_with_etherscan_key_and_json_format() {
1397 let tmp = tempfile::tempdir().unwrap();
1398 let config_path = tmp.path().join("config.yaml");
1399 let config = Config::default();
1400
1401 let input = b"MY_ETH_KEY\nn\n2\n";
1403 let mut reader = std::io::Cursor::new(&input[..]);
1404 let mut writer = Vec::new();
1405
1406 run_setup_wizard_impl(&mut reader, &mut writer, &config, &config_path).unwrap();
1407
1408 let output = String::from_utf8(writer).unwrap();
1409 assert!(output.contains("Configuration saved"));
1410 assert!(config_path.exists());
1411 let content = std::fs::read_to_string(&config_path).unwrap();
1412 assert!(content.contains("MY_ETH_KEY"));
1413 assert!(content.contains("json"));
1414 }
1415
1416 #[test]
1417 fn test_wizard_with_csv_format() {
1418 let tmp = tempfile::tempdir().unwrap();
1419 let config_path = tmp.path().join("config.yaml");
1420 let config = Config::default();
1421
1422 let input = b"\nn\n3\n";
1424 let mut reader = std::io::Cursor::new(&input[..]);
1425 let mut writer = Vec::new();
1426
1427 run_setup_wizard_impl(&mut reader, &mut writer, &config, &config_path).unwrap();
1428
1429 let output = String::from_utf8(writer).unwrap();
1430 assert!(output.contains("Configuration saved"));
1431 let content = std::fs::read_to_string(&config_path).unwrap();
1432 assert!(content.contains("csv"));
1433 }
1434
1435 #[test]
1436 fn test_wizard_with_other_chains_yes() {
1437 let tmp = tempfile::tempdir().unwrap();
1438 let config_path = tmp.path().join("config.yaml");
1439 let config = Config::default();
1440
1441 let input = b"\ny\nBSC_KEY_123\n\n\n\n\n\n";
1443 let mut reader = std::io::Cursor::new(&input[..]);
1444 let mut writer = Vec::new();
1445
1446 run_setup_wizard_impl(&mut reader, &mut writer, &config, &config_path).unwrap();
1447
1448 let output = String::from_utf8(writer).unwrap();
1449 assert!(output.contains("BSCSCAN API KEY"));
1450 assert!(output.contains("Configuration saved"));
1451 let content = std::fs::read_to_string(&config_path).unwrap();
1452 assert!(content.contains("BSC_KEY_123"));
1453 }
1454
1455 #[test]
1456 fn test_wizard_etherscan_already_configured() {
1457 let tmp = tempfile::tempdir().unwrap();
1458 let config_path = tmp.path().join("config.yaml");
1459 let mut config = Config::default();
1460 config
1461 .chains
1462 .api_keys
1463 .insert("etherscan".to_string(), "existing_key".to_string());
1464
1465 let input = b"n\n\n";
1467 let mut reader = std::io::Cursor::new(&input[..]);
1468 let mut writer = Vec::new();
1469
1470 run_setup_wizard_impl(&mut reader, &mut writer, &config, &config_path).unwrap();
1471
1472 let output = String::from_utf8(writer).unwrap();
1473 assert!(output.contains("Etherscan API key already configured"));
1474 assert!(output.contains("No changes made"));
1475 }
1476}