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