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