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