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 let http: std::sync::Arc<dyn crate::http::HttpClient> =
1168 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
1169 crate::chains::DefaultClientFactory {
1170 chains_config: crate::config::ChainsConfig::default(),
1171 http,
1172 }
1173 }
1174
1175 #[tokio::test]
1176 async fn test_exit_commands() {
1177 let config = test_config();
1178 for cmd in &["exit", "quit", "q", ".exit", ".quit"] {
1179 let mut ctx = SessionContext::default();
1180 let result = execute_input(cmd, &mut ctx, &config, &test_factory())
1181 .await
1182 .unwrap();
1183 assert!(result, "'{cmd}' should return true (exit)");
1184 }
1185 }
1186
1187 #[tokio::test]
1188 async fn test_help_command() {
1189 let config = test_config();
1190 let mut ctx = SessionContext::default();
1191 let result = execute_input("help", &mut ctx, &config, &test_factory())
1192 .await
1193 .unwrap();
1194 assert!(!result);
1195 }
1196
1197 #[tokio::test]
1198 async fn test_context_command() {
1199 let config = test_config();
1200 let mut ctx = SessionContext::default();
1201 let result = execute_input("ctx", &mut ctx, &config, &test_factory())
1202 .await
1203 .unwrap();
1204 assert!(!result);
1205 }
1206
1207 #[tokio::test]
1208 async fn test_clear_command() {
1209 let config = test_config();
1210 let mut ctx = SessionContext {
1211 chain: "polygon".to_string(),
1212 include_tokens: true,
1213 limit: 42,
1214 ..Default::default()
1215 };
1216
1217 let result = execute_input("clear", &mut ctx, &config, &test_factory())
1218 .await
1219 .unwrap();
1220 assert!(!result);
1221 assert_eq!(ctx.chain, "auto");
1222 assert!(!ctx.include_tokens);
1223 assert_eq!(ctx.limit, 100);
1224 }
1225
1226 #[tokio::test]
1227 async fn test_chain_set_valid() {
1228 let config = test_config();
1229 let mut ctx = SessionContext::default();
1230
1231 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1232 .await
1233 .unwrap();
1234 assert_eq!(ctx.chain, "polygon");
1235 assert!(!ctx.is_auto_chain());
1236 }
1237
1238 #[tokio::test]
1239 async fn test_chain_set_solana() {
1240 let config = test_config();
1241 let mut ctx = SessionContext::default();
1242
1243 execute_input("chain solana", &mut ctx, &config, &test_factory())
1244 .await
1245 .unwrap();
1246 assert_eq!(ctx.chain, "solana");
1247 assert!(!ctx.is_auto_chain());
1248 }
1249
1250 #[tokio::test]
1251 async fn test_chain_auto() {
1252 let config = test_config();
1253 let mut ctx = SessionContext {
1254 chain: "polygon".to_string(),
1255 ..Default::default()
1256 };
1257
1258 execute_input("chain auto", &mut ctx, &config, &test_factory())
1259 .await
1260 .unwrap();
1261 assert_eq!(ctx.chain, "auto");
1262 assert!(ctx.is_auto_chain());
1263 }
1264
1265 #[tokio::test]
1266 async fn test_chain_invalid() {
1267 let config = test_config();
1268 let mut ctx = SessionContext::default();
1269 execute_input("chain foobar", &mut ctx, &config, &test_factory())
1271 .await
1272 .unwrap();
1273 assert_eq!(ctx.chain, "auto");
1274 assert!(ctx.is_auto_chain());
1275 }
1276
1277 #[tokio::test]
1278 async fn test_chain_show() {
1279 let config = test_config();
1280 let mut ctx = SessionContext::default();
1281 let result = execute_input("chain", &mut ctx, &config, &test_factory())
1283 .await
1284 .unwrap();
1285 assert!(!result);
1286 assert_eq!(ctx.chain, "auto");
1287 }
1288
1289 #[tokio::test]
1290 async fn test_format_set_json() {
1291 let config = test_config();
1292 let mut ctx = SessionContext::default();
1293 execute_input("format json", &mut ctx, &config, &test_factory())
1294 .await
1295 .unwrap();
1296 assert_eq!(ctx.format, OutputFormat::Json);
1297 }
1298
1299 #[tokio::test]
1300 async fn test_format_set_csv() {
1301 let config = test_config();
1302 let mut ctx = SessionContext::default();
1303 execute_input("format csv", &mut ctx, &config, &test_factory())
1304 .await
1305 .unwrap();
1306 assert_eq!(ctx.format, OutputFormat::Csv);
1307 }
1308
1309 #[tokio::test]
1310 async fn test_format_set_table() {
1311 let config = test_config();
1312 let mut ctx = SessionContext {
1313 format: OutputFormat::Json,
1314 ..Default::default()
1315 };
1316 execute_input("format table", &mut ctx, &config, &test_factory())
1317 .await
1318 .unwrap();
1319 assert_eq!(ctx.format, OutputFormat::Table);
1320 }
1321
1322 #[tokio::test]
1323 async fn test_format_invalid() {
1324 let config = test_config();
1325 let mut ctx = SessionContext::default();
1326 execute_input("format xml", &mut ctx, &config, &test_factory())
1327 .await
1328 .unwrap();
1329 assert_eq!(ctx.format, OutputFormat::Table);
1331 }
1332
1333 #[tokio::test]
1334 async fn test_format_show() {
1335 let config = test_config();
1336 let mut ctx = SessionContext::default();
1337 let result = execute_input("format", &mut ctx, &config, &test_factory())
1338 .await
1339 .unwrap();
1340 assert!(!result);
1341 }
1342
1343 #[tokio::test]
1344 async fn test_toggle_tokens() {
1345 let config = test_config();
1346 let mut ctx = SessionContext::default();
1347 assert!(!ctx.include_tokens);
1348
1349 execute_input("+tokens", &mut ctx, &config, &test_factory())
1350 .await
1351 .unwrap();
1352 assert!(ctx.include_tokens);
1353
1354 execute_input("+tokens", &mut ctx, &config, &test_factory())
1355 .await
1356 .unwrap();
1357 assert!(!ctx.include_tokens);
1358 }
1359
1360 #[tokio::test]
1361 async fn test_toggle_txs() {
1362 let config = test_config();
1363 let mut ctx = SessionContext::default();
1364 assert!(!ctx.include_txs);
1365
1366 execute_input("+txs", &mut ctx, &config, &test_factory())
1367 .await
1368 .unwrap();
1369 assert!(ctx.include_txs);
1370
1371 execute_input("+txs", &mut ctx, &config, &test_factory())
1372 .await
1373 .unwrap();
1374 assert!(!ctx.include_txs);
1375 }
1376
1377 #[tokio::test]
1378 async fn test_toggle_trace() {
1379 let config = test_config();
1380 let mut ctx = SessionContext::default();
1381 assert!(!ctx.trace);
1382
1383 execute_input("trace", &mut ctx, &config, &test_factory())
1384 .await
1385 .unwrap();
1386 assert!(ctx.trace);
1387
1388 execute_input("trace", &mut ctx, &config, &test_factory())
1389 .await
1390 .unwrap();
1391 assert!(!ctx.trace);
1392 }
1393
1394 #[tokio::test]
1395 async fn test_toggle_decode() {
1396 let config = test_config();
1397 let mut ctx = SessionContext::default();
1398 assert!(!ctx.decode);
1399
1400 execute_input("decode", &mut ctx, &config, &test_factory())
1401 .await
1402 .unwrap();
1403 assert!(ctx.decode);
1404
1405 execute_input("decode", &mut ctx, &config, &test_factory())
1406 .await
1407 .unwrap();
1408 assert!(!ctx.decode);
1409 }
1410
1411 #[tokio::test]
1412 async fn test_limit_set_valid() {
1413 let config = test_config();
1414 let mut ctx = SessionContext::default();
1415 execute_input("limit 50", &mut ctx, &config, &test_factory())
1416 .await
1417 .unwrap();
1418 assert_eq!(ctx.limit, 50);
1419 }
1420
1421 #[tokio::test]
1422 async fn test_limit_set_invalid() {
1423 let config = test_config();
1424 let mut ctx = SessionContext::default();
1425 execute_input("limit abc", &mut ctx, &config, &test_factory())
1426 .await
1427 .unwrap();
1428 assert_eq!(ctx.limit, 100);
1430 }
1431
1432 #[tokio::test]
1433 async fn test_limit_show() {
1434 let config = test_config();
1435 let mut ctx = SessionContext::default();
1436 let result = execute_input("limit", &mut ctx, &config, &test_factory())
1437 .await
1438 .unwrap();
1439 assert!(!result);
1440 }
1441
1442 #[tokio::test]
1443 async fn test_unknown_command() {
1444 let config = test_config();
1445 let mut ctx = SessionContext::default();
1446 let result = execute_input("foobar", &mut ctx, &config, &test_factory())
1447 .await
1448 .unwrap();
1449 assert!(!result);
1450 }
1451
1452 #[tokio::test]
1453 async fn test_empty_input() {
1454 let config = test_config();
1455 let mut ctx = SessionContext::default();
1456 let result = execute_input("", &mut ctx, &config, &test_factory())
1457 .await
1458 .unwrap();
1459 assert!(!result);
1460 }
1461
1462 #[tokio::test]
1463 async fn test_address_no_arg_no_last() {
1464 let config = test_config();
1465 let mut ctx = SessionContext::default();
1466 let result = execute_input("address", &mut ctx, &config, &test_factory())
1468 .await
1469 .unwrap();
1470 assert!(!result);
1471 }
1472
1473 #[tokio::test]
1474 async fn test_tx_no_arg_no_last() {
1475 let config = test_config();
1476 let mut ctx = SessionContext::default();
1477 let result = execute_input("tx", &mut ctx, &config, &test_factory())
1479 .await
1480 .unwrap();
1481 assert!(!result);
1482 }
1483
1484 #[tokio::test]
1485 async fn test_crawl_no_arg() {
1486 let config = test_config();
1487 let mut ctx = SessionContext::default();
1488 let result = execute_input("crawl", &mut ctx, &config, &test_factory())
1490 .await
1491 .unwrap();
1492 assert!(!result);
1493 }
1494
1495 #[tokio::test]
1496 async fn test_multiple_context_commands() {
1497 let config = test_config();
1498 let mut ctx = SessionContext::default();
1499
1500 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1502 .await
1503 .unwrap();
1504 execute_input("format json", &mut ctx, &config, &test_factory())
1505 .await
1506 .unwrap();
1507 execute_input("+tokens", &mut ctx, &config, &test_factory())
1508 .await
1509 .unwrap();
1510 execute_input("trace", &mut ctx, &config, &test_factory())
1511 .await
1512 .unwrap();
1513 execute_input("limit 25", &mut ctx, &config, &test_factory())
1514 .await
1515 .unwrap();
1516
1517 assert_eq!(ctx.chain, "polygon");
1518 assert_eq!(ctx.format, OutputFormat::Json);
1519 assert!(ctx.include_tokens);
1520 assert!(ctx.trace);
1521 assert_eq!(ctx.limit, 25);
1522
1523 execute_input("clear", &mut ctx, &config, &test_factory())
1525 .await
1526 .unwrap();
1527 assert_eq!(ctx.chain, "auto");
1528 assert!(!ctx.include_tokens);
1529 assert!(!ctx.trace);
1530 assert_eq!(ctx.limit, 100);
1531 }
1532
1533 #[tokio::test]
1534 async fn test_dot_prefix_commands() {
1535 let config = test_config();
1536 let mut ctx = SessionContext::default();
1537
1538 let result = execute_input(".help", &mut ctx, &config, &test_factory())
1540 .await
1541 .unwrap();
1542 assert!(!result);
1543
1544 execute_input(".chain polygon", &mut ctx, &config, &test_factory())
1545 .await
1546 .unwrap();
1547 assert_eq!(ctx.chain, "polygon");
1548
1549 execute_input(".format json", &mut ctx, &config, &test_factory())
1550 .await
1551 .unwrap();
1552 assert_eq!(ctx.format, OutputFormat::Json);
1553
1554 execute_input(".trace", &mut ctx, &config, &test_factory())
1555 .await
1556 .unwrap();
1557 assert!(ctx.trace);
1558
1559 execute_input(".decode", &mut ctx, &config, &test_factory())
1560 .await
1561 .unwrap();
1562 assert!(ctx.decode);
1563 }
1564
1565 #[tokio::test]
1566 async fn test_all_valid_chains() {
1567 let config = test_config();
1568 let valid_chains = [
1569 "ethereum", "polygon", "arbitrum", "optimism", "base", "bsc", "solana", "tron",
1570 ];
1571 for chain in valid_chains {
1572 let mut ctx = SessionContext::default();
1573 execute_input(
1574 &format!("chain {}", chain),
1575 &mut ctx,
1576 &config,
1577 &test_factory(),
1578 )
1579 .await
1580 .unwrap();
1581 assert_eq!(ctx.chain, chain);
1582 assert!(!ctx.is_auto_chain());
1583 }
1584 }
1585
1586 use crate::chains::mocks::MockClientFactory;
1591
1592 fn mock_factory() -> MockClientFactory {
1593 let mut factory = MockClientFactory::new();
1594 factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
1595 factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
1596 token: crate::chains::Token {
1597 contract_address: "0xtoken".to_string(),
1598 symbol: "TEST".to_string(),
1599 name: "Test Token".to_string(),
1600 decimals: 18,
1601 },
1602 balance: "1000".to_string(),
1603 formatted_balance: "0.001".to_string(),
1604 usd_value: None,
1605 }];
1606 factory
1607 }
1608
1609 #[tokio::test]
1610 async fn test_address_command_with_args() {
1611 let config = test_config();
1612 let factory = mock_factory();
1613 let mut ctx = SessionContext::default();
1614 let result = execute_input(
1615 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1616 &mut ctx,
1617 &config,
1618 &factory,
1619 )
1620 .await;
1621 assert!(result.is_ok());
1622 assert!(!result.unwrap());
1623 assert_eq!(
1624 ctx.last_address,
1625 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
1626 );
1627 }
1628
1629 #[tokio::test]
1630 async fn test_address_command_with_chain_override() {
1631 let config = test_config();
1632 let factory = mock_factory();
1633 let mut ctx = SessionContext::default();
1634 let result = execute_input(
1635 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon",
1636 &mut ctx,
1637 &config,
1638 &factory,
1639 )
1640 .await;
1641 assert!(result.is_ok());
1642 }
1643
1644 #[tokio::test]
1645 async fn test_address_command_with_tokens_flag() {
1646 let config = test_config();
1647 let factory = mock_factory();
1648 let mut ctx = SessionContext::default();
1649 let result = execute_input(
1650 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --tokens",
1651 &mut ctx,
1652 &config,
1653 &factory,
1654 )
1655 .await;
1656 assert!(result.is_ok());
1657 }
1658
1659 #[tokio::test]
1660 async fn test_address_command_with_txs_flag() {
1661 let config = test_config();
1662 let factory = mock_factory();
1663 let mut ctx = SessionContext::default();
1664 let result = execute_input(
1665 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --txs",
1666 &mut ctx,
1667 &config,
1668 &factory,
1669 )
1670 .await;
1671 assert!(result.is_ok());
1672 }
1673
1674 #[tokio::test]
1675 async fn test_address_reuses_last_address() {
1676 let config = test_config();
1677 let factory = mock_factory();
1678 let mut ctx = SessionContext {
1679 last_address: Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string()),
1680 ..Default::default()
1681 };
1682 let result = execute_input("address", &mut ctx, &config, &factory).await;
1683 assert!(result.is_ok());
1684 }
1685
1686 #[tokio::test]
1687 async fn test_address_auto_detects_solana() {
1688 let config = test_config();
1689 let factory = mock_factory();
1690 let mut ctx = SessionContext::default();
1691 let result = execute_input(
1693 "address DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy",
1694 &mut ctx,
1695 &config,
1696 &factory,
1697 )
1698 .await;
1699 assert!(result.is_ok());
1700 assert_eq!(ctx.chain, "auto");
1702 }
1703
1704 #[tokio::test]
1705 async fn test_tx_command_with_args() {
1706 let config = test_config();
1707 let factory = mock_factory();
1708 let mut ctx = SessionContext::default();
1709 let result = execute_input(
1710 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd",
1711 &mut ctx,
1712 &config,
1713 &factory,
1714 )
1715 .await;
1716 assert!(result.is_ok());
1717 assert_eq!(
1718 ctx.last_tx,
1719 Some("0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string())
1720 );
1721 }
1722
1723 #[tokio::test]
1724 async fn test_tx_command_with_trace_decode() {
1725 let config = test_config();
1726 let factory = mock_factory();
1727 let mut ctx = SessionContext::default();
1728 let result = execute_input(
1729 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --trace --decode",
1730 &mut ctx,
1731 &config,
1732 &factory,
1733 )
1734 .await;
1735 assert!(result.is_ok());
1736 }
1737
1738 #[tokio::test]
1739 async fn test_tx_command_with_chain_override() {
1740 let config = test_config();
1741 let factory = mock_factory();
1742 let mut ctx = SessionContext::default();
1743 let result = execute_input(
1744 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --chain=polygon",
1745 &mut ctx,
1746 &config,
1747 &factory,
1748 )
1749 .await;
1750 assert!(result.is_ok());
1751 }
1752
1753 #[tokio::test]
1754 async fn test_tx_reuses_last_tx() {
1755 let config = test_config();
1756 let factory = mock_factory();
1757 let mut ctx = SessionContext {
1758 last_tx: Some(
1759 "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
1760 ),
1761 ..Default::default()
1762 };
1763 let result = execute_input("tx", &mut ctx, &config, &factory).await;
1764 assert!(result.is_ok());
1765 }
1766
1767 #[tokio::test]
1768 async fn test_tx_auto_detects_tron() {
1769 let config = test_config();
1770 let factory = mock_factory();
1771 let mut ctx = SessionContext::default();
1772 let result = execute_input(
1773 "tx abc123def456789012345678901234567890123456789012345678901234abcd",
1774 &mut ctx,
1775 &config,
1776 &factory,
1777 )
1778 .await;
1779 assert!(result.is_ok());
1780 assert_eq!(ctx.chain, "auto");
1782 }
1783
1784 #[tokio::test]
1785 async fn test_crawl_command_with_args() {
1786 let config = test_config();
1787 let factory = mock_factory();
1788 let mut ctx = SessionContext::default();
1789 let result = execute_input(
1790 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
1791 &mut ctx,
1792 &config,
1793 &factory,
1794 )
1795 .await;
1796 assert!(result.is_ok());
1797 }
1798
1799 #[tokio::test]
1800 async fn test_crawl_command_with_period() {
1801 let config = test_config();
1802 let factory = mock_factory();
1803 let mut ctx = SessionContext::default();
1804 let result = execute_input(
1805 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d --no-charts",
1806 &mut ctx,
1807 &config,
1808 &factory,
1809 )
1810 .await;
1811 assert!(result.is_ok());
1812 }
1813
1814 #[tokio::test]
1815 async fn test_crawl_command_with_chain_flag() {
1816 let config = test_config();
1817 let factory = mock_factory();
1818 let mut ctx = SessionContext::default();
1819 let result = execute_input(
1820 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --chain polygon --no-charts",
1821 &mut ctx,
1822 &config,
1823 &factory,
1824 )
1825 .await;
1826 assert!(result.is_ok());
1827 }
1828
1829 #[tokio::test]
1830 async fn test_crawl_command_with_period_flag() {
1831 let config = test_config();
1832 let factory = mock_factory();
1833 let mut ctx = SessionContext::default();
1834 let result = execute_input(
1835 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h --no-charts",
1836 &mut ctx,
1837 &config,
1838 &factory,
1839 )
1840 .await;
1841 assert!(result.is_ok());
1842 }
1843
1844 #[tokio::test]
1845 async fn test_crawl_command_with_report() {
1846 let config = test_config();
1847 let factory = mock_factory();
1848 let mut ctx = SessionContext::default();
1849 let tmp = tempfile::NamedTempFile::new().unwrap();
1850 let result = execute_input(
1851 &format!(
1852 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report {} --no-charts",
1853 tmp.path().display()
1854 ),
1855 &mut ctx,
1856 &config,
1857 &factory,
1858 )
1859 .await;
1860 assert!(result.is_ok());
1861 }
1862
1863 #[tokio::test]
1864 async fn test_portfolio_list_command() {
1865 let tmp_dir = tempfile::tempdir().unwrap();
1866 let config = Config {
1867 address_book: crate::config::AddressBookConfig {
1868 data_dir: Some(tmp_dir.path().to_path_buf()),
1869 },
1870 ..Default::default()
1871 };
1872 let factory = mock_factory();
1873 let mut ctx = SessionContext::default();
1874 let result = execute_input("portfolio list", &mut ctx, &config, &factory).await;
1875 assert!(result.is_ok());
1876 }
1877
1878 #[tokio::test]
1879 async fn test_portfolio_add_command() {
1880 let tmp_dir = tempfile::tempdir().unwrap();
1881 let config = Config {
1882 address_book: crate::config::AddressBookConfig {
1883 data_dir: Some(tmp_dir.path().to_path_buf()),
1884 },
1885 ..Default::default()
1886 };
1887 let factory = mock_factory();
1888 let mut ctx = SessionContext::default();
1889 let result = execute_input(
1890 "portfolio add 0xtest --label mytest",
1891 &mut ctx,
1892 &config,
1893 &factory,
1894 )
1895 .await;
1896 assert!(result.is_ok());
1897 }
1898
1899 #[tokio::test]
1900 async fn test_portfolio_summary_command() {
1901 let tmp_dir = tempfile::tempdir().unwrap();
1902 let config = Config {
1903 address_book: crate::config::AddressBookConfig {
1904 data_dir: Some(tmp_dir.path().to_path_buf()),
1905 },
1906 ..Default::default()
1907 };
1908 let factory = mock_factory();
1909 let mut ctx = SessionContext::default();
1910 execute_input("portfolio add 0xtest", &mut ctx, &config, &factory)
1912 .await
1913 .unwrap();
1914 let result = execute_input("portfolio summary", &mut ctx, &config, &factory).await;
1916 assert!(result.is_ok());
1917 }
1918
1919 #[tokio::test]
1920 async fn test_portfolio_remove_command() {
1921 let tmp_dir = tempfile::tempdir().unwrap();
1922 let config = Config {
1923 address_book: crate::config::AddressBookConfig {
1924 data_dir: Some(tmp_dir.path().to_path_buf()),
1925 },
1926 ..Default::default()
1927 };
1928 let factory = mock_factory();
1929 let mut ctx = SessionContext::default();
1930 let result = execute_input("portfolio remove 0xtest", &mut ctx, &config, &factory).await;
1931 assert!(result.is_ok());
1932 }
1933
1934 #[tokio::test]
1935 async fn test_portfolio_no_subcommand() {
1936 let config = test_config();
1937 let factory = mock_factory();
1938 let mut ctx = SessionContext::default();
1939 let result = execute_input("portfolio", &mut ctx, &config, &factory).await;
1940 assert!(result.is_ok());
1941 }
1942
1943 #[tokio::test]
1944 async fn test_portfolio_unknown_subcommand() {
1945 let tmp_dir = tempfile::tempdir().unwrap();
1946 let config = Config {
1947 address_book: crate::config::AddressBookConfig {
1948 data_dir: Some(tmp_dir.path().to_path_buf()),
1949 },
1950 ..Default::default()
1951 };
1952 let factory = mock_factory();
1953 let mut ctx = SessionContext::default();
1954 let result = execute_input("portfolio foobar", &mut ctx, &config, &factory).await;
1955 assert!(result.is_ok());
1956 }
1957
1958 #[tokio::test]
1959 async fn test_tokens_command_list() {
1960 let config = test_config();
1961 let factory = mock_factory();
1962 let mut ctx = SessionContext::default();
1963 let result = execute_input("tokens list", &mut ctx, &config, &factory).await;
1964 assert!(result.is_ok());
1965 }
1966
1967 #[tokio::test]
1968 async fn test_tokens_command_no_args() {
1969 let config = test_config();
1970 let factory = mock_factory();
1971 let mut ctx = SessionContext::default();
1972 let result = execute_input("tokens", &mut ctx, &config, &factory).await;
1973 assert!(result.is_ok());
1974 }
1975
1976 #[tokio::test]
1977 async fn test_tokens_command_recent() {
1978 let config = test_config();
1979 let factory = mock_factory();
1980 let mut ctx = SessionContext::default();
1981 let result = execute_input("tokens recent", &mut ctx, &config, &factory).await;
1982 assert!(result.is_ok());
1983 }
1984
1985 #[tokio::test]
1986 async fn test_tokens_command_remove_no_args() {
1987 let config = test_config();
1988 let factory = mock_factory();
1989 let mut ctx = SessionContext::default();
1990 let result = execute_input("tokens remove", &mut ctx, &config, &factory).await;
1991 assert!(result.is_ok());
1992 }
1993
1994 #[tokio::test]
1995 async fn test_tokens_command_add_no_args() {
1996 let config = test_config();
1997 let factory = mock_factory();
1998 let mut ctx = SessionContext::default();
1999 let result = execute_input("tokens add", &mut ctx, &config, &factory).await;
2000 assert!(result.is_ok());
2001 }
2002
2003 #[tokio::test]
2004 async fn test_tokens_command_unknown() {
2005 let config = test_config();
2006 let factory = mock_factory();
2007 let mut ctx = SessionContext::default();
2008 let result = execute_input("tokens foobar", &mut ctx, &config, &factory).await;
2009 assert!(result.is_ok());
2010 }
2011
2012 #[tokio::test]
2013 async fn test_setup_command_status() {
2014 let config = test_config();
2015 let factory = mock_factory();
2016 let mut ctx = SessionContext::default();
2017 let result = execute_input("setup --status", &mut ctx, &config, &factory).await;
2018 assert!(result.is_ok());
2019 }
2020
2021 #[tokio::test]
2022 async fn test_transaction_alias() {
2023 let config = test_config();
2024 let factory = mock_factory();
2025 let mut ctx = SessionContext::default();
2026 let result = execute_input(
2027 "transaction 0xabc123def456789012345678901234567890123456789012345678901234abcd",
2028 &mut ctx,
2029 &config,
2030 &factory,
2031 )
2032 .await;
2033 assert!(result.is_ok());
2034 }
2035
2036 #[tokio::test]
2037 async fn test_token_alias_for_crawl() {
2038 let config = test_config();
2039 let factory = mock_factory();
2040 let mut ctx = SessionContext::default();
2041 let result = execute_input(
2042 "token 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
2043 &mut ctx,
2044 &config,
2045 &factory,
2046 )
2047 .await;
2048 assert!(result.is_ok());
2049 }
2050
2051 #[tokio::test]
2052 async fn test_port_alias_for_portfolio() {
2053 let tmp_dir = tempfile::tempdir().unwrap();
2054 let config = Config {
2055 address_book: crate::config::AddressBookConfig {
2056 data_dir: Some(tmp_dir.path().to_path_buf()),
2057 },
2058 ..Default::default()
2059 };
2060 let factory = mock_factory();
2061 let mut ctx = SessionContext::default();
2062 let result = execute_input("port list", &mut ctx, &config, &factory).await;
2063 assert!(result.is_ok());
2064 }
2065
2066 #[tokio::test]
2071 async fn test_execute_tokens_list_empty() {
2072 let result = execute_tokens_command(&[]).await;
2073 assert!(result.is_ok());
2074 }
2075
2076 #[tokio::test]
2077 async fn test_execute_tokens_list_subcommand() {
2078 let result = execute_tokens_command(&["list"]).await;
2079 assert!(result.is_ok());
2080 }
2081
2082 #[tokio::test]
2083 async fn test_execute_tokens_recent() {
2084 let result = execute_tokens_command(&["recent"]).await;
2085 assert!(result.is_ok());
2086 }
2087
2088 #[tokio::test]
2089 async fn test_execute_tokens_add_insufficient_args() {
2090 let result = execute_tokens_command(&["add"]).await;
2091 assert!(result.is_ok());
2092 }
2093
2094 #[tokio::test]
2095 async fn test_execute_tokens_add_success() {
2096 let result = execute_tokens_command(&[
2097 "add",
2098 "TEST_INTERACTIVE",
2099 "ethereum",
2100 "0xtest123456789",
2101 "Test Token",
2102 ])
2103 .await;
2104 assert!(result.is_ok());
2105 let _ = execute_tokens_command(&["remove", "TEST_INTERACTIVE"]).await;
2106 }
2107
2108 #[tokio::test]
2109 async fn test_execute_tokens_remove_no_args() {
2110 let result = execute_tokens_command(&["remove"]).await;
2111 assert!(result.is_ok());
2112 }
2113
2114 #[tokio::test]
2115 async fn test_execute_tokens_remove_with_symbol() {
2116 let _ =
2117 execute_tokens_command(&["add", "RMTEST", "ethereum", "0xrmtest", "Remove Test"]).await;
2118 let result = execute_tokens_command(&["remove", "RMTEST"]).await;
2119 assert!(result.is_ok());
2120 }
2121
2122 #[tokio::test]
2123 async fn test_execute_tokens_unknown_subcommand() {
2124 let result = execute_tokens_command(&["invalid"]).await;
2125 assert!(result.is_ok());
2126 }
2127
2128 #[test]
2133 fn test_session_context_serialization_roundtrip() {
2134 let ctx = SessionContext {
2135 chain: "solana".to_string(),
2136 include_tokens: true,
2137 limit: 25,
2138 last_address: Some("0xtest".to_string()),
2139 ..Default::default()
2140 };
2141
2142 let yaml = serde_yaml::to_string(&ctx).unwrap();
2143 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
2144 assert_eq!(deserialized.chain, "solana");
2145 assert!(deserialized.include_tokens);
2146 assert_eq!(deserialized.limit, 25);
2147 assert_eq!(deserialized.last_address, Some("0xtest".to_string()));
2148 }
2149
2150 #[tokio::test]
2155 async fn test_chain_show_explicit() {
2156 let config = test_config();
2157 let factory = test_factory();
2158 let mut context = SessionContext {
2159 chain: "polygon".to_string(),
2160 ..Default::default()
2161 };
2162
2163 let result = execute_input("chain", &mut context, &config, &factory).await;
2165 assert!(result.is_ok());
2166 assert!(!result.unwrap()); }
2168
2169 #[tokio::test]
2170 async fn test_address_with_explicit_chain() {
2171 let config = test_config();
2172 let factory = mock_factory();
2173 let mut context = SessionContext {
2174 chain: "polygon".to_string(),
2175 ..Default::default()
2176 };
2177
2178 let result = execute_input(
2180 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2181 &mut context,
2182 &config,
2183 &factory,
2184 )
2185 .await;
2186 assert!(result.is_ok() || result.is_err());
2188 }
2189
2190 #[tokio::test]
2191 async fn test_tx_with_explicit_chain() {
2192 let config = test_config();
2193 let factory = mock_factory();
2194 let mut context = SessionContext {
2195 chain: "polygon".to_string(),
2196 ..Default::default()
2197 };
2198
2199 let result = execute_input("tx 0xabc123def456789", &mut context, &config, &factory).await;
2201 assert!(result.is_ok() || result.is_err());
2202 }
2203
2204 #[tokio::test]
2205 async fn test_crawl_with_period_eq_flag() {
2206 let config = test_config();
2207 let factory = test_factory();
2208 let mut context = SessionContext::default();
2209
2210 let result = execute_input(
2212 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d",
2213 &mut context,
2214 &config,
2215 &factory,
2216 )
2217 .await;
2218 assert!(result.is_ok() || result.is_err());
2220 }
2221
2222 #[tokio::test]
2223 async fn test_crawl_with_period_space_flag() {
2224 let config = test_config();
2225 let factory = test_factory();
2226 let mut context = SessionContext::default();
2227
2228 let result = execute_input(
2230 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h",
2231 &mut context,
2232 &config,
2233 &factory,
2234 )
2235 .await;
2236 assert!(result.is_ok() || result.is_err());
2237 }
2238
2239 #[tokio::test]
2240 async fn test_crawl_with_chain_eq_flag() {
2241 let config = test_config();
2242 let factory = test_factory();
2243 let mut context = SessionContext::default();
2244
2245 let result = execute_input(
2247 "crawl 0xAddress --chain=polygon",
2248 &mut context,
2249 &config,
2250 &factory,
2251 )
2252 .await;
2253 assert!(result.is_ok() || result.is_err());
2254 }
2255
2256 #[tokio::test]
2257 async fn test_crawl_with_chain_space_flag() {
2258 let config = test_config();
2259 let factory = test_factory();
2260 let mut context = SessionContext::default();
2261
2262 let result = execute_input(
2264 "crawl 0xAddress --chain polygon",
2265 &mut context,
2266 &config,
2267 &factory,
2268 )
2269 .await;
2270 assert!(result.is_ok() || result.is_err());
2271 }
2272
2273 #[tokio::test]
2274 async fn test_crawl_with_report_flag() {
2275 let config = test_config();
2276 let factory = test_factory();
2277 let mut context = SessionContext::default();
2278
2279 let tmp = tempfile::NamedTempFile::new().unwrap();
2280 let path = tmp.path().to_string_lossy();
2281 let input = format!(
2282 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report={}",
2283 path
2284 );
2285 let result = execute_input(&input, &mut context, &config, &factory).await;
2286 assert!(result.is_ok() || result.is_err());
2287 }
2288
2289 #[tokio::test]
2290 async fn test_crawl_with_no_charts_flag() {
2291 let config = test_config();
2292 let factory = test_factory();
2293 let mut context = SessionContext::default();
2294
2295 let result = execute_input(
2296 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
2297 &mut context,
2298 &config,
2299 &factory,
2300 )
2301 .await;
2302 assert!(result.is_ok() || result.is_err());
2303 }
2304
2305 #[tokio::test]
2306 async fn test_crawl_with_explicit_chain() {
2307 let config = test_config();
2308 let factory = test_factory();
2309 let mut context = SessionContext {
2310 chain: "arbitrum".to_string(),
2311 ..Default::default()
2312 };
2313
2314 let result = execute_input("crawl 0xAddress", &mut context, &config, &factory).await;
2315 assert!(result.is_ok() || result.is_err());
2316 }
2317
2318 #[tokio::test]
2319 async fn test_portfolio_add_with_label_and_tags() {
2320 let tmp_dir = tempfile::tempdir().unwrap();
2321 let config = Config {
2322 address_book: crate::config::AddressBookConfig {
2323 data_dir: Some(tmp_dir.path().to_path_buf()),
2324 },
2325 ..Default::default()
2326 };
2327 let factory = mock_factory();
2328 let mut context = SessionContext::default();
2329
2330 let result = execute_input(
2331 "portfolio add 0xAbC123 --label MyWallet --tags defi,staking",
2332 &mut context,
2333 &config,
2334 &factory,
2335 )
2336 .await;
2337 assert!(result.is_ok());
2338 }
2339
2340 #[tokio::test]
2341 async fn test_portfolio_remove_no_args() {
2342 let tmp_dir = tempfile::tempdir().unwrap();
2343 let config = Config {
2344 address_book: crate::config::AddressBookConfig {
2345 data_dir: Some(tmp_dir.path().to_path_buf()),
2346 },
2347 ..Default::default()
2348 };
2349 let factory = mock_factory();
2350 let mut context = SessionContext::default();
2351
2352 let result = execute_input("portfolio remove", &mut context, &config, &factory).await;
2353 assert!(result.is_ok());
2354 }
2355
2356 #[tokio::test]
2357 async fn test_portfolio_summary_with_chain_and_tag() {
2358 let tmp_dir = tempfile::tempdir().unwrap();
2359 let config = Config {
2360 address_book: crate::config::AddressBookConfig {
2361 data_dir: Some(tmp_dir.path().to_path_buf()),
2362 },
2363 ..Default::default()
2364 };
2365 let factory = mock_factory();
2366 let mut context = SessionContext::default();
2367
2368 let result = execute_input(
2369 "portfolio summary --chain ethereum --tag defi --tokens",
2370 &mut context,
2371 &config,
2372 &factory,
2373 )
2374 .await;
2375 assert!(result.is_ok());
2376 }
2377
2378 #[tokio::test]
2379 async fn test_tokens_add_with_name() {
2380 let result = execute_tokens_command(&[
2381 "add",
2382 "USDC",
2383 "ethereum",
2384 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2385 "USD",
2386 "Coin",
2387 ])
2388 .await;
2389 assert!(result.is_ok());
2390 }
2391
2392 #[tokio::test]
2393 async fn test_tokens_remove_with_chain() {
2394 let result = execute_tokens_command(&["remove", "USDC", "--chain", "ethereum"]).await;
2395 assert!(result.is_ok());
2396 }
2397
2398 #[tokio::test]
2399 async fn test_tokens_add_then_list_nonempty() {
2400 let _ = execute_tokens_command(&[
2402 "add",
2403 "TEST_TOKEN_XYZ",
2404 "ethereum",
2405 "0x1234567890abcdef1234567890abcdef12345678",
2406 "Test",
2407 "Token",
2408 ])
2409 .await;
2410
2411 let result = execute_tokens_command(&["list"]).await;
2413 assert!(result.is_ok());
2414
2415 let result = execute_tokens_command(&["recent"]).await;
2417 assert!(result.is_ok());
2418
2419 let _ = execute_tokens_command(&["remove", "TEST_TOKEN_XYZ"]).await;
2421 }
2422
2423 #[tokio::test]
2424 async fn test_session_context_save_and_load() {
2425 let ctx = SessionContext {
2428 chain: "solana".to_string(),
2429 last_address: Some("0xabc".to_string()),
2430 last_tx: Some("0xdef".to_string()),
2431 ..Default::default()
2432 };
2433 let _ = ctx.save();
2435 let loaded = SessionContext::load();
2437 assert!(!loaded.chain.is_empty());
2439 }
2440
2441 #[tokio::test]
2446 async fn test_help_alias_question_mark() {
2447 let config = test_config();
2448 let mut ctx = SessionContext::default();
2449 let result = execute_input("?", &mut ctx, &config, &test_factory())
2450 .await
2451 .unwrap();
2452 assert!(!result);
2453 }
2454
2455 #[tokio::test]
2456 async fn test_context_alias() {
2457 let config = test_config();
2458 let mut ctx = SessionContext::default();
2459 let result = execute_input("context", &mut ctx, &config, &test_factory())
2460 .await
2461 .unwrap();
2462 assert!(!result);
2463 }
2464
2465 #[tokio::test]
2466 async fn test_dot_context_alias() {
2467 let config = test_config();
2468 let mut ctx = SessionContext::default();
2469 let result = execute_input(".context", &mut ctx, &config, &test_factory())
2470 .await
2471 .unwrap();
2472 assert!(!result);
2473 }
2474
2475 #[tokio::test]
2476 async fn test_reset_alias() {
2477 let config = test_config();
2478 let mut ctx = SessionContext {
2479 chain: "ethereum".to_string(),
2480 ..Default::default()
2481 };
2482 execute_input("reset", &mut ctx, &config, &test_factory())
2483 .await
2484 .unwrap();
2485 assert_eq!(ctx.chain, "auto");
2486 }
2487
2488 #[tokio::test]
2489 async fn test_dot_reset_alias() {
2490 let config = test_config();
2491 let mut ctx = SessionContext {
2492 chain: "base".to_string(),
2493 ..Default::default()
2494 };
2495 execute_input(".reset", &mut ctx, &config, &test_factory())
2496 .await
2497 .unwrap();
2498 assert_eq!(ctx.chain, "auto");
2499 }
2500
2501 #[tokio::test]
2502 async fn test_dot_clear_alias() {
2503 let config = test_config();
2504 let mut ctx = SessionContext {
2505 chain: "bsc".to_string(),
2506 ..Default::default()
2507 };
2508 execute_input(".clear", &mut ctx, &config, &test_factory())
2509 .await
2510 .unwrap();
2511 assert_eq!(ctx.chain, "auto");
2512 }
2513
2514 #[tokio::test]
2515 async fn test_showtokens_alias() {
2516 let config = test_config();
2517 let mut ctx = SessionContext::default();
2518 execute_input("showtokens", &mut ctx, &config, &test_factory())
2519 .await
2520 .unwrap();
2521 assert!(ctx.include_tokens);
2522 }
2523
2524 #[tokio::test]
2525 async fn test_showtxs_alias() {
2526 let config = test_config();
2527 let mut ctx = SessionContext::default();
2528 execute_input("showtxs", &mut ctx, &config, &test_factory())
2529 .await
2530 .unwrap();
2531 assert!(ctx.include_txs);
2532 }
2533
2534 #[tokio::test]
2535 async fn test_txs_alias() {
2536 let config = test_config();
2537 let mut ctx = SessionContext::default();
2538 execute_input("txs", &mut ctx, &config, &test_factory())
2539 .await
2540 .unwrap();
2541 assert!(ctx.include_txs);
2542 }
2543
2544 #[tokio::test]
2545 async fn test_dot_txs_alias() {
2546 let config = test_config();
2547 let mut ctx = SessionContext::default();
2548 execute_input(".txs", &mut ctx, &config, &test_factory())
2549 .await
2550 .unwrap();
2551 assert!(ctx.include_txs);
2552 }
2553
2554 #[tokio::test]
2555 async fn test_addr_alias() {
2556 let config = test_config();
2557 let factory = mock_factory();
2558 let mut ctx = SessionContext::default();
2559 let result = execute_input(
2560 "addr 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2561 &mut ctx,
2562 &config,
2563 &factory,
2564 )
2565 .await;
2566 assert!(result.is_ok());
2567 assert_eq!(
2568 ctx.last_address,
2569 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
2570 );
2571 }
2572
2573 #[test]
2574 fn test_session_context_is_auto_chain() {
2575 let auto_ctx = SessionContext::default();
2576 assert!(auto_ctx.is_auto_chain());
2577 let pinned_ctx = SessionContext {
2578 chain: "ethereum".to_string(),
2579 ..Default::default()
2580 };
2581 assert!(!pinned_ctx.is_auto_chain());
2582 }
2583
2584 #[test]
2585 fn test_print_help_no_panic() {
2586 print_help();
2587 }
2588
2589 #[tokio::test]
2594 async fn test_contract_no_args() {
2595 let config = test_config();
2596 let factory = mock_factory();
2597 let mut ctx = SessionContext::default();
2598 let result = execute_input("contract", &mut ctx, &config, &factory).await;
2599 assert!(result.is_ok());
2600 assert!(!result.unwrap());
2601 }
2602
2603 #[tokio::test]
2604 async fn test_contract_ct_alias_with_args() {
2605 let config = test_config();
2606 let factory = mock_factory();
2607 let mut ctx = SessionContext::default();
2608 let result = execute_input(
2609 "ct 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2610 &mut ctx,
2611 &config,
2612 &factory,
2613 )
2614 .await;
2615 if let Ok(should_exit) = result {
2616 assert!(!should_exit);
2617 }
2618 }
2619
2620 #[tokio::test]
2621 async fn test_contract_with_chain_and_json() {
2622 let config = test_config();
2623 let factory = mock_factory();
2624 let mut ctx = SessionContext::default();
2625 let result = execute_input(
2626 "contract 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon --json",
2627 &mut ctx,
2628 &config,
2629 &factory,
2630 )
2631 .await;
2632 if let Ok(should_exit) = result {
2633 assert!(!should_exit);
2634 }
2635 }
2636
2637 #[tokio::test]
2642 async fn test_address_book_list_command() {
2643 let tmp_dir = tempfile::tempdir().unwrap();
2644 let config = Config {
2645 address_book: crate::config::AddressBookConfig {
2646 data_dir: Some(tmp_dir.path().to_path_buf()),
2647 },
2648 ..Default::default()
2649 };
2650 let factory = mock_factory();
2651 let mut ctx = SessionContext::default();
2652 let result = execute_input("address-book list", &mut ctx, &config, &factory).await;
2653 assert!(result.is_ok());
2654 }
2655
2656 #[tokio::test]
2657 async fn test_address_book_underscore_list() {
2658 let tmp_dir = tempfile::tempdir().unwrap();
2659 let config = Config {
2660 address_book: crate::config::AddressBookConfig {
2661 data_dir: Some(tmp_dir.path().to_path_buf()),
2662 },
2663 ..Default::default()
2664 };
2665 let factory = mock_factory();
2666 let mut ctx = SessionContext::default();
2667 let result = execute_input("address_book list", &mut ctx, &config, &factory).await;
2668 assert!(result.is_ok());
2669 }
2670
2671 #[tokio::test]
2672 async fn test_address_book_add_insufficient_args() {
2673 let tmp_dir = tempfile::tempdir().unwrap();
2674 let config = Config {
2675 address_book: crate::config::AddressBookConfig {
2676 data_dir: Some(tmp_dir.path().to_path_buf()),
2677 },
2678 ..Default::default()
2679 };
2680 let factory = mock_factory();
2681 let mut ctx = SessionContext::default();
2682 let result = execute_input("address-book add", &mut ctx, &config, &factory).await;
2683 assert!(result.is_ok());
2684 }
2685
2686 #[tokio::test]
2687 async fn test_address_book_remove_insufficient_args() {
2688 let tmp_dir = tempfile::tempdir().unwrap();
2689 let config = Config {
2690 address_book: crate::config::AddressBookConfig {
2691 data_dir: Some(tmp_dir.path().to_path_buf()),
2692 },
2693 ..Default::default()
2694 };
2695 let factory = mock_factory();
2696 let mut ctx = SessionContext::default();
2697 let result = execute_input("address-book remove", &mut ctx, &config, &factory).await;
2698 assert!(result.is_ok());
2699 }
2700
2701 #[tokio::test]
2702 async fn test_address_book_empty_subcommand() {
2703 let tmp_dir = tempfile::tempdir().unwrap();
2704 let config = Config {
2705 address_book: crate::config::AddressBookConfig {
2706 data_dir: Some(tmp_dir.path().to_path_buf()),
2707 },
2708 ..Default::default()
2709 };
2710 let factory = mock_factory();
2711 let mut ctx = SessionContext::default();
2712 let result = execute_input("address-book", &mut ctx, &config, &factory).await;
2713 assert!(result.is_ok());
2714 }
2715
2716 #[tokio::test]
2721 async fn test_aliases_command() {
2722 let config = test_config();
2723 let factory = mock_factory();
2724 let mut ctx = SessionContext::default();
2725 let result = execute_input("aliases", &mut ctx, &config, &factory).await;
2726 assert!(result.is_ok());
2727 }
2728
2729 #[tokio::test]
2730 async fn test_config_alias() {
2731 let config = test_config();
2732 let factory = mock_factory();
2733 let mut ctx = SessionContext::default();
2734 let result = execute_input("config --status", &mut ctx, &config, &factory).await;
2735 assert!(result.is_ok());
2736 }
2737
2738 #[tokio::test]
2739 #[ignore = "setup --key prompts for API key input on stdin"]
2740 async fn test_setup_with_key_flag() {
2741 let config = test_config();
2742 let factory = mock_factory();
2743 let mut ctx = SessionContext::default();
2744 let result = execute_input("setup --key=etherscan", &mut ctx, &config, &factory).await;
2745 assert!(result.is_ok());
2746 }
2747
2748 #[tokio::test]
2749 async fn test_setup_with_key_short_flag() {
2750 let config = test_config();
2751 let factory = mock_factory();
2752 let mut ctx = SessionContext::default();
2753 let result = execute_input("setup -s", &mut ctx, &config, &factory).await;
2754 assert!(result.is_ok());
2755 }
2756
2757 #[tokio::test]
2761 #[ignore = "monitor starts TUI and blocks until exit"]
2762 async fn test_monitor_command_no_token() {
2763 let config = test_config();
2764 let factory = mock_factory();
2765 let mut ctx = SessionContext::default();
2766 let result = execute_input("monitor", &mut ctx, &config, &factory).await;
2767 assert!(result.is_ok() || result.is_err());
2768 }
2769
2770 #[tokio::test]
2771 #[ignore = "monitor starts TUI and blocks until exit"]
2772 async fn test_mon_alias() {
2773 let config = test_config();
2774 let factory = mock_factory();
2775 let mut ctx = SessionContext::default();
2776 let result = execute_input(
2777 "mon 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2778 &mut ctx,
2779 &config,
2780 &factory,
2781 )
2782 .await;
2783 assert!(result.is_ok() || result.is_err());
2784 }
2785
2786 #[tokio::test]
2791 async fn test_tokens_ls_alias() {
2792 let config = test_config();
2793 let factory = mock_factory();
2794 let mut ctx = SessionContext::default();
2795 let result = execute_input("tokens ls", &mut ctx, &config, &factory).await;
2796 assert!(result.is_ok());
2797 }
2798
2799 #[tokio::test]
2800 async fn test_execute_tokens_ls_alias() {
2801 let result = execute_tokens_command(&["ls"]).await;
2802 assert!(result.is_ok());
2803 }
2804
2805 #[tokio::test]
2806 async fn test_crawl_period_1h() {
2807 let config = test_config();
2808 let factory = mock_factory();
2809 let mut ctx = SessionContext::default();
2810 let result = execute_input(
2811 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=1h --no-charts",
2812 &mut ctx,
2813 &config,
2814 &factory,
2815 )
2816 .await;
2817 assert!(result.is_ok());
2818 }
2819
2820 #[tokio::test]
2821 async fn test_crawl_period_30d() {
2822 let config = test_config();
2823 let factory = mock_factory();
2824 let mut ctx = SessionContext::default();
2825 let result = execute_input(
2826 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=30d --no-charts",
2827 &mut ctx,
2828 &config,
2829 &factory,
2830 )
2831 .await;
2832 assert!(result.is_ok());
2833 }
2834
2835 #[tokio::test]
2836 async fn test_crawl_invalid_period_defaults() {
2837 let config = test_config();
2838 let factory = mock_factory();
2839 let mut ctx = SessionContext::default();
2840 let result = execute_input(
2841 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=invalid --no-charts",
2842 &mut ctx,
2843 &config,
2844 &factory,
2845 )
2846 .await;
2847 assert!(result.is_ok());
2848 }
2849
2850 #[tokio::test]
2851 async fn test_tokens_add_three_args_insufficient() {
2852 let result = execute_tokens_command(&["add", "SYM", "ethereum"]).await;
2853 assert!(result.is_ok());
2854 }
2855
2856 #[tokio::test]
2857 async fn test_format_show_when_csv() {
2858 let config = test_config();
2859 let mut ctx = SessionContext {
2860 format: OutputFormat::Csv,
2861 ..Default::default()
2862 };
2863 let result = execute_input("format", &mut ctx, &config, &test_factory())
2864 .await
2865 .unwrap();
2866 assert!(!result);
2867 }
2868}