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