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