1use crate::chains::ChainClientFactory;
28use crate::config::{Config, OutputFormat};
29use crate::error::Result;
30use clap::Args;
31use rustyline::DefaultEditor;
32use rustyline::error::ReadlineError;
33use serde::{Deserialize, Serialize};
34use std::fmt;
35use std::path::PathBuf;
36
37use super::{AddressArgs, AddressBookArgs, CrawlArgs, TxArgs};
38use super::{address, address_book, crawl, monitor, tx};
39
40#[derive(Debug, Clone, Args)]
42#[command(after_help = "\x1b[1mExamples:\x1b[0m
43 scope interactive
44 scope shell
45 scope interactive --no-banner")]
46pub struct InteractiveArgs {
47 #[arg(long)]
49 pub no_banner: bool,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SessionContext {
55 pub chain: String,
57
58 pub format: OutputFormat,
60
61 pub last_address: Option<String>,
63
64 pub last_tx: Option<String>,
66
67 pub include_tokens: bool,
69
70 pub include_txs: bool,
72
73 pub trace: bool,
75
76 pub decode: bool,
78
79 pub limit: u32,
81}
82
83impl SessionContext {
84 pub fn is_auto_chain(&self) -> bool {
86 self.chain == "auto"
87 }
88}
89
90impl Default for SessionContext {
91 fn default() -> Self {
92 Self {
93 chain: "auto".to_string(),
94 format: OutputFormat::Table,
95 last_address: None,
96 last_tx: None,
97 include_tokens: false,
98 include_txs: false,
99 trace: false,
100 decode: false,
101 limit: 100,
102 }
103 }
104}
105
106impl fmt::Display for SessionContext {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 writeln!(f, "Current Context:")?;
109 if self.is_auto_chain() {
110 writeln!(f, " Chain: auto (inferred from input)")?;
111 } else {
112 writeln!(f, " Chain: {} (pinned)", self.chain)?;
113 }
114 writeln!(f, " Format: {:?}", self.format)?;
115 writeln!(f, " Include Tokens: {}", self.include_tokens)?;
116 writeln!(f, " Include TXs: {}", self.include_txs)?;
117 writeln!(f, " Trace: {}", self.trace)?;
118 writeln!(f, " Decode: {}", self.decode)?;
119 writeln!(f, " Limit: {}", self.limit)?;
120 if let Some(ref addr) = self.last_address {
121 writeln!(f, " Last Address: {}", addr)?;
122 }
123 if let Some(ref tx) = self.last_tx {
124 writeln!(f, " Last TX: {}", tx)?;
125 }
126 Ok(())
127 }
128}
129
130impl SessionContext {
131 fn context_path() -> Option<PathBuf> {
133 dirs::data_dir().map(|p| p.join("scope").join("session.yaml"))
134 }
135
136 pub fn load() -> Self {
138 Self::context_path()
139 .and_then(|path| std::fs::read_to_string(&path).ok())
140 .and_then(|contents| serde_yaml::from_str(&contents).ok())
141 .unwrap_or_default()
142 }
143
144 pub fn save(&self) -> Result<()> {
146 if let Some(path) = Self::context_path() {
147 if let Some(parent) = path.parent() {
148 std::fs::create_dir_all(parent)?;
149 }
150 let contents = serde_yaml::to_string(self)
151 .map_err(|e| crate::error::ScopeError::Export(e.to_string()))?;
152 std::fs::write(&path, contents)?;
153 }
154 Ok(())
155 }
156}
157
158pub async fn run(
160 args: InteractiveArgs,
161 config: &Config,
162 clients: &dyn ChainClientFactory,
163) -> Result<()> {
164 if !args.no_banner {
166 let banner = include_str!("../../assets/banner.txt");
167 eprintln!("{}", banner);
168 }
169
170 println!("Welcome to Scope Interactive Mode!");
171 println!("Type 'help' for available commands, 'exit' to quit.\n");
172
173 let mut context = SessionContext::load();
175
176 if context.is_auto_chain() && context.format == OutputFormat::Table {
178 context.format = config.output.format;
179 }
180
181 let mut rl = DefaultEditor::new().map_err(|e| {
183 crate::error::ScopeError::Chain(format!("Failed to initialize readline: {}", e))
184 })?;
185
186 let history_path = dirs::data_dir().map(|p| p.join("scope").join("history.txt"));
188 if let Some(ref path) = history_path {
189 let _ = rl.load_history(path);
190 }
191
192 loop {
193 let prompt = format!("scope:{}> ", context.chain);
194
195 match rl.readline(&prompt) {
196 Ok(input_line) => {
197 let line = input_line.trim();
198 if line.is_empty() {
199 continue;
200 }
201
202 let _ = rl.add_history_entry(line);
204
205 match execute_input(line, &mut context, config, clients).await {
207 Ok(should_exit) => {
208 if should_exit {
209 break;
210 }
211 }
212 Err(e) => {
213 eprintln!("Error: {}", e);
214 }
215 }
216 }
217 Err(ReadlineError::Interrupted) => {
218 println!("^C");
219 continue;
220 }
221 Err(ReadlineError::Eof) => {
222 println!("exit");
223 break;
224 }
225 Err(err) => {
226 eprintln!("Error: {:?}", err);
227 break;
228 }
229 }
230 }
231
232 if let Some(ref path) = history_path {
234 if let Some(parent) = path.parent() {
235 let _ = std::fs::create_dir_all(parent);
236 }
237 let _ = rl.save_history(path);
238 }
239
240 if let Err(e) = context.save() {
242 tracing::debug!("Failed to save session context: {}", e);
243 }
244
245 println!("Goodbye!");
246 Ok(())
247}
248
249async fn execute_input(
251 input: &str,
252 context: &mut SessionContext,
253 config: &Config,
254 clients: &dyn ChainClientFactory,
255) -> Result<bool> {
256 let parts: Vec<&str> = input.split_whitespace().collect();
257 if parts.is_empty() {
258 return Ok(false);
259 }
260
261 let command = parts[0].to_lowercase();
262 let args = &parts[1..];
263
264 match command.as_str() {
265 "exit" | "quit" | ".exit" | ".quit" | "q" => {
267 return Ok(true);
268 }
269
270 "help" | "?" | ".help" => {
272 print_help();
273 }
274
275 "ctx" | "context" | ".ctx" | ".context" => {
277 println!("{}", context);
278 }
279
280 "clear" | ".clear" | "reset" | ".reset" => {
282 *context = SessionContext::default();
283 context.format = config.output.format;
284 println!("Context reset to defaults.");
285 }
286
287 "chain" | ".chain" => {
289 if args.is_empty() {
290 if context.is_auto_chain() {
291 println!("Current chain: auto (inferred from input)");
292 } else {
293 println!("Current chain: {} (pinned)", context.chain);
294 }
295 } else {
296 let new_chain = args[0].to_lowercase();
297 let valid_chains = [
298 "ethereum", "polygon", "arbitrum", "optimism", "base", "bsc", "solana", "tron",
299 ];
300 if new_chain == "auto" {
301 context.chain = "auto".to_string();
302 println!("Chain set to auto — will infer from each input");
303 } else if valid_chains.contains(&new_chain.as_str()) {
304 context.chain = new_chain.clone();
305 println!(
306 "Chain pinned to: {} (use `chain auto` to unlock)",
307 new_chain
308 );
309 } else {
310 eprintln!(
311 " ✗ Unknown chain: {}. Valid: auto, {}",
312 new_chain,
313 valid_chains.join(", ")
314 );
315 }
316 }
317 }
318
319 "format" | ".format" => {
321 if args.is_empty() {
322 println!("Current format: {:?}", context.format);
323 } else {
324 match args[0].to_lowercase().as_str() {
325 "table" => {
326 context.format = OutputFormat::Table;
327 println!("Format set to: table");
328 }
329 "json" => {
330 context.format = OutputFormat::Json;
331 println!("Format set to: json");
332 }
333 "csv" => {
334 context.format = OutputFormat::Csv;
335 println!("Format set to: csv");
336 }
337 other => {
338 eprintln!("Unknown format: {}. Valid formats: table, json, csv", other);
339 }
340 }
341 }
342 }
343
344 "+tokens" | "showtokens" => {
346 context.include_tokens = !context.include_tokens;
347 println!(
348 "Include tokens: {}",
349 if context.include_tokens { "on" } else { "off" }
350 );
351 }
352
353 "+txs" | "showtxs" | "txs" | ".txs" => {
354 context.include_txs = !context.include_txs;
355 println!(
356 "Include transactions: {}",
357 if context.include_txs { "on" } else { "off" }
358 );
359 }
360
361 "trace" | ".trace" => {
362 context.trace = !context.trace;
363 println!("Trace: {}", if context.trace { "on" } else { "off" });
364 }
365
366 "decode" | ".decode" => {
367 context.decode = !context.decode;
368 println!("Decode: {}", if context.decode { "on" } else { "off" });
369 }
370
371 "limit" | ".limit" => {
373 if args.is_empty() {
374 println!("Current limit: {}", context.limit);
375 } else if let Ok(n) = args[0].parse::<u32>() {
376 context.limit = n;
377 println!("Limit set to: {}", n);
378 } else {
379 eprintln!("Invalid limit: {}. Must be a positive integer.", args[0]);
380 }
381 }
382
383 "address" | "addr" => {
385 let addr = if args.is_empty() {
386 match &context.last_address {
388 Some(a) => a.clone(),
389 None => {
390 eprintln!("No address specified and no previous address in context.");
391 return Ok(false);
392 }
393 }
394 } else {
395 args[0].to_string()
396 };
397
398 let mut chain_override = None;
400 for arg in args.iter().skip(1) {
401 if arg.starts_with("--chain=") {
402 chain_override = Some(arg.trim_start_matches("--chain=").to_string());
403 }
404 }
405
406 let effective_chain = if let Some(chain) = chain_override {
408 chain
409 } else if context.is_auto_chain() {
410 if let Some(inferred) = crate::chains::infer_chain_from_address(&addr) {
411 eprintln!(" Chain: {} (auto-detected)", inferred);
412 inferred.to_string()
413 } else {
414 "ethereum".to_string()
416 }
417 } else {
418 context.chain.clone()
419 };
420
421 let mut address_args = AddressArgs {
423 address: addr.clone(),
424 chain: effective_chain,
425 format: Some(context.format),
426 include_txs: context.include_txs,
427 include_tokens: context.include_tokens,
428 limit: context.limit,
429 report: None,
430 dossier: false,
431 };
432
433 for arg in args.iter().skip(1) {
435 if *arg == "--tokens" {
436 address_args.include_tokens = true;
437 } else if *arg == "--txs" {
438 address_args.include_txs = true;
439 }
440 }
441
442 context.last_address = Some(addr);
444
445 address::run(address_args, config, clients).await?;
447 }
448
449 "tx" | "transaction" => {
451 let hash = if args.is_empty() {
452 match &context.last_tx {
454 Some(h) => h.clone(),
455 None => {
456 eprintln!("No transaction hash specified and no previous hash in context.");
457 return Ok(false);
458 }
459 }
460 } else {
461 args[0].to_string()
462 };
463
464 let mut chain_override = None;
466 for arg in args.iter().skip(1) {
467 if arg.starts_with("--chain=") {
468 chain_override = Some(arg.trim_start_matches("--chain=").to_string());
469 }
470 }
471
472 let effective_chain = if let Some(chain) = chain_override {
474 chain
475 } else if context.is_auto_chain() {
476 if let Some(inferred) = crate::chains::infer_chain_from_hash(&hash) {
477 eprintln!(" Chain: {} (auto-detected)", inferred);
478 inferred.to_string()
479 } else {
480 "ethereum".to_string()
481 }
482 } else {
483 context.chain.clone()
484 };
485
486 let mut tx_args = TxArgs {
487 hash: hash.clone(),
488 chain: effective_chain,
489 format: Some(context.format),
490 trace: context.trace,
491 decode: context.decode,
492 };
493
494 for arg in args.iter().skip(1) {
496 if *arg == "--trace" {
497 tx_args.trace = true;
498 } else if *arg == "--decode" {
499 tx_args.decode = true;
500 }
501 }
502
503 context.last_tx = Some(hash);
505
506 tx::run(tx_args, config, clients).await?;
508 }
509
510 "contract" | "ct" => {
512 if args.is_empty() {
513 eprintln!("Usage: contract <address> [--chain=<chain>] [--json]");
514 return Ok(false);
515 }
516
517 let address = args[0].to_string();
518 let mut chain = context.chain.clone();
519 let mut json_output = false;
520
521 for arg in args.iter().skip(1) {
522 if arg.starts_with("--chain=") {
523 chain = arg.trim_start_matches("--chain=").to_string();
524 } else if *arg == "--json" {
525 json_output = true;
526 }
527 }
528
529 if chain == "auto" {
531 chain = "ethereum".to_string();
532 }
533
534 let ct_args = crate::cli::contract::ContractArgs {
535 address,
536 chain,
537 json: json_output,
538 };
539
540 crate::cli::contract::run(&ct_args, config, clients).await?;
541 }
542
543 "crawl" | "token" => {
545 if args.is_empty() {
546 eprintln!(
547 "Usage: crawl <token_address> [--period <1h|24h|7d|30d>] [--report <path>]"
548 );
549 return Ok(false);
550 }
551
552 let token = args[0].to_string();
553
554 let mut chain_override = None;
556 let mut period = crawl::Period::Hour24;
557 let mut report_path = None;
558 let mut no_charts = false;
559
560 let mut i = 1;
561 while i < args.len() {
562 if args[i].starts_with("--chain=") {
563 chain_override = Some(args[i].trim_start_matches("--chain=").to_string());
564 } else if args[i] == "--chain" && i + 1 < args.len() {
565 chain_override = Some(args[i + 1].to_string());
566 i += 1;
567 } else if args[i].starts_with("--period=") {
568 let p = args[i].trim_start_matches("--period=");
569 period = match p {
570 "1h" => crawl::Period::Hour1,
571 "24h" => crawl::Period::Hour24,
572 "7d" => crawl::Period::Day7,
573 "30d" => crawl::Period::Day30,
574 _ => crawl::Period::Hour24,
575 };
576 } else if args[i] == "--period" && i + 1 < args.len() {
577 period = match args[i + 1] {
578 "1h" => crawl::Period::Hour1,
579 "24h" => crawl::Period::Hour24,
580 "7d" => crawl::Period::Day7,
581 "30d" => crawl::Period::Day30,
582 _ => crawl::Period::Hour24,
583 };
584 i += 1;
585 } else if args[i].starts_with("--report=") {
586 report_path = Some(std::path::PathBuf::from(
587 args[i].trim_start_matches("--report="),
588 ));
589 } else if args[i] == "--report" && i + 1 < args.len() {
590 report_path = Some(std::path::PathBuf::from(args[i + 1]));
591 i += 1;
592 } else if args[i] == "--no-charts" {
593 no_charts = true;
594 }
595 i += 1;
596 }
597
598 let effective_chain = if let Some(chain) = chain_override {
600 chain
601 } else if context.is_auto_chain() {
602 if let Some(inferred) = crate::chains::infer_chain_from_address(&token) {
603 eprintln!(" Chain: {} (auto-detected)", inferred);
604 inferred.to_string()
605 } else {
606 "ethereum".to_string()
607 }
608 } else {
609 context.chain.clone()
610 };
611
612 let crawl_args = CrawlArgs {
613 token,
614 chain: effective_chain,
615 period,
616 holders_limit: 10,
617 format: context.format,
618 no_charts,
619 report: report_path,
620 yes: false, save: false, };
623
624 crawl::run(crawl_args, config, clients).await?;
625 }
626
627 "address-book" | "address_book" | "portfolio" | "port" => {
629 let input = args.join(" ");
630 execute_address_book(&input, context, config, clients).await?;
631 }
632
633 "tokens" | "aliases" => {
635 execute_tokens_command(args).await?;
636 }
637
638 "setup" | "config" => {
640 use super::setup::{SetupArgs, run as setup_run};
641 let setup_args = SetupArgs {
642 status: args.contains(&"--status") || args.contains(&"-s"),
643 key: args
644 .iter()
645 .find(|a| a.starts_with("--key="))
646 .map(|a| a.trim_start_matches("--key=").to_string())
647 .or_else(|| {
648 args.iter()
649 .position(|&a| a == "--key" || a == "-k")
650 .and_then(|i| args.get(i + 1).map(|s| s.to_string()))
651 }),
652 reset: args.contains(&"--reset"),
653 };
654 setup_run(setup_args, config).await?;
655 }
656
657 "monitor" | "mon" => {
659 let token = args.first().map(|s| s.to_string());
660 monitor::run(token, None, context, config, clients).await?;
661 }
662
663 _ => {
665 eprintln!(
666 "Unknown command: {}. Type 'help' for available commands.",
667 command
668 );
669 }
670 }
671
672 Ok(false)
673}
674
675async fn execute_tokens_command(args: &[&str]) -> Result<()> {
677 use crate::display::terminal as t;
678 use crate::tokens::TokenAliases;
679
680 let mut aliases = TokenAliases::load();
681
682 if args.is_empty() {
683 let tokens = aliases.list();
685 if tokens.is_empty() {
686 println!("{}", t::info_row("No saved token aliases."));
687 println!(
688 "{}",
689 t::info_row("Use 'crawl <token_name> --save' to save a token alias.")
690 );
691 return Ok(());
692 }
693
694 println!("{}", t::section_header("Saved Token Aliases"));
695 let cols = &[
696 t::Col {
697 label: "Symbol",
698 width: 10,
699 align: '<',
700 },
701 t::Col {
702 label: "Chain",
703 width: 12,
704 align: '<',
705 },
706 t::Col {
707 label: "Name",
708 width: 20,
709 align: '<',
710 },
711 t::Col {
712 label: "Address",
713 width: 42,
714 align: '<',
715 },
716 ];
717 println!("{}", t::table_header(cols));
718 for token in tokens {
719 println!(
720 "{}",
721 t::table_row(
722 cols,
723 &[&token.symbol, &token.chain, &token.name, &token.address]
724 )
725 );
726 }
727 println!("{}", t::section_footer());
728 return Ok(());
729 }
730
731 let subcommand = args[0].to_lowercase();
732 match subcommand.as_str() {
733 "list" | "ls" => {
734 let tokens = aliases.list();
735 if tokens.is_empty() {
736 println!("{}", t::info_row("No saved token aliases."));
737 return Ok(());
738 }
739
740 println!("{}", t::section_header("Saved Token Aliases"));
741 let cols = &[
742 t::Col {
743 label: "Symbol",
744 width: 10,
745 align: '<',
746 },
747 t::Col {
748 label: "Chain",
749 width: 12,
750 align: '<',
751 },
752 t::Col {
753 label: "Name",
754 width: 20,
755 align: '<',
756 },
757 t::Col {
758 label: "Address",
759 width: 42,
760 align: '<',
761 },
762 ];
763 println!("{}", t::table_header(cols));
764 for token in tokens {
765 println!(
766 "{}",
767 t::table_row(
768 cols,
769 &[&token.symbol, &token.chain, &token.name, &token.address]
770 )
771 );
772 }
773 println!("{}", t::section_footer());
774 }
775
776 "recent" => {
777 let recent = aliases.recent();
778 if recent.is_empty() {
779 println!("{}", t::info_row("No recently used tokens."));
780 return Ok(());
781 }
782
783 println!("{}", t::section_header("Recently Used Tokens"));
784 let cols = &[
785 t::Col {
786 label: "Symbol",
787 width: 10,
788 align: '<',
789 },
790 t::Col {
791 label: "Chain",
792 width: 12,
793 align: '<',
794 },
795 t::Col {
796 label: "Name",
797 width: 20,
798 align: '<',
799 },
800 t::Col {
801 label: "Address",
802 width: 42,
803 align: '<',
804 },
805 ];
806 println!("{}", t::table_header(cols));
807 for token in recent {
808 println!(
809 "{}",
810 t::table_row(
811 cols,
812 &[&token.symbol, &token.chain, &token.name, &token.address]
813 )
814 );
815 }
816 println!("{}", t::section_footer());
817 }
818
819 "remove" | "rm" | "delete" => {
820 if args.len() < 2 {
821 eprintln!("Usage: tokens remove <symbol> [--chain <chain>]");
822 return Ok(());
823 }
824
825 let symbol = args[1];
826 let chain = if args.len() > 3 && args[2] == "--chain" {
827 Some(args[3])
828 } else {
829 None
830 };
831
832 aliases.remove(symbol, chain);
833 if let Err(e) = aliases.save() {
834 eprintln!("Failed to save: {}", e);
835 } else {
836 println!("Removed alias: {}", symbol);
837 }
838 }
839
840 "add" => {
841 if args.len() < 4 {
842 eprintln!("Usage: tokens add <symbol> <chain> <address> [name]");
843 return Ok(());
844 }
845
846 let symbol = args[1];
847 let chain = args[2];
848 let address = args[3];
849 let name = if args.len() > 4 {
850 args[4..].join(" ")
851 } else {
852 symbol.to_string()
853 };
854
855 aliases.add(symbol, chain, address, &name);
856 if let Err(e) = aliases.save() {
857 eprintln!("Failed to save: {}", e);
858 } else {
859 println!("Added alias: {} -> {} on {}", symbol, address, chain);
860 }
861 }
862
863 _ => {
864 eprintln!("Unknown tokens subcommand: {}", subcommand);
865 eprintln!("Available: list, recent, add, remove");
866 }
867 }
868
869 Ok(())
870}
871
872async fn execute_address_book(
874 input: &str,
875 context: &SessionContext,
876 config: &Config,
877 clients: &dyn ChainClientFactory,
878) -> Result<()> {
879 let parts: Vec<&str> = input.split_whitespace().collect();
880 if parts.is_empty() {
881 eprintln!("Address book subcommand required: add, remove, list, summary");
882 return Ok(());
883 }
884
885 use super::address_book::{AddArgs, AddressBookCommands, RemoveArgs, SummaryArgs};
886
887 let subcommand = parts[0].to_lowercase();
888
889 let address_book_args = match subcommand.as_str() {
890 "add" => {
891 if parts.len() < 2 {
892 eprintln!("Usage: address-book add <address> [--label <label>] [--tags <tags>]");
893 return Ok(());
894 }
895 let address = parts[1].to_string();
896 let mut label = None;
897 let mut tags = Vec::new();
898
899 let mut i = 2;
900 while i < parts.len() {
901 if parts[i] == "--label" && i + 1 < parts.len() {
902 label = Some(parts[i + 1].to_string());
903 i += 2;
904 } else if parts[i] == "--tags" && i + 1 < parts.len() {
905 tags = parts[i + 1]
906 .split(',')
907 .map(|s| s.trim().to_string())
908 .collect();
909 i += 2;
910 } else {
911 i += 1;
912 }
913 }
914
915 AddressBookArgs {
916 command: AddressBookCommands::Add(AddArgs {
917 chain: if context.is_auto_chain() {
918 crate::chains::infer_chain_from_address(&address)
919 .unwrap_or("ethereum")
920 .to_string()
921 } else {
922 context.chain.clone()
923 },
924 address,
925 label,
926 tags,
927 }),
928 format: Some(context.format),
929 }
930 }
931 "remove" | "rm" => {
932 if parts.len() < 2 {
933 eprintln!("Usage: address-book remove <address>");
934 return Ok(());
935 }
936 AddressBookArgs {
937 command: AddressBookCommands::Remove(RemoveArgs {
938 address: parts[1].to_string(),
939 }),
940 format: Some(context.format),
941 }
942 }
943 "list" | "ls" => AddressBookArgs {
944 command: AddressBookCommands::List,
945 format: Some(context.format),
946 },
947 "summary" => {
948 let mut chain = None;
949 let mut tag = None;
950 let mut include_tokens = context.include_tokens;
951
952 let mut i = 1;
953 while i < parts.len() {
954 if parts[i] == "--chain" && i + 1 < parts.len() {
955 chain = Some(parts[i + 1].to_string());
956 i += 2;
957 } else if parts[i] == "--tag" && i + 1 < parts.len() {
958 tag = Some(parts[i + 1].to_string());
959 i += 2;
960 } else if parts[i] == "--tokens" {
961 include_tokens = true;
962 i += 1;
963 } else {
964 i += 1;
965 }
966 }
967
968 AddressBookArgs {
969 command: AddressBookCommands::Summary(SummaryArgs {
970 chain,
971 tag,
972 include_tokens,
973 report: None,
974 }),
975 format: Some(context.format),
976 }
977 }
978 _ => {
979 eprintln!(
980 "Unknown address book subcommand: {}. Use: add, remove, list, summary",
981 subcommand
982 );
983 return Ok(());
984 }
985 };
986
987 address_book::run(address_book_args, config, clients).await
988}
989
990fn print_help() {
992 println!(
993 r#"
994Scope Interactive Mode - Available Commands
995==========================================
996
997Navigation & Control:
998 help, ? Show this help message
999 exit, quit, q Exit interactive mode
1000 ctx, context Show current session context
1001 clear, reset Reset context to defaults
1002
1003Context Settings:
1004 chain [name] Set or show current chain (default: auto)
1005 auto = infer chain from each input
1006 Valid: auto, ethereum, polygon, arbitrum, optimism, base, bsc, solana, tron
1007 format [fmt] Set or show output format (table, json, csv)
1008 limit [n] Set or show transaction limit
1009 +tokens Toggle include_tokens flag for address analysis
1010 +txs Toggle include_txs flag
1011 trace Toggle trace flag
1012 decode Toggle decode flag
1013
1014Analysis Commands:
1015 address <addr> Analyze an address (uses current chain/format)
1016 addr Shorthand for address
1017 tx <hash> Analyze a transaction (uses current chain/format)
1018 contract <addr> Analyze a smart contract (security, proxy, access control)
1019 ct Shorthand for contract
1020 crawl <token> Crawl token analytics (holders, volume, price)
1021 token Shorthand for crawl
1022 monitor <token> Live-updating charts for a token (TUI mode)
1023 mon Shorthand for monitor
1024
1025Token Search:
1026 crawl USDC Search for token by name/symbol (interactive selection)
1027 crawl 0x... Use address directly (no search)
1028 tokens List saved token aliases
1029 tokens recent Show recently used tokens
1030 tokens add <sym> <chain> <addr> [name] Add a token alias
1031 tokens remove <sym> [--chain <chain>] Remove a token alias
1032
1033Address Book Commands:
1034 address-book add <addr> [--label <name>] [--tags <t1,t2>]
1035 address-book remove <addr>
1036 address-book list
1037 address-book summary [--chain <name>] [--tag <tag>] [--tokens]
1038
1039Configuration:
1040 setup Run the setup wizard to configure API keys
1041 setup --status Show current configuration status
1042 setup --key <provider> Configure a specific API key
1043 config Alias for setup
1044
1045Inline Overrides:
1046 address 0x... --chain=polygon --tokens
1047 tx 0x... --chain=arbitrum --trace --decode
1048 contract 0x... --chain=polygon --json
1049 crawl USDC --chain=ethereum --period=7d --report=report.md
1050
1051Live Monitor:
1052 monitor USDC Start live monitoring with real-time charts
1053 mon 0x... Monitor by address
1054 Time periods: [1]=15m [2]=1h [3]=6h [4]=24h [T]=cycle
1055 Chart modes: [C]=toggle between Line and Candlestick
1056 Controls: [Q]uit [R]efresh [P]ause [+/-]speed [Esc]exit
1057 Data is cached to temp folder and persists between sessions (24h retention)
1058
1059Tips:
1060 - Search by token name: 'crawl WETH' or 'crawl "wrapped ether"'
1061 - Save aliases for quick access: select a token and choose to save
1062 - Context persists: set chain once, use it for multiple commands
1063 - Use Ctrl+C to cancel, Ctrl+D to exit
1064"#
1065 );
1066}
1067
1068#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1077 fn test_session_context_default() {
1078 let ctx = SessionContext::default();
1079 assert_eq!(ctx.chain, "auto");
1080 assert_eq!(ctx.format, OutputFormat::Table);
1081 assert!(!ctx.include_tokens);
1082 assert!(!ctx.include_txs);
1083 assert!(!ctx.trace);
1084 assert!(!ctx.decode);
1085 assert_eq!(ctx.limit, 100);
1086 assert!(ctx.last_address.is_none());
1087 assert!(ctx.last_tx.is_none());
1088 }
1089
1090 #[test]
1091 fn test_session_context_display() {
1092 let ctx = SessionContext::default();
1093 let display = format!("{}", ctx);
1094 assert!(display.contains("auto"));
1095 assert!(display.contains("Table"));
1096 }
1097
1098 #[test]
1099 fn test_interactive_args_default() {
1100 let args = InteractiveArgs { no_banner: false };
1101 assert!(!args.no_banner);
1102 }
1103
1104 #[test]
1109 fn test_session_context_serialization() {
1110 let ctx = SessionContext {
1111 chain: "polygon".to_string(),
1112 format: OutputFormat::Json,
1113 last_address: Some("0xabc".to_string()),
1114 last_tx: Some("0xdef".to_string()),
1115 include_tokens: true,
1116 include_txs: true,
1117 trace: true,
1118 decode: true,
1119 limit: 50,
1120 };
1121
1122 let yaml = serde_yaml::to_string(&ctx).unwrap();
1123 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
1124 assert_eq!(deserialized.chain, "polygon");
1125 assert!(!deserialized.is_auto_chain());
1126 assert_eq!(deserialized.format, OutputFormat::Json);
1127 assert_eq!(deserialized.last_address.as_deref(), Some("0xabc"));
1128 assert_eq!(deserialized.last_tx.as_deref(), Some("0xdef"));
1129 assert!(deserialized.include_tokens);
1130 assert!(deserialized.include_txs);
1131 assert!(deserialized.trace);
1132 assert!(deserialized.decode);
1133 assert_eq!(deserialized.limit, 50);
1134 }
1135
1136 #[test]
1137 fn test_session_context_display_with_address_and_tx() {
1138 let ctx = SessionContext {
1139 chain: "polygon".to_string(),
1140 last_address: Some("0x1234".to_string()),
1141 last_tx: Some("0xabcd".to_string()),
1142 ..Default::default()
1143 };
1144 let display = format!("{}", ctx);
1145 assert!(display.contains("0x1234"));
1146 assert!(display.contains("0xabcd"));
1147 assert!(display.contains("(pinned)"));
1148 }
1149
1150 #[test]
1151 fn test_session_context_display_auto_chain() {
1152 let ctx = SessionContext::default();
1153 let display = format!("{}", ctx);
1154 assert!(display.contains("auto"));
1155 assert!(display.contains("inferred from input"));
1156 }
1157
1158 fn test_config() -> Config {
1163 Config::default()
1164 }
1165
1166 fn test_factory() -> crate::chains::DefaultClientFactory {
1167 crate::chains::DefaultClientFactory {
1168 chains_config: crate::config::ChainsConfig::default(),
1169 }
1170 }
1171
1172 #[tokio::test]
1173 async fn test_exit_commands() {
1174 let config = test_config();
1175 for cmd in &["exit", "quit", "q", ".exit", ".quit"] {
1176 let mut ctx = SessionContext::default();
1177 let result = execute_input(cmd, &mut ctx, &config, &test_factory())
1178 .await
1179 .unwrap();
1180 assert!(result, "'{cmd}' should return true (exit)");
1181 }
1182 }
1183
1184 #[tokio::test]
1185 async fn test_help_command() {
1186 let config = test_config();
1187 let mut ctx = SessionContext::default();
1188 let result = execute_input("help", &mut ctx, &config, &test_factory())
1189 .await
1190 .unwrap();
1191 assert!(!result);
1192 }
1193
1194 #[tokio::test]
1195 async fn test_context_command() {
1196 let config = test_config();
1197 let mut ctx = SessionContext::default();
1198 let result = execute_input("ctx", &mut ctx, &config, &test_factory())
1199 .await
1200 .unwrap();
1201 assert!(!result);
1202 }
1203
1204 #[tokio::test]
1205 async fn test_clear_command() {
1206 let config = test_config();
1207 let mut ctx = SessionContext {
1208 chain: "polygon".to_string(),
1209 include_tokens: true,
1210 limit: 42,
1211 ..Default::default()
1212 };
1213
1214 let result = execute_input("clear", &mut ctx, &config, &test_factory())
1215 .await
1216 .unwrap();
1217 assert!(!result);
1218 assert_eq!(ctx.chain, "auto");
1219 assert!(!ctx.include_tokens);
1220 assert_eq!(ctx.limit, 100);
1221 }
1222
1223 #[tokio::test]
1224 async fn test_chain_set_valid() {
1225 let config = test_config();
1226 let mut ctx = SessionContext::default();
1227
1228 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1229 .await
1230 .unwrap();
1231 assert_eq!(ctx.chain, "polygon");
1232 assert!(!ctx.is_auto_chain());
1233 }
1234
1235 #[tokio::test]
1236 async fn test_chain_set_solana() {
1237 let config = test_config();
1238 let mut ctx = SessionContext::default();
1239
1240 execute_input("chain solana", &mut ctx, &config, &test_factory())
1241 .await
1242 .unwrap();
1243 assert_eq!(ctx.chain, "solana");
1244 assert!(!ctx.is_auto_chain());
1245 }
1246
1247 #[tokio::test]
1248 async fn test_chain_auto() {
1249 let config = test_config();
1250 let mut ctx = SessionContext {
1251 chain: "polygon".to_string(),
1252 ..Default::default()
1253 };
1254
1255 execute_input("chain auto", &mut ctx, &config, &test_factory())
1256 .await
1257 .unwrap();
1258 assert_eq!(ctx.chain, "auto");
1259 assert!(ctx.is_auto_chain());
1260 }
1261
1262 #[tokio::test]
1263 async fn test_chain_invalid() {
1264 let config = test_config();
1265 let mut ctx = SessionContext::default();
1266 execute_input("chain foobar", &mut ctx, &config, &test_factory())
1268 .await
1269 .unwrap();
1270 assert_eq!(ctx.chain, "auto");
1271 assert!(ctx.is_auto_chain());
1272 }
1273
1274 #[tokio::test]
1275 async fn test_chain_show() {
1276 let config = test_config();
1277 let mut ctx = SessionContext::default();
1278 let result = execute_input("chain", &mut ctx, &config, &test_factory())
1280 .await
1281 .unwrap();
1282 assert!(!result);
1283 assert_eq!(ctx.chain, "auto");
1284 }
1285
1286 #[tokio::test]
1287 async fn test_format_set_json() {
1288 let config = test_config();
1289 let mut ctx = SessionContext::default();
1290 execute_input("format json", &mut ctx, &config, &test_factory())
1291 .await
1292 .unwrap();
1293 assert_eq!(ctx.format, OutputFormat::Json);
1294 }
1295
1296 #[tokio::test]
1297 async fn test_format_set_csv() {
1298 let config = test_config();
1299 let mut ctx = SessionContext::default();
1300 execute_input("format csv", &mut ctx, &config, &test_factory())
1301 .await
1302 .unwrap();
1303 assert_eq!(ctx.format, OutputFormat::Csv);
1304 }
1305
1306 #[tokio::test]
1307 async fn test_format_set_table() {
1308 let config = test_config();
1309 let mut ctx = SessionContext {
1310 format: OutputFormat::Json,
1311 ..Default::default()
1312 };
1313 execute_input("format table", &mut ctx, &config, &test_factory())
1314 .await
1315 .unwrap();
1316 assert_eq!(ctx.format, OutputFormat::Table);
1317 }
1318
1319 #[tokio::test]
1320 async fn test_format_invalid() {
1321 let config = test_config();
1322 let mut ctx = SessionContext::default();
1323 execute_input("format xml", &mut ctx, &config, &test_factory())
1324 .await
1325 .unwrap();
1326 assert_eq!(ctx.format, OutputFormat::Table);
1328 }
1329
1330 #[tokio::test]
1331 async fn test_format_show() {
1332 let config = test_config();
1333 let mut ctx = SessionContext::default();
1334 let result = execute_input("format", &mut ctx, &config, &test_factory())
1335 .await
1336 .unwrap();
1337 assert!(!result);
1338 }
1339
1340 #[tokio::test]
1341 async fn test_toggle_tokens() {
1342 let config = test_config();
1343 let mut ctx = SessionContext::default();
1344 assert!(!ctx.include_tokens);
1345
1346 execute_input("+tokens", &mut ctx, &config, &test_factory())
1347 .await
1348 .unwrap();
1349 assert!(ctx.include_tokens);
1350
1351 execute_input("+tokens", &mut ctx, &config, &test_factory())
1352 .await
1353 .unwrap();
1354 assert!(!ctx.include_tokens);
1355 }
1356
1357 #[tokio::test]
1358 async fn test_toggle_txs() {
1359 let config = test_config();
1360 let mut ctx = SessionContext::default();
1361 assert!(!ctx.include_txs);
1362
1363 execute_input("+txs", &mut ctx, &config, &test_factory())
1364 .await
1365 .unwrap();
1366 assert!(ctx.include_txs);
1367
1368 execute_input("+txs", &mut ctx, &config, &test_factory())
1369 .await
1370 .unwrap();
1371 assert!(!ctx.include_txs);
1372 }
1373
1374 #[tokio::test]
1375 async fn test_toggle_trace() {
1376 let config = test_config();
1377 let mut ctx = SessionContext::default();
1378 assert!(!ctx.trace);
1379
1380 execute_input("trace", &mut ctx, &config, &test_factory())
1381 .await
1382 .unwrap();
1383 assert!(ctx.trace);
1384
1385 execute_input("trace", &mut ctx, &config, &test_factory())
1386 .await
1387 .unwrap();
1388 assert!(!ctx.trace);
1389 }
1390
1391 #[tokio::test]
1392 async fn test_toggle_decode() {
1393 let config = test_config();
1394 let mut ctx = SessionContext::default();
1395 assert!(!ctx.decode);
1396
1397 execute_input("decode", &mut ctx, &config, &test_factory())
1398 .await
1399 .unwrap();
1400 assert!(ctx.decode);
1401
1402 execute_input("decode", &mut ctx, &config, &test_factory())
1403 .await
1404 .unwrap();
1405 assert!(!ctx.decode);
1406 }
1407
1408 #[tokio::test]
1409 async fn test_limit_set_valid() {
1410 let config = test_config();
1411 let mut ctx = SessionContext::default();
1412 execute_input("limit 50", &mut ctx, &config, &test_factory())
1413 .await
1414 .unwrap();
1415 assert_eq!(ctx.limit, 50);
1416 }
1417
1418 #[tokio::test]
1419 async fn test_limit_set_invalid() {
1420 let config = test_config();
1421 let mut ctx = SessionContext::default();
1422 execute_input("limit abc", &mut ctx, &config, &test_factory())
1423 .await
1424 .unwrap();
1425 assert_eq!(ctx.limit, 100);
1427 }
1428
1429 #[tokio::test]
1430 async fn test_limit_show() {
1431 let config = test_config();
1432 let mut ctx = SessionContext::default();
1433 let result = execute_input("limit", &mut ctx, &config, &test_factory())
1434 .await
1435 .unwrap();
1436 assert!(!result);
1437 }
1438
1439 #[tokio::test]
1440 async fn test_unknown_command() {
1441 let config = test_config();
1442 let mut ctx = SessionContext::default();
1443 let result = execute_input("foobar", &mut ctx, &config, &test_factory())
1444 .await
1445 .unwrap();
1446 assert!(!result);
1447 }
1448
1449 #[tokio::test]
1450 async fn test_empty_input() {
1451 let config = test_config();
1452 let mut ctx = SessionContext::default();
1453 let result = execute_input("", &mut ctx, &config, &test_factory())
1454 .await
1455 .unwrap();
1456 assert!(!result);
1457 }
1458
1459 #[tokio::test]
1460 async fn test_address_no_arg_no_last() {
1461 let config = test_config();
1462 let mut ctx = SessionContext::default();
1463 let result = execute_input("address", &mut ctx, &config, &test_factory())
1465 .await
1466 .unwrap();
1467 assert!(!result);
1468 }
1469
1470 #[tokio::test]
1471 async fn test_tx_no_arg_no_last() {
1472 let config = test_config();
1473 let mut ctx = SessionContext::default();
1474 let result = execute_input("tx", &mut ctx, &config, &test_factory())
1476 .await
1477 .unwrap();
1478 assert!(!result);
1479 }
1480
1481 #[tokio::test]
1482 async fn test_crawl_no_arg() {
1483 let config = test_config();
1484 let mut ctx = SessionContext::default();
1485 let result = execute_input("crawl", &mut ctx, &config, &test_factory())
1487 .await
1488 .unwrap();
1489 assert!(!result);
1490 }
1491
1492 #[tokio::test]
1493 async fn test_multiple_context_commands() {
1494 let config = test_config();
1495 let mut ctx = SessionContext::default();
1496
1497 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1499 .await
1500 .unwrap();
1501 execute_input("format json", &mut ctx, &config, &test_factory())
1502 .await
1503 .unwrap();
1504 execute_input("+tokens", &mut ctx, &config, &test_factory())
1505 .await
1506 .unwrap();
1507 execute_input("trace", &mut ctx, &config, &test_factory())
1508 .await
1509 .unwrap();
1510 execute_input("limit 25", &mut ctx, &config, &test_factory())
1511 .await
1512 .unwrap();
1513
1514 assert_eq!(ctx.chain, "polygon");
1515 assert_eq!(ctx.format, OutputFormat::Json);
1516 assert!(ctx.include_tokens);
1517 assert!(ctx.trace);
1518 assert_eq!(ctx.limit, 25);
1519
1520 execute_input("clear", &mut ctx, &config, &test_factory())
1522 .await
1523 .unwrap();
1524 assert_eq!(ctx.chain, "auto");
1525 assert!(!ctx.include_tokens);
1526 assert!(!ctx.trace);
1527 assert_eq!(ctx.limit, 100);
1528 }
1529
1530 #[tokio::test]
1531 async fn test_dot_prefix_commands() {
1532 let config = test_config();
1533 let mut ctx = SessionContext::default();
1534
1535 let result = execute_input(".help", &mut ctx, &config, &test_factory())
1537 .await
1538 .unwrap();
1539 assert!(!result);
1540
1541 execute_input(".chain polygon", &mut ctx, &config, &test_factory())
1542 .await
1543 .unwrap();
1544 assert_eq!(ctx.chain, "polygon");
1545
1546 execute_input(".format json", &mut ctx, &config, &test_factory())
1547 .await
1548 .unwrap();
1549 assert_eq!(ctx.format, OutputFormat::Json);
1550
1551 execute_input(".trace", &mut ctx, &config, &test_factory())
1552 .await
1553 .unwrap();
1554 assert!(ctx.trace);
1555
1556 execute_input(".decode", &mut ctx, &config, &test_factory())
1557 .await
1558 .unwrap();
1559 assert!(ctx.decode);
1560 }
1561
1562 #[tokio::test]
1563 async fn test_all_valid_chains() {
1564 let config = test_config();
1565 let valid_chains = [
1566 "ethereum", "polygon", "arbitrum", "optimism", "base", "bsc", "solana", "tron",
1567 ];
1568 for chain in valid_chains {
1569 let mut ctx = SessionContext::default();
1570 execute_input(
1571 &format!("chain {}", chain),
1572 &mut ctx,
1573 &config,
1574 &test_factory(),
1575 )
1576 .await
1577 .unwrap();
1578 assert_eq!(ctx.chain, chain);
1579 assert!(!ctx.is_auto_chain());
1580 }
1581 }
1582
1583 use crate::chains::mocks::MockClientFactory;
1588
1589 fn mock_factory() -> MockClientFactory {
1590 let mut factory = MockClientFactory::new();
1591 factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
1592 factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
1593 token: crate::chains::Token {
1594 contract_address: "0xtoken".to_string(),
1595 symbol: "TEST".to_string(),
1596 name: "Test Token".to_string(),
1597 decimals: 18,
1598 },
1599 balance: "1000".to_string(),
1600 formatted_balance: "0.001".to_string(),
1601 usd_value: None,
1602 }];
1603 factory
1604 }
1605
1606 #[tokio::test]
1607 async fn test_address_command_with_args() {
1608 let config = test_config();
1609 let factory = mock_factory();
1610 let mut ctx = SessionContext::default();
1611 let result = execute_input(
1612 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1613 &mut ctx,
1614 &config,
1615 &factory,
1616 )
1617 .await;
1618 assert!(result.is_ok());
1619 assert!(!result.unwrap());
1620 assert_eq!(
1621 ctx.last_address,
1622 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
1623 );
1624 }
1625
1626 #[tokio::test]
1627 async fn test_address_command_with_chain_override() {
1628 let config = test_config();
1629 let factory = mock_factory();
1630 let mut ctx = SessionContext::default();
1631 let result = execute_input(
1632 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon",
1633 &mut ctx,
1634 &config,
1635 &factory,
1636 )
1637 .await;
1638 assert!(result.is_ok());
1639 }
1640
1641 #[tokio::test]
1642 async fn test_address_command_with_tokens_flag() {
1643 let config = test_config();
1644 let factory = mock_factory();
1645 let mut ctx = SessionContext::default();
1646 let result = execute_input(
1647 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --tokens",
1648 &mut ctx,
1649 &config,
1650 &factory,
1651 )
1652 .await;
1653 assert!(result.is_ok());
1654 }
1655
1656 #[tokio::test]
1657 async fn test_address_command_with_txs_flag() {
1658 let config = test_config();
1659 let factory = mock_factory();
1660 let mut ctx = SessionContext::default();
1661 let result = execute_input(
1662 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --txs",
1663 &mut ctx,
1664 &config,
1665 &factory,
1666 )
1667 .await;
1668 assert!(result.is_ok());
1669 }
1670
1671 #[tokio::test]
1672 async fn test_address_reuses_last_address() {
1673 let config = test_config();
1674 let factory = mock_factory();
1675 let mut ctx = SessionContext {
1676 last_address: Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string()),
1677 ..Default::default()
1678 };
1679 let result = execute_input("address", &mut ctx, &config, &factory).await;
1680 assert!(result.is_ok());
1681 }
1682
1683 #[tokio::test]
1684 async fn test_address_auto_detects_solana() {
1685 let config = test_config();
1686 let factory = mock_factory();
1687 let mut ctx = SessionContext::default();
1688 let result = execute_input(
1690 "address DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy",
1691 &mut ctx,
1692 &config,
1693 &factory,
1694 )
1695 .await;
1696 assert!(result.is_ok());
1697 assert_eq!(ctx.chain, "auto");
1699 }
1700
1701 #[tokio::test]
1702 async fn test_tx_command_with_args() {
1703 let config = test_config();
1704 let factory = mock_factory();
1705 let mut ctx = SessionContext::default();
1706 let result = execute_input(
1707 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd",
1708 &mut ctx,
1709 &config,
1710 &factory,
1711 )
1712 .await;
1713 assert!(result.is_ok());
1714 assert_eq!(
1715 ctx.last_tx,
1716 Some("0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string())
1717 );
1718 }
1719
1720 #[tokio::test]
1721 async fn test_tx_command_with_trace_decode() {
1722 let config = test_config();
1723 let factory = mock_factory();
1724 let mut ctx = SessionContext::default();
1725 let result = execute_input(
1726 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --trace --decode",
1727 &mut ctx,
1728 &config,
1729 &factory,
1730 )
1731 .await;
1732 assert!(result.is_ok());
1733 }
1734
1735 #[tokio::test]
1736 async fn test_tx_command_with_chain_override() {
1737 let config = test_config();
1738 let factory = mock_factory();
1739 let mut ctx = SessionContext::default();
1740 let result = execute_input(
1741 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --chain=polygon",
1742 &mut ctx,
1743 &config,
1744 &factory,
1745 )
1746 .await;
1747 assert!(result.is_ok());
1748 }
1749
1750 #[tokio::test]
1751 async fn test_tx_reuses_last_tx() {
1752 let config = test_config();
1753 let factory = mock_factory();
1754 let mut ctx = SessionContext {
1755 last_tx: Some(
1756 "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
1757 ),
1758 ..Default::default()
1759 };
1760 let result = execute_input("tx", &mut ctx, &config, &factory).await;
1761 assert!(result.is_ok());
1762 }
1763
1764 #[tokio::test]
1765 async fn test_tx_auto_detects_tron() {
1766 let config = test_config();
1767 let factory = mock_factory();
1768 let mut ctx = SessionContext::default();
1769 let result = execute_input(
1770 "tx abc123def456789012345678901234567890123456789012345678901234abcd",
1771 &mut ctx,
1772 &config,
1773 &factory,
1774 )
1775 .await;
1776 assert!(result.is_ok());
1777 assert_eq!(ctx.chain, "auto");
1779 }
1780
1781 #[tokio::test]
1782 async fn test_crawl_command_with_args() {
1783 let config = test_config();
1784 let factory = mock_factory();
1785 let mut ctx = SessionContext::default();
1786 let result = execute_input(
1787 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
1788 &mut ctx,
1789 &config,
1790 &factory,
1791 )
1792 .await;
1793 assert!(result.is_ok());
1794 }
1795
1796 #[tokio::test]
1797 async fn test_crawl_command_with_period() {
1798 let config = test_config();
1799 let factory = mock_factory();
1800 let mut ctx = SessionContext::default();
1801 let result = execute_input(
1802 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d --no-charts",
1803 &mut ctx,
1804 &config,
1805 &factory,
1806 )
1807 .await;
1808 assert!(result.is_ok());
1809 }
1810
1811 #[tokio::test]
1812 async fn test_crawl_command_with_chain_flag() {
1813 let config = test_config();
1814 let factory = mock_factory();
1815 let mut ctx = SessionContext::default();
1816 let result = execute_input(
1817 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --chain polygon --no-charts",
1818 &mut ctx,
1819 &config,
1820 &factory,
1821 )
1822 .await;
1823 assert!(result.is_ok());
1824 }
1825
1826 #[tokio::test]
1827 async fn test_crawl_command_with_period_flag() {
1828 let config = test_config();
1829 let factory = mock_factory();
1830 let mut ctx = SessionContext::default();
1831 let result = execute_input(
1832 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h --no-charts",
1833 &mut ctx,
1834 &config,
1835 &factory,
1836 )
1837 .await;
1838 assert!(result.is_ok());
1839 }
1840
1841 #[tokio::test]
1842 async fn test_crawl_command_with_report() {
1843 let config = test_config();
1844 let factory = mock_factory();
1845 let mut ctx = SessionContext::default();
1846 let tmp = tempfile::NamedTempFile::new().unwrap();
1847 let result = execute_input(
1848 &format!(
1849 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report {} --no-charts",
1850 tmp.path().display()
1851 ),
1852 &mut ctx,
1853 &config,
1854 &factory,
1855 )
1856 .await;
1857 assert!(result.is_ok());
1858 }
1859
1860 #[tokio::test]
1861 async fn test_portfolio_list_command() {
1862 let tmp_dir = tempfile::tempdir().unwrap();
1863 let config = Config {
1864 address_book: crate::config::AddressBookConfig {
1865 data_dir: Some(tmp_dir.path().to_path_buf()),
1866 },
1867 ..Default::default()
1868 };
1869 let factory = mock_factory();
1870 let mut ctx = SessionContext::default();
1871 let result = execute_input("portfolio list", &mut ctx, &config, &factory).await;
1872 assert!(result.is_ok());
1873 }
1874
1875 #[tokio::test]
1876 async fn test_portfolio_add_command() {
1877 let tmp_dir = tempfile::tempdir().unwrap();
1878 let config = Config {
1879 address_book: crate::config::AddressBookConfig {
1880 data_dir: Some(tmp_dir.path().to_path_buf()),
1881 },
1882 ..Default::default()
1883 };
1884 let factory = mock_factory();
1885 let mut ctx = SessionContext::default();
1886 let result = execute_input(
1887 "portfolio add 0xtest --label mytest",
1888 &mut ctx,
1889 &config,
1890 &factory,
1891 )
1892 .await;
1893 assert!(result.is_ok());
1894 }
1895
1896 #[tokio::test]
1897 async fn test_portfolio_summary_command() {
1898 let tmp_dir = tempfile::tempdir().unwrap();
1899 let config = Config {
1900 address_book: crate::config::AddressBookConfig {
1901 data_dir: Some(tmp_dir.path().to_path_buf()),
1902 },
1903 ..Default::default()
1904 };
1905 let factory = mock_factory();
1906 let mut ctx = SessionContext::default();
1907 execute_input("portfolio add 0xtest", &mut ctx, &config, &factory)
1909 .await
1910 .unwrap();
1911 let result = execute_input("portfolio summary", &mut ctx, &config, &factory).await;
1913 assert!(result.is_ok());
1914 }
1915
1916 #[tokio::test]
1917 async fn test_portfolio_remove_command() {
1918 let tmp_dir = tempfile::tempdir().unwrap();
1919 let config = Config {
1920 address_book: crate::config::AddressBookConfig {
1921 data_dir: Some(tmp_dir.path().to_path_buf()),
1922 },
1923 ..Default::default()
1924 };
1925 let factory = mock_factory();
1926 let mut ctx = SessionContext::default();
1927 let result = execute_input("portfolio remove 0xtest", &mut ctx, &config, &factory).await;
1928 assert!(result.is_ok());
1929 }
1930
1931 #[tokio::test]
1932 async fn test_portfolio_no_subcommand() {
1933 let config = test_config();
1934 let factory = mock_factory();
1935 let mut ctx = SessionContext::default();
1936 let result = execute_input("portfolio", &mut ctx, &config, &factory).await;
1937 assert!(result.is_ok());
1938 }
1939
1940 #[tokio::test]
1941 async fn test_portfolio_unknown_subcommand() {
1942 let tmp_dir = tempfile::tempdir().unwrap();
1943 let config = Config {
1944 address_book: crate::config::AddressBookConfig {
1945 data_dir: Some(tmp_dir.path().to_path_buf()),
1946 },
1947 ..Default::default()
1948 };
1949 let factory = mock_factory();
1950 let mut ctx = SessionContext::default();
1951 let result = execute_input("portfolio foobar", &mut ctx, &config, &factory).await;
1952 assert!(result.is_ok());
1953 }
1954
1955 #[tokio::test]
1956 async fn test_tokens_command_list() {
1957 let config = test_config();
1958 let factory = mock_factory();
1959 let mut ctx = SessionContext::default();
1960 let result = execute_input("tokens list", &mut ctx, &config, &factory).await;
1961 assert!(result.is_ok());
1962 }
1963
1964 #[tokio::test]
1965 async fn test_tokens_command_no_args() {
1966 let config = test_config();
1967 let factory = mock_factory();
1968 let mut ctx = SessionContext::default();
1969 let result = execute_input("tokens", &mut ctx, &config, &factory).await;
1970 assert!(result.is_ok());
1971 }
1972
1973 #[tokio::test]
1974 async fn test_tokens_command_recent() {
1975 let config = test_config();
1976 let factory = mock_factory();
1977 let mut ctx = SessionContext::default();
1978 let result = execute_input("tokens recent", &mut ctx, &config, &factory).await;
1979 assert!(result.is_ok());
1980 }
1981
1982 #[tokio::test]
1983 async fn test_tokens_command_remove_no_args() {
1984 let config = test_config();
1985 let factory = mock_factory();
1986 let mut ctx = SessionContext::default();
1987 let result = execute_input("tokens remove", &mut ctx, &config, &factory).await;
1988 assert!(result.is_ok());
1989 }
1990
1991 #[tokio::test]
1992 async fn test_tokens_command_add_no_args() {
1993 let config = test_config();
1994 let factory = mock_factory();
1995 let mut ctx = SessionContext::default();
1996 let result = execute_input("tokens add", &mut ctx, &config, &factory).await;
1997 assert!(result.is_ok());
1998 }
1999
2000 #[tokio::test]
2001 async fn test_tokens_command_unknown() {
2002 let config = test_config();
2003 let factory = mock_factory();
2004 let mut ctx = SessionContext::default();
2005 let result = execute_input("tokens foobar", &mut ctx, &config, &factory).await;
2006 assert!(result.is_ok());
2007 }
2008
2009 #[tokio::test]
2010 async fn test_setup_command_status() {
2011 let config = test_config();
2012 let factory = mock_factory();
2013 let mut ctx = SessionContext::default();
2014 let result = execute_input("setup --status", &mut ctx, &config, &factory).await;
2015 assert!(result.is_ok());
2016 }
2017
2018 #[tokio::test]
2019 async fn test_transaction_alias() {
2020 let config = test_config();
2021 let factory = mock_factory();
2022 let mut ctx = SessionContext::default();
2023 let result = execute_input(
2024 "transaction 0xabc123def456789012345678901234567890123456789012345678901234abcd",
2025 &mut ctx,
2026 &config,
2027 &factory,
2028 )
2029 .await;
2030 assert!(result.is_ok());
2031 }
2032
2033 #[tokio::test]
2034 async fn test_token_alias_for_crawl() {
2035 let config = test_config();
2036 let factory = mock_factory();
2037 let mut ctx = SessionContext::default();
2038 let result = execute_input(
2039 "token 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
2040 &mut ctx,
2041 &config,
2042 &factory,
2043 )
2044 .await;
2045 assert!(result.is_ok());
2046 }
2047
2048 #[tokio::test]
2049 async fn test_port_alias_for_portfolio() {
2050 let tmp_dir = tempfile::tempdir().unwrap();
2051 let config = Config {
2052 address_book: crate::config::AddressBookConfig {
2053 data_dir: Some(tmp_dir.path().to_path_buf()),
2054 },
2055 ..Default::default()
2056 };
2057 let factory = mock_factory();
2058 let mut ctx = SessionContext::default();
2059 let result = execute_input("port list", &mut ctx, &config, &factory).await;
2060 assert!(result.is_ok());
2061 }
2062
2063 #[tokio::test]
2068 async fn test_execute_tokens_list_empty() {
2069 let result = execute_tokens_command(&[]).await;
2070 assert!(result.is_ok());
2071 }
2072
2073 #[tokio::test]
2074 async fn test_execute_tokens_list_subcommand() {
2075 let result = execute_tokens_command(&["list"]).await;
2076 assert!(result.is_ok());
2077 }
2078
2079 #[tokio::test]
2080 async fn test_execute_tokens_recent() {
2081 let result = execute_tokens_command(&["recent"]).await;
2082 assert!(result.is_ok());
2083 }
2084
2085 #[tokio::test]
2086 async fn test_execute_tokens_add_insufficient_args() {
2087 let result = execute_tokens_command(&["add"]).await;
2088 assert!(result.is_ok());
2089 }
2090
2091 #[tokio::test]
2092 async fn test_execute_tokens_add_success() {
2093 let result = execute_tokens_command(&[
2094 "add",
2095 "TEST_INTERACTIVE",
2096 "ethereum",
2097 "0xtest123456789",
2098 "Test Token",
2099 ])
2100 .await;
2101 assert!(result.is_ok());
2102 let _ = execute_tokens_command(&["remove", "TEST_INTERACTIVE"]).await;
2103 }
2104
2105 #[tokio::test]
2106 async fn test_execute_tokens_remove_no_args() {
2107 let result = execute_tokens_command(&["remove"]).await;
2108 assert!(result.is_ok());
2109 }
2110
2111 #[tokio::test]
2112 async fn test_execute_tokens_remove_with_symbol() {
2113 let _ =
2114 execute_tokens_command(&["add", "RMTEST", "ethereum", "0xrmtest", "Remove Test"]).await;
2115 let result = execute_tokens_command(&["remove", "RMTEST"]).await;
2116 assert!(result.is_ok());
2117 }
2118
2119 #[tokio::test]
2120 async fn test_execute_tokens_unknown_subcommand() {
2121 let result = execute_tokens_command(&["invalid"]).await;
2122 assert!(result.is_ok());
2123 }
2124
2125 #[test]
2130 fn test_session_context_serialization_roundtrip() {
2131 let ctx = SessionContext {
2132 chain: "solana".to_string(),
2133 include_tokens: true,
2134 limit: 25,
2135 last_address: Some("0xtest".to_string()),
2136 ..Default::default()
2137 };
2138
2139 let yaml = serde_yaml::to_string(&ctx).unwrap();
2140 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
2141 assert_eq!(deserialized.chain, "solana");
2142 assert!(deserialized.include_tokens);
2143 assert_eq!(deserialized.limit, 25);
2144 assert_eq!(deserialized.last_address, Some("0xtest".to_string()));
2145 }
2146
2147 #[tokio::test]
2152 async fn test_chain_show_explicit() {
2153 let config = test_config();
2154 let factory = test_factory();
2155 let mut context = SessionContext {
2156 chain: "polygon".to_string(),
2157 ..Default::default()
2158 };
2159
2160 let result = execute_input("chain", &mut context, &config, &factory).await;
2162 assert!(result.is_ok());
2163 assert!(!result.unwrap()); }
2165
2166 #[tokio::test]
2167 async fn test_address_with_explicit_chain() {
2168 let config = test_config();
2169 let factory = mock_factory();
2170 let mut context = SessionContext {
2171 chain: "polygon".to_string(),
2172 ..Default::default()
2173 };
2174
2175 let result = execute_input(
2177 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2178 &mut context,
2179 &config,
2180 &factory,
2181 )
2182 .await;
2183 assert!(result.is_ok() || result.is_err());
2185 }
2186
2187 #[tokio::test]
2188 async fn test_tx_with_explicit_chain() {
2189 let config = test_config();
2190 let factory = mock_factory();
2191 let mut context = SessionContext {
2192 chain: "polygon".to_string(),
2193 ..Default::default()
2194 };
2195
2196 let result = execute_input("tx 0xabc123def456789", &mut context, &config, &factory).await;
2198 assert!(result.is_ok() || result.is_err());
2199 }
2200
2201 #[tokio::test]
2202 async fn test_crawl_with_period_eq_flag() {
2203 let config = test_config();
2204 let factory = test_factory();
2205 let mut context = SessionContext::default();
2206
2207 let result = execute_input(
2209 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d",
2210 &mut context,
2211 &config,
2212 &factory,
2213 )
2214 .await;
2215 assert!(result.is_ok() || result.is_err());
2217 }
2218
2219 #[tokio::test]
2220 async fn test_crawl_with_period_space_flag() {
2221 let config = test_config();
2222 let factory = test_factory();
2223 let mut context = SessionContext::default();
2224
2225 let result = execute_input(
2227 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h",
2228 &mut context,
2229 &config,
2230 &factory,
2231 )
2232 .await;
2233 assert!(result.is_ok() || result.is_err());
2234 }
2235
2236 #[tokio::test]
2237 async fn test_crawl_with_chain_eq_flag() {
2238 let config = test_config();
2239 let factory = test_factory();
2240 let mut context = SessionContext::default();
2241
2242 let result = execute_input(
2244 "crawl 0xAddress --chain=polygon",
2245 &mut context,
2246 &config,
2247 &factory,
2248 )
2249 .await;
2250 assert!(result.is_ok() || result.is_err());
2251 }
2252
2253 #[tokio::test]
2254 async fn test_crawl_with_chain_space_flag() {
2255 let config = test_config();
2256 let factory = test_factory();
2257 let mut context = SessionContext::default();
2258
2259 let result = execute_input(
2261 "crawl 0xAddress --chain polygon",
2262 &mut context,
2263 &config,
2264 &factory,
2265 )
2266 .await;
2267 assert!(result.is_ok() || result.is_err());
2268 }
2269
2270 #[tokio::test]
2271 async fn test_crawl_with_report_flag() {
2272 let config = test_config();
2273 let factory = test_factory();
2274 let mut context = SessionContext::default();
2275
2276 let tmp = tempfile::NamedTempFile::new().unwrap();
2277 let path = tmp.path().to_string_lossy();
2278 let input = format!(
2279 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report={}",
2280 path
2281 );
2282 let result = execute_input(&input, &mut context, &config, &factory).await;
2283 assert!(result.is_ok() || result.is_err());
2284 }
2285
2286 #[tokio::test]
2287 async fn test_crawl_with_no_charts_flag() {
2288 let config = test_config();
2289 let factory = test_factory();
2290 let mut context = SessionContext::default();
2291
2292 let result = execute_input(
2293 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
2294 &mut context,
2295 &config,
2296 &factory,
2297 )
2298 .await;
2299 assert!(result.is_ok() || result.is_err());
2300 }
2301
2302 #[tokio::test]
2303 async fn test_crawl_with_explicit_chain() {
2304 let config = test_config();
2305 let factory = test_factory();
2306 let mut context = SessionContext {
2307 chain: "arbitrum".to_string(),
2308 ..Default::default()
2309 };
2310
2311 let result = execute_input("crawl 0xAddress", &mut context, &config, &factory).await;
2312 assert!(result.is_ok() || result.is_err());
2313 }
2314
2315 #[tokio::test]
2316 async fn test_portfolio_add_with_label_and_tags() {
2317 let tmp_dir = tempfile::tempdir().unwrap();
2318 let config = Config {
2319 address_book: crate::config::AddressBookConfig {
2320 data_dir: Some(tmp_dir.path().to_path_buf()),
2321 },
2322 ..Default::default()
2323 };
2324 let factory = mock_factory();
2325 let mut context = SessionContext::default();
2326
2327 let result = execute_input(
2328 "portfolio add 0xAbC123 --label MyWallet --tags defi,staking",
2329 &mut context,
2330 &config,
2331 &factory,
2332 )
2333 .await;
2334 assert!(result.is_ok());
2335 }
2336
2337 #[tokio::test]
2338 async fn test_portfolio_remove_no_args() {
2339 let tmp_dir = tempfile::tempdir().unwrap();
2340 let config = Config {
2341 address_book: crate::config::AddressBookConfig {
2342 data_dir: Some(tmp_dir.path().to_path_buf()),
2343 },
2344 ..Default::default()
2345 };
2346 let factory = mock_factory();
2347 let mut context = SessionContext::default();
2348
2349 let result = execute_input("portfolio remove", &mut context, &config, &factory).await;
2350 assert!(result.is_ok());
2351 }
2352
2353 #[tokio::test]
2354 async fn test_portfolio_summary_with_chain_and_tag() {
2355 let tmp_dir = tempfile::tempdir().unwrap();
2356 let config = Config {
2357 address_book: crate::config::AddressBookConfig {
2358 data_dir: Some(tmp_dir.path().to_path_buf()),
2359 },
2360 ..Default::default()
2361 };
2362 let factory = mock_factory();
2363 let mut context = SessionContext::default();
2364
2365 let result = execute_input(
2366 "portfolio summary --chain ethereum --tag defi --tokens",
2367 &mut context,
2368 &config,
2369 &factory,
2370 )
2371 .await;
2372 assert!(result.is_ok());
2373 }
2374
2375 #[tokio::test]
2376 async fn test_tokens_add_with_name() {
2377 let result = execute_tokens_command(&[
2378 "add",
2379 "USDC",
2380 "ethereum",
2381 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2382 "USD",
2383 "Coin",
2384 ])
2385 .await;
2386 assert!(result.is_ok());
2387 }
2388
2389 #[tokio::test]
2390 async fn test_tokens_remove_with_chain() {
2391 let result = execute_tokens_command(&["remove", "USDC", "--chain", "ethereum"]).await;
2392 assert!(result.is_ok());
2393 }
2394
2395 #[tokio::test]
2396 async fn test_tokens_add_then_list_nonempty() {
2397 let _ = execute_tokens_command(&[
2399 "add",
2400 "TEST_TOKEN_XYZ",
2401 "ethereum",
2402 "0x1234567890abcdef1234567890abcdef12345678",
2403 "Test",
2404 "Token",
2405 ])
2406 .await;
2407
2408 let result = execute_tokens_command(&["list"]).await;
2410 assert!(result.is_ok());
2411
2412 let result = execute_tokens_command(&["recent"]).await;
2414 assert!(result.is_ok());
2415
2416 let _ = execute_tokens_command(&["remove", "TEST_TOKEN_XYZ"]).await;
2418 }
2419
2420 #[tokio::test]
2421 async fn test_session_context_save_and_load() {
2422 let ctx = SessionContext {
2425 chain: "solana".to_string(),
2426 last_address: Some("0xabc".to_string()),
2427 last_tx: Some("0xdef".to_string()),
2428 ..Default::default()
2429 };
2430 let _ = ctx.save();
2432 let loaded = SessionContext::load();
2434 assert!(!loaded.chain.is_empty());
2436 }
2437
2438 #[tokio::test]
2443 async fn test_help_alias_question_mark() {
2444 let config = test_config();
2445 let mut ctx = SessionContext::default();
2446 let result = execute_input("?", &mut ctx, &config, &test_factory())
2447 .await
2448 .unwrap();
2449 assert!(!result);
2450 }
2451
2452 #[tokio::test]
2453 async fn test_context_alias() {
2454 let config = test_config();
2455 let mut ctx = SessionContext::default();
2456 let result = execute_input("context", &mut ctx, &config, &test_factory())
2457 .await
2458 .unwrap();
2459 assert!(!result);
2460 }
2461
2462 #[tokio::test]
2463 async fn test_dot_context_alias() {
2464 let config = test_config();
2465 let mut ctx = SessionContext::default();
2466 let result = execute_input(".context", &mut ctx, &config, &test_factory())
2467 .await
2468 .unwrap();
2469 assert!(!result);
2470 }
2471
2472 #[tokio::test]
2473 async fn test_reset_alias() {
2474 let config = test_config();
2475 let mut ctx = SessionContext {
2476 chain: "ethereum".to_string(),
2477 ..Default::default()
2478 };
2479 execute_input("reset", &mut ctx, &config, &test_factory())
2480 .await
2481 .unwrap();
2482 assert_eq!(ctx.chain, "auto");
2483 }
2484
2485 #[tokio::test]
2486 async fn test_dot_reset_alias() {
2487 let config = test_config();
2488 let mut ctx = SessionContext {
2489 chain: "base".to_string(),
2490 ..Default::default()
2491 };
2492 execute_input(".reset", &mut ctx, &config, &test_factory())
2493 .await
2494 .unwrap();
2495 assert_eq!(ctx.chain, "auto");
2496 }
2497
2498 #[tokio::test]
2499 async fn test_dot_clear_alias() {
2500 let config = test_config();
2501 let mut ctx = SessionContext {
2502 chain: "bsc".to_string(),
2503 ..Default::default()
2504 };
2505 execute_input(".clear", &mut ctx, &config, &test_factory())
2506 .await
2507 .unwrap();
2508 assert_eq!(ctx.chain, "auto");
2509 }
2510
2511 #[tokio::test]
2512 async fn test_showtokens_alias() {
2513 let config = test_config();
2514 let mut ctx = SessionContext::default();
2515 execute_input("showtokens", &mut ctx, &config, &test_factory())
2516 .await
2517 .unwrap();
2518 assert!(ctx.include_tokens);
2519 }
2520
2521 #[tokio::test]
2522 async fn test_showtxs_alias() {
2523 let config = test_config();
2524 let mut ctx = SessionContext::default();
2525 execute_input("showtxs", &mut ctx, &config, &test_factory())
2526 .await
2527 .unwrap();
2528 assert!(ctx.include_txs);
2529 }
2530
2531 #[tokio::test]
2532 async fn test_txs_alias() {
2533 let config = test_config();
2534 let mut ctx = SessionContext::default();
2535 execute_input("txs", &mut ctx, &config, &test_factory())
2536 .await
2537 .unwrap();
2538 assert!(ctx.include_txs);
2539 }
2540
2541 #[tokio::test]
2542 async fn test_dot_txs_alias() {
2543 let config = test_config();
2544 let mut ctx = SessionContext::default();
2545 execute_input(".txs", &mut ctx, &config, &test_factory())
2546 .await
2547 .unwrap();
2548 assert!(ctx.include_txs);
2549 }
2550
2551 #[tokio::test]
2552 async fn test_addr_alias() {
2553 let config = test_config();
2554 let factory = mock_factory();
2555 let mut ctx = SessionContext::default();
2556 let result = execute_input(
2557 "addr 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2558 &mut ctx,
2559 &config,
2560 &factory,
2561 )
2562 .await;
2563 assert!(result.is_ok());
2564 assert_eq!(
2565 ctx.last_address,
2566 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
2567 );
2568 }
2569
2570 #[test]
2571 fn test_session_context_is_auto_chain() {
2572 let auto_ctx = SessionContext::default();
2573 assert!(auto_ctx.is_auto_chain());
2574 let pinned_ctx = SessionContext {
2575 chain: "ethereum".to_string(),
2576 ..Default::default()
2577 };
2578 assert!(!pinned_ctx.is_auto_chain());
2579 }
2580
2581 #[test]
2582 fn test_print_help_no_panic() {
2583 print_help();
2584 }
2585
2586 #[tokio::test]
2591 async fn test_contract_no_args() {
2592 let config = test_config();
2593 let factory = mock_factory();
2594 let mut ctx = SessionContext::default();
2595 let result = execute_input("contract", &mut ctx, &config, &factory).await;
2596 assert!(result.is_ok());
2597 assert!(!result.unwrap());
2598 }
2599
2600 #[tokio::test]
2601 async fn test_contract_ct_alias_with_args() {
2602 let config = test_config();
2603 let factory = mock_factory();
2604 let mut ctx = SessionContext::default();
2605 let result = execute_input(
2606 "ct 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2607 &mut ctx,
2608 &config,
2609 &factory,
2610 )
2611 .await;
2612 if let Ok(should_exit) = result {
2613 assert!(!should_exit);
2614 }
2615 }
2616
2617 #[tokio::test]
2618 async fn test_contract_with_chain_and_json() {
2619 let config = test_config();
2620 let factory = mock_factory();
2621 let mut ctx = SessionContext::default();
2622 let result = execute_input(
2623 "contract 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon --json",
2624 &mut ctx,
2625 &config,
2626 &factory,
2627 )
2628 .await;
2629 if let Ok(should_exit) = result {
2630 assert!(!should_exit);
2631 }
2632 }
2633
2634 #[tokio::test]
2639 async fn test_address_book_list_command() {
2640 let tmp_dir = tempfile::tempdir().unwrap();
2641 let config = Config {
2642 address_book: crate::config::AddressBookConfig {
2643 data_dir: Some(tmp_dir.path().to_path_buf()),
2644 },
2645 ..Default::default()
2646 };
2647 let factory = mock_factory();
2648 let mut ctx = SessionContext::default();
2649 let result = execute_input("address-book list", &mut ctx, &config, &factory).await;
2650 assert!(result.is_ok());
2651 }
2652
2653 #[tokio::test]
2654 async fn test_address_book_underscore_list() {
2655 let tmp_dir = tempfile::tempdir().unwrap();
2656 let config = Config {
2657 address_book: crate::config::AddressBookConfig {
2658 data_dir: Some(tmp_dir.path().to_path_buf()),
2659 },
2660 ..Default::default()
2661 };
2662 let factory = mock_factory();
2663 let mut ctx = SessionContext::default();
2664 let result = execute_input("address_book list", &mut ctx, &config, &factory).await;
2665 assert!(result.is_ok());
2666 }
2667
2668 #[tokio::test]
2669 async fn test_address_book_add_insufficient_args() {
2670 let tmp_dir = tempfile::tempdir().unwrap();
2671 let config = Config {
2672 address_book: crate::config::AddressBookConfig {
2673 data_dir: Some(tmp_dir.path().to_path_buf()),
2674 },
2675 ..Default::default()
2676 };
2677 let factory = mock_factory();
2678 let mut ctx = SessionContext::default();
2679 let result = execute_input("address-book add", &mut ctx, &config, &factory).await;
2680 assert!(result.is_ok());
2681 }
2682
2683 #[tokio::test]
2684 async fn test_address_book_remove_insufficient_args() {
2685 let tmp_dir = tempfile::tempdir().unwrap();
2686 let config = Config {
2687 address_book: crate::config::AddressBookConfig {
2688 data_dir: Some(tmp_dir.path().to_path_buf()),
2689 },
2690 ..Default::default()
2691 };
2692 let factory = mock_factory();
2693 let mut ctx = SessionContext::default();
2694 let result = execute_input("address-book remove", &mut ctx, &config, &factory).await;
2695 assert!(result.is_ok());
2696 }
2697
2698 #[tokio::test]
2699 async fn test_address_book_empty_subcommand() {
2700 let tmp_dir = tempfile::tempdir().unwrap();
2701 let config = Config {
2702 address_book: crate::config::AddressBookConfig {
2703 data_dir: Some(tmp_dir.path().to_path_buf()),
2704 },
2705 ..Default::default()
2706 };
2707 let factory = mock_factory();
2708 let mut ctx = SessionContext::default();
2709 let result = execute_input("address-book", &mut ctx, &config, &factory).await;
2710 assert!(result.is_ok());
2711 }
2712
2713 #[tokio::test]
2718 async fn test_aliases_command() {
2719 let config = test_config();
2720 let factory = mock_factory();
2721 let mut ctx = SessionContext::default();
2722 let result = execute_input("aliases", &mut ctx, &config, &factory).await;
2723 assert!(result.is_ok());
2724 }
2725
2726 #[tokio::test]
2727 async fn test_config_alias() {
2728 let config = test_config();
2729 let factory = mock_factory();
2730 let mut ctx = SessionContext::default();
2731 let result = execute_input("config --status", &mut ctx, &config, &factory).await;
2732 assert!(result.is_ok());
2733 }
2734
2735 #[tokio::test]
2736 #[ignore = "setup --key prompts for API key input on stdin"]
2737 async fn test_setup_with_key_flag() {
2738 let config = test_config();
2739 let factory = mock_factory();
2740 let mut ctx = SessionContext::default();
2741 let result = execute_input("setup --key=etherscan", &mut ctx, &config, &factory).await;
2742 assert!(result.is_ok());
2743 }
2744
2745 #[tokio::test]
2746 async fn test_setup_with_key_short_flag() {
2747 let config = test_config();
2748 let factory = mock_factory();
2749 let mut ctx = SessionContext::default();
2750 let result = execute_input("setup -s", &mut ctx, &config, &factory).await;
2751 assert!(result.is_ok());
2752 }
2753
2754 #[tokio::test]
2758 #[ignore = "monitor starts TUI and blocks until exit"]
2759 async fn test_monitor_command_no_token() {
2760 let config = test_config();
2761 let factory = mock_factory();
2762 let mut ctx = SessionContext::default();
2763 let result = execute_input("monitor", &mut ctx, &config, &factory).await;
2764 assert!(result.is_ok() || result.is_err());
2765 }
2766
2767 #[tokio::test]
2768 #[ignore = "monitor starts TUI and blocks until exit"]
2769 async fn test_mon_alias() {
2770 let config = test_config();
2771 let factory = mock_factory();
2772 let mut ctx = SessionContext::default();
2773 let result = execute_input(
2774 "mon 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2775 &mut ctx,
2776 &config,
2777 &factory,
2778 )
2779 .await;
2780 assert!(result.is_ok() || result.is_err());
2781 }
2782
2783 #[tokio::test]
2788 async fn test_tokens_ls_alias() {
2789 let config = test_config();
2790 let factory = mock_factory();
2791 let mut ctx = SessionContext::default();
2792 let result = execute_input("tokens ls", &mut ctx, &config, &factory).await;
2793 assert!(result.is_ok());
2794 }
2795
2796 #[tokio::test]
2797 async fn test_execute_tokens_ls_alias() {
2798 let result = execute_tokens_command(&["ls"]).await;
2799 assert!(result.is_ok());
2800 }
2801
2802 #[tokio::test]
2803 async fn test_crawl_period_1h() {
2804 let config = test_config();
2805 let factory = mock_factory();
2806 let mut ctx = SessionContext::default();
2807 let result = execute_input(
2808 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=1h --no-charts",
2809 &mut ctx,
2810 &config,
2811 &factory,
2812 )
2813 .await;
2814 assert!(result.is_ok());
2815 }
2816
2817 #[tokio::test]
2818 async fn test_crawl_period_30d() {
2819 let config = test_config();
2820 let factory = mock_factory();
2821 let mut ctx = SessionContext::default();
2822 let result = execute_input(
2823 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=30d --no-charts",
2824 &mut ctx,
2825 &config,
2826 &factory,
2827 )
2828 .await;
2829 assert!(result.is_ok());
2830 }
2831
2832 #[tokio::test]
2833 async fn test_crawl_invalid_period_defaults() {
2834 let config = test_config();
2835 let factory = mock_factory();
2836 let mut ctx = SessionContext::default();
2837 let result = execute_input(
2838 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=invalid --no-charts",
2839 &mut ctx,
2840 &config,
2841 &factory,
2842 )
2843 .await;
2844 assert!(result.is_ok());
2845 }
2846
2847 #[tokio::test]
2848 async fn test_tokens_add_three_args_insufficient() {
2849 let result = execute_tokens_command(&["add", "SYM", "ethereum"]).await;
2850 assert!(result.is_ok());
2851 }
2852
2853 #[tokio::test]
2854 async fn test_format_show_when_csv() {
2855 let config = test_config();
2856 let mut ctx = SessionContext {
2857 format: OutputFormat::Csv,
2858 ..Default::default()
2859 };
2860 let result = execute_input("format", &mut ctx, &config, &test_factory())
2861 .await
2862 .unwrap();
2863 assert!(!result);
2864 }
2865}