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::tokens::TokenAliases;
678
679 let mut aliases = TokenAliases::load();
680
681 if args.is_empty() {
682 let tokens = aliases.list();
684 if tokens.is_empty() {
685 println!("No saved token aliases.");
686 println!("Use 'crawl <token_name> --save' to save a token alias.");
687 return Ok(());
688 }
689
690 println!("\nSaved Token Aliases\n{}\n", "=".repeat(60));
691 println!("{:<10} {:<12} {:<20} Address", "Symbol", "Chain", "Name");
692 println!("{}", "-".repeat(80));
693
694 for token in tokens {
695 println!(
696 "{:<10} {:<12} {:<20} {}",
697 token.symbol, token.chain, token.name, token.address
698 );
699 }
700 println!();
701 return Ok(());
702 }
703
704 let subcommand = args[0].to_lowercase();
705 match subcommand.as_str() {
706 "list" | "ls" => {
707 let tokens = aliases.list();
708 if tokens.is_empty() {
709 println!("No saved token aliases.");
710 return Ok(());
711 }
712
713 println!("\nSaved Token Aliases\n{}\n", "=".repeat(60));
714 println!("{:<10} {:<12} {:<20} Address", "Symbol", "Chain", "Name");
715 println!("{}", "-".repeat(80));
716
717 for token in tokens {
718 println!(
719 "{:<10} {:<12} {:<20} {}",
720 token.symbol, token.chain, token.name, token.address
721 );
722 }
723 println!();
724 }
725
726 "recent" => {
727 let recent = aliases.recent();
728 if recent.is_empty() {
729 println!("No recently used tokens.");
730 return Ok(());
731 }
732
733 println!("\nRecently Used Tokens\n{}\n", "=".repeat(60));
734 println!("{:<10} {:<12} {:<20} Address", "Symbol", "Chain", "Name");
735 println!("{}", "-".repeat(80));
736
737 for token in recent {
738 println!(
739 "{:<10} {:<12} {:<20} {}",
740 token.symbol, token.chain, token.name, token.address
741 );
742 }
743 println!();
744 }
745
746 "remove" | "rm" | "delete" => {
747 if args.len() < 2 {
748 eprintln!("Usage: tokens remove <symbol> [--chain <chain>]");
749 return Ok(());
750 }
751
752 let symbol = args[1];
753 let chain = if args.len() > 3 && args[2] == "--chain" {
754 Some(args[3])
755 } else {
756 None
757 };
758
759 aliases.remove(symbol, chain);
760 if let Err(e) = aliases.save() {
761 eprintln!("Failed to save: {}", e);
762 } else {
763 println!("Removed alias: {}", symbol);
764 }
765 }
766
767 "add" => {
768 if args.len() < 4 {
769 eprintln!("Usage: tokens add <symbol> <chain> <address> [name]");
770 return Ok(());
771 }
772
773 let symbol = args[1];
774 let chain = args[2];
775 let address = args[3];
776 let name = if args.len() > 4 {
777 args[4..].join(" ")
778 } else {
779 symbol.to_string()
780 };
781
782 aliases.add(symbol, chain, address, &name);
783 if let Err(e) = aliases.save() {
784 eprintln!("Failed to save: {}", e);
785 } else {
786 println!("Added alias: {} -> {} on {}", symbol, address, chain);
787 }
788 }
789
790 _ => {
791 eprintln!("Unknown tokens subcommand: {}", subcommand);
792 eprintln!("Available: list, recent, add, remove");
793 }
794 }
795
796 Ok(())
797}
798
799async fn execute_address_book(
801 input: &str,
802 context: &SessionContext,
803 config: &Config,
804 clients: &dyn ChainClientFactory,
805) -> Result<()> {
806 let parts: Vec<&str> = input.split_whitespace().collect();
807 if parts.is_empty() {
808 eprintln!("Address book subcommand required: add, remove, list, summary");
809 return Ok(());
810 }
811
812 use super::address_book::{AddArgs, AddressBookCommands, RemoveArgs, SummaryArgs};
813
814 let subcommand = parts[0].to_lowercase();
815
816 let address_book_args = match subcommand.as_str() {
817 "add" => {
818 if parts.len() < 2 {
819 eprintln!("Usage: address-book add <address> [--label <label>] [--tags <tags>]");
820 return Ok(());
821 }
822 let address = parts[1].to_string();
823 let mut label = None;
824 let mut tags = Vec::new();
825
826 let mut i = 2;
827 while i < parts.len() {
828 if parts[i] == "--label" && i + 1 < parts.len() {
829 label = Some(parts[i + 1].to_string());
830 i += 2;
831 } else if parts[i] == "--tags" && i + 1 < parts.len() {
832 tags = parts[i + 1]
833 .split(',')
834 .map(|s| s.trim().to_string())
835 .collect();
836 i += 2;
837 } else {
838 i += 1;
839 }
840 }
841
842 AddressBookArgs {
843 command: AddressBookCommands::Add(AddArgs {
844 chain: if context.is_auto_chain() {
845 crate::chains::infer_chain_from_address(&address)
846 .unwrap_or("ethereum")
847 .to_string()
848 } else {
849 context.chain.clone()
850 },
851 address,
852 label,
853 tags,
854 }),
855 format: Some(context.format),
856 }
857 }
858 "remove" | "rm" => {
859 if parts.len() < 2 {
860 eprintln!("Usage: address-book remove <address>");
861 return Ok(());
862 }
863 AddressBookArgs {
864 command: AddressBookCommands::Remove(RemoveArgs {
865 address: parts[1].to_string(),
866 }),
867 format: Some(context.format),
868 }
869 }
870 "list" | "ls" => AddressBookArgs {
871 command: AddressBookCommands::List,
872 format: Some(context.format),
873 },
874 "summary" => {
875 let mut chain = None;
876 let mut tag = None;
877 let mut include_tokens = context.include_tokens;
878
879 let mut i = 1;
880 while i < parts.len() {
881 if parts[i] == "--chain" && i + 1 < parts.len() {
882 chain = Some(parts[i + 1].to_string());
883 i += 2;
884 } else if parts[i] == "--tag" && i + 1 < parts.len() {
885 tag = Some(parts[i + 1].to_string());
886 i += 2;
887 } else if parts[i] == "--tokens" {
888 include_tokens = true;
889 i += 1;
890 } else {
891 i += 1;
892 }
893 }
894
895 AddressBookArgs {
896 command: AddressBookCommands::Summary(SummaryArgs {
897 chain,
898 tag,
899 include_tokens,
900 report: None,
901 }),
902 format: Some(context.format),
903 }
904 }
905 _ => {
906 eprintln!(
907 "Unknown address book subcommand: {}. Use: add, remove, list, summary",
908 subcommand
909 );
910 return Ok(());
911 }
912 };
913
914 address_book::run(address_book_args, config, clients).await
915}
916
917fn print_help() {
919 println!(
920 r#"
921Scope Interactive Mode - Available Commands
922==========================================
923
924Navigation & Control:
925 help, ? Show this help message
926 exit, quit, q Exit interactive mode
927 ctx, context Show current session context
928 clear, reset Reset context to defaults
929
930Context Settings:
931 chain [name] Set or show current chain (default: auto)
932 auto = infer chain from each input
933 Valid: auto, ethereum, polygon, arbitrum, optimism, base, bsc, solana, tron
934 format [fmt] Set or show output format (table, json, csv)
935 limit [n] Set or show transaction limit
936 +tokens Toggle include_tokens flag for address analysis
937 +txs Toggle include_txs flag
938 trace Toggle trace flag
939 decode Toggle decode flag
940
941Analysis Commands:
942 address <addr> Analyze an address (uses current chain/format)
943 addr Shorthand for address
944 tx <hash> Analyze a transaction (uses current chain/format)
945 contract <addr> Analyze a smart contract (security, proxy, access control)
946 ct Shorthand for contract
947 crawl <token> Crawl token analytics (holders, volume, price)
948 token Shorthand for crawl
949 monitor <token> Live-updating charts for a token (TUI mode)
950 mon Shorthand for monitor
951
952Token Search:
953 crawl USDC Search for token by name/symbol (interactive selection)
954 crawl 0x... Use address directly (no search)
955 tokens List saved token aliases
956 tokens recent Show recently used tokens
957 tokens add <sym> <chain> <addr> [name] Add a token alias
958 tokens remove <sym> [--chain <chain>] Remove a token alias
959
960Address Book Commands:
961 address-book add <addr> [--label <name>] [--tags <t1,t2>]
962 address-book remove <addr>
963 address-book list
964 address-book summary [--chain <name>] [--tag <tag>] [--tokens]
965
966Configuration:
967 setup Run the setup wizard to configure API keys
968 setup --status Show current configuration status
969 setup --key <provider> Configure a specific API key
970 config Alias for setup
971
972Inline Overrides:
973 address 0x... --chain=polygon --tokens
974 tx 0x... --chain=arbitrum --trace --decode
975 contract 0x... --chain=polygon --json
976 crawl USDC --chain=ethereum --period=7d --report=report.md
977
978Live Monitor:
979 monitor USDC Start live monitoring with real-time charts
980 mon 0x... Monitor by address
981 Time periods: [1]=15m [2]=1h [3]=6h [4]=24h [T]=cycle
982 Chart modes: [C]=toggle between Line and Candlestick
983 Controls: [Q]uit [R]efresh [P]ause [+/-]speed [Esc]exit
984 Data is cached to temp folder and persists between sessions (24h retention)
985
986Tips:
987 - Search by token name: 'crawl WETH' or 'crawl "wrapped ether"'
988 - Save aliases for quick access: select a token and choose to save
989 - Context persists: set chain once, use it for multiple commands
990 - Use Ctrl+C to cancel, Ctrl+D to exit
991"#
992 );
993}
994
995#[cfg(test)]
1000mod tests {
1001 use super::*;
1002
1003 #[test]
1004 fn test_session_context_default() {
1005 let ctx = SessionContext::default();
1006 assert_eq!(ctx.chain, "auto");
1007 assert_eq!(ctx.format, OutputFormat::Table);
1008 assert!(!ctx.include_tokens);
1009 assert!(!ctx.include_txs);
1010 assert!(!ctx.trace);
1011 assert!(!ctx.decode);
1012 assert_eq!(ctx.limit, 100);
1013 assert!(ctx.last_address.is_none());
1014 assert!(ctx.last_tx.is_none());
1015 }
1016
1017 #[test]
1018 fn test_session_context_display() {
1019 let ctx = SessionContext::default();
1020 let display = format!("{}", ctx);
1021 assert!(display.contains("auto"));
1022 assert!(display.contains("Table"));
1023 }
1024
1025 #[test]
1026 fn test_interactive_args_default() {
1027 let args = InteractiveArgs { no_banner: false };
1028 assert!(!args.no_banner);
1029 }
1030
1031 #[test]
1036 fn test_session_context_serialization() {
1037 let ctx = SessionContext {
1038 chain: "polygon".to_string(),
1039 format: OutputFormat::Json,
1040 last_address: Some("0xabc".to_string()),
1041 last_tx: Some("0xdef".to_string()),
1042 include_tokens: true,
1043 include_txs: true,
1044 trace: true,
1045 decode: true,
1046 limit: 50,
1047 };
1048
1049 let yaml = serde_yaml::to_string(&ctx).unwrap();
1050 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
1051 assert_eq!(deserialized.chain, "polygon");
1052 assert!(!deserialized.is_auto_chain());
1053 assert_eq!(deserialized.format, OutputFormat::Json);
1054 assert_eq!(deserialized.last_address.as_deref(), Some("0xabc"));
1055 assert_eq!(deserialized.last_tx.as_deref(), Some("0xdef"));
1056 assert!(deserialized.include_tokens);
1057 assert!(deserialized.include_txs);
1058 assert!(deserialized.trace);
1059 assert!(deserialized.decode);
1060 assert_eq!(deserialized.limit, 50);
1061 }
1062
1063 #[test]
1064 fn test_session_context_display_with_address_and_tx() {
1065 let ctx = SessionContext {
1066 chain: "polygon".to_string(),
1067 last_address: Some("0x1234".to_string()),
1068 last_tx: Some("0xabcd".to_string()),
1069 ..Default::default()
1070 };
1071 let display = format!("{}", ctx);
1072 assert!(display.contains("0x1234"));
1073 assert!(display.contains("0xabcd"));
1074 assert!(display.contains("(pinned)"));
1075 }
1076
1077 #[test]
1078 fn test_session_context_display_auto_chain() {
1079 let ctx = SessionContext::default();
1080 let display = format!("{}", ctx);
1081 assert!(display.contains("auto"));
1082 assert!(display.contains("inferred from input"));
1083 }
1084
1085 fn test_config() -> Config {
1090 Config::default()
1091 }
1092
1093 fn test_factory() -> crate::chains::DefaultClientFactory {
1094 crate::chains::DefaultClientFactory {
1095 chains_config: crate::config::ChainsConfig::default(),
1096 }
1097 }
1098
1099 #[tokio::test]
1100 async fn test_exit_commands() {
1101 let config = test_config();
1102 for cmd in &["exit", "quit", "q", ".exit", ".quit"] {
1103 let mut ctx = SessionContext::default();
1104 let result = execute_input(cmd, &mut ctx, &config, &test_factory())
1105 .await
1106 .unwrap();
1107 assert!(result, "'{cmd}' should return true (exit)");
1108 }
1109 }
1110
1111 #[tokio::test]
1112 async fn test_help_command() {
1113 let config = test_config();
1114 let mut ctx = SessionContext::default();
1115 let result = execute_input("help", &mut ctx, &config, &test_factory())
1116 .await
1117 .unwrap();
1118 assert!(!result);
1119 }
1120
1121 #[tokio::test]
1122 async fn test_context_command() {
1123 let config = test_config();
1124 let mut ctx = SessionContext::default();
1125 let result = execute_input("ctx", &mut ctx, &config, &test_factory())
1126 .await
1127 .unwrap();
1128 assert!(!result);
1129 }
1130
1131 #[tokio::test]
1132 async fn test_clear_command() {
1133 let config = test_config();
1134 let mut ctx = SessionContext {
1135 chain: "polygon".to_string(),
1136 include_tokens: true,
1137 limit: 42,
1138 ..Default::default()
1139 };
1140
1141 let result = execute_input("clear", &mut ctx, &config, &test_factory())
1142 .await
1143 .unwrap();
1144 assert!(!result);
1145 assert_eq!(ctx.chain, "auto");
1146 assert!(!ctx.include_tokens);
1147 assert_eq!(ctx.limit, 100);
1148 }
1149
1150 #[tokio::test]
1151 async fn test_chain_set_valid() {
1152 let config = test_config();
1153 let mut ctx = SessionContext::default();
1154
1155 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1156 .await
1157 .unwrap();
1158 assert_eq!(ctx.chain, "polygon");
1159 assert!(!ctx.is_auto_chain());
1160 }
1161
1162 #[tokio::test]
1163 async fn test_chain_set_solana() {
1164 let config = test_config();
1165 let mut ctx = SessionContext::default();
1166
1167 execute_input("chain solana", &mut ctx, &config, &test_factory())
1168 .await
1169 .unwrap();
1170 assert_eq!(ctx.chain, "solana");
1171 assert!(!ctx.is_auto_chain());
1172 }
1173
1174 #[tokio::test]
1175 async fn test_chain_auto() {
1176 let config = test_config();
1177 let mut ctx = SessionContext {
1178 chain: "polygon".to_string(),
1179 ..Default::default()
1180 };
1181
1182 execute_input("chain auto", &mut ctx, &config, &test_factory())
1183 .await
1184 .unwrap();
1185 assert_eq!(ctx.chain, "auto");
1186 assert!(ctx.is_auto_chain());
1187 }
1188
1189 #[tokio::test]
1190 async fn test_chain_invalid() {
1191 let config = test_config();
1192 let mut ctx = SessionContext::default();
1193 execute_input("chain foobar", &mut ctx, &config, &test_factory())
1195 .await
1196 .unwrap();
1197 assert_eq!(ctx.chain, "auto");
1198 assert!(ctx.is_auto_chain());
1199 }
1200
1201 #[tokio::test]
1202 async fn test_chain_show() {
1203 let config = test_config();
1204 let mut ctx = SessionContext::default();
1205 let result = execute_input("chain", &mut ctx, &config, &test_factory())
1207 .await
1208 .unwrap();
1209 assert!(!result);
1210 assert_eq!(ctx.chain, "auto");
1211 }
1212
1213 #[tokio::test]
1214 async fn test_format_set_json() {
1215 let config = test_config();
1216 let mut ctx = SessionContext::default();
1217 execute_input("format json", &mut ctx, &config, &test_factory())
1218 .await
1219 .unwrap();
1220 assert_eq!(ctx.format, OutputFormat::Json);
1221 }
1222
1223 #[tokio::test]
1224 async fn test_format_set_csv() {
1225 let config = test_config();
1226 let mut ctx = SessionContext::default();
1227 execute_input("format csv", &mut ctx, &config, &test_factory())
1228 .await
1229 .unwrap();
1230 assert_eq!(ctx.format, OutputFormat::Csv);
1231 }
1232
1233 #[tokio::test]
1234 async fn test_format_set_table() {
1235 let config = test_config();
1236 let mut ctx = SessionContext {
1237 format: OutputFormat::Json,
1238 ..Default::default()
1239 };
1240 execute_input("format table", &mut ctx, &config, &test_factory())
1241 .await
1242 .unwrap();
1243 assert_eq!(ctx.format, OutputFormat::Table);
1244 }
1245
1246 #[tokio::test]
1247 async fn test_format_invalid() {
1248 let config = test_config();
1249 let mut ctx = SessionContext::default();
1250 execute_input("format xml", &mut ctx, &config, &test_factory())
1251 .await
1252 .unwrap();
1253 assert_eq!(ctx.format, OutputFormat::Table);
1255 }
1256
1257 #[tokio::test]
1258 async fn test_format_show() {
1259 let config = test_config();
1260 let mut ctx = SessionContext::default();
1261 let result = execute_input("format", &mut ctx, &config, &test_factory())
1262 .await
1263 .unwrap();
1264 assert!(!result);
1265 }
1266
1267 #[tokio::test]
1268 async fn test_toggle_tokens() {
1269 let config = test_config();
1270 let mut ctx = SessionContext::default();
1271 assert!(!ctx.include_tokens);
1272
1273 execute_input("+tokens", &mut ctx, &config, &test_factory())
1274 .await
1275 .unwrap();
1276 assert!(ctx.include_tokens);
1277
1278 execute_input("+tokens", &mut ctx, &config, &test_factory())
1279 .await
1280 .unwrap();
1281 assert!(!ctx.include_tokens);
1282 }
1283
1284 #[tokio::test]
1285 async fn test_toggle_txs() {
1286 let config = test_config();
1287 let mut ctx = SessionContext::default();
1288 assert!(!ctx.include_txs);
1289
1290 execute_input("+txs", &mut ctx, &config, &test_factory())
1291 .await
1292 .unwrap();
1293 assert!(ctx.include_txs);
1294
1295 execute_input("+txs", &mut ctx, &config, &test_factory())
1296 .await
1297 .unwrap();
1298 assert!(!ctx.include_txs);
1299 }
1300
1301 #[tokio::test]
1302 async fn test_toggle_trace() {
1303 let config = test_config();
1304 let mut ctx = SessionContext::default();
1305 assert!(!ctx.trace);
1306
1307 execute_input("trace", &mut ctx, &config, &test_factory())
1308 .await
1309 .unwrap();
1310 assert!(ctx.trace);
1311
1312 execute_input("trace", &mut ctx, &config, &test_factory())
1313 .await
1314 .unwrap();
1315 assert!(!ctx.trace);
1316 }
1317
1318 #[tokio::test]
1319 async fn test_toggle_decode() {
1320 let config = test_config();
1321 let mut ctx = SessionContext::default();
1322 assert!(!ctx.decode);
1323
1324 execute_input("decode", &mut ctx, &config, &test_factory())
1325 .await
1326 .unwrap();
1327 assert!(ctx.decode);
1328
1329 execute_input("decode", &mut ctx, &config, &test_factory())
1330 .await
1331 .unwrap();
1332 assert!(!ctx.decode);
1333 }
1334
1335 #[tokio::test]
1336 async fn test_limit_set_valid() {
1337 let config = test_config();
1338 let mut ctx = SessionContext::default();
1339 execute_input("limit 50", &mut ctx, &config, &test_factory())
1340 .await
1341 .unwrap();
1342 assert_eq!(ctx.limit, 50);
1343 }
1344
1345 #[tokio::test]
1346 async fn test_limit_set_invalid() {
1347 let config = test_config();
1348 let mut ctx = SessionContext::default();
1349 execute_input("limit abc", &mut ctx, &config, &test_factory())
1350 .await
1351 .unwrap();
1352 assert_eq!(ctx.limit, 100);
1354 }
1355
1356 #[tokio::test]
1357 async fn test_limit_show() {
1358 let config = test_config();
1359 let mut ctx = SessionContext::default();
1360 let result = execute_input("limit", &mut ctx, &config, &test_factory())
1361 .await
1362 .unwrap();
1363 assert!(!result);
1364 }
1365
1366 #[tokio::test]
1367 async fn test_unknown_command() {
1368 let config = test_config();
1369 let mut ctx = SessionContext::default();
1370 let result = execute_input("foobar", &mut ctx, &config, &test_factory())
1371 .await
1372 .unwrap();
1373 assert!(!result);
1374 }
1375
1376 #[tokio::test]
1377 async fn test_empty_input() {
1378 let config = test_config();
1379 let mut ctx = SessionContext::default();
1380 let result = execute_input("", &mut ctx, &config, &test_factory())
1381 .await
1382 .unwrap();
1383 assert!(!result);
1384 }
1385
1386 #[tokio::test]
1387 async fn test_address_no_arg_no_last() {
1388 let config = test_config();
1389 let mut ctx = SessionContext::default();
1390 let result = execute_input("address", &mut ctx, &config, &test_factory())
1392 .await
1393 .unwrap();
1394 assert!(!result);
1395 }
1396
1397 #[tokio::test]
1398 async fn test_tx_no_arg_no_last() {
1399 let config = test_config();
1400 let mut ctx = SessionContext::default();
1401 let result = execute_input("tx", &mut ctx, &config, &test_factory())
1403 .await
1404 .unwrap();
1405 assert!(!result);
1406 }
1407
1408 #[tokio::test]
1409 async fn test_crawl_no_arg() {
1410 let config = test_config();
1411 let mut ctx = SessionContext::default();
1412 let result = execute_input("crawl", &mut ctx, &config, &test_factory())
1414 .await
1415 .unwrap();
1416 assert!(!result);
1417 }
1418
1419 #[tokio::test]
1420 async fn test_multiple_context_commands() {
1421 let config = test_config();
1422 let mut ctx = SessionContext::default();
1423
1424 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1426 .await
1427 .unwrap();
1428 execute_input("format json", &mut ctx, &config, &test_factory())
1429 .await
1430 .unwrap();
1431 execute_input("+tokens", &mut ctx, &config, &test_factory())
1432 .await
1433 .unwrap();
1434 execute_input("trace", &mut ctx, &config, &test_factory())
1435 .await
1436 .unwrap();
1437 execute_input("limit 25", &mut ctx, &config, &test_factory())
1438 .await
1439 .unwrap();
1440
1441 assert_eq!(ctx.chain, "polygon");
1442 assert_eq!(ctx.format, OutputFormat::Json);
1443 assert!(ctx.include_tokens);
1444 assert!(ctx.trace);
1445 assert_eq!(ctx.limit, 25);
1446
1447 execute_input("clear", &mut ctx, &config, &test_factory())
1449 .await
1450 .unwrap();
1451 assert_eq!(ctx.chain, "auto");
1452 assert!(!ctx.include_tokens);
1453 assert!(!ctx.trace);
1454 assert_eq!(ctx.limit, 100);
1455 }
1456
1457 #[tokio::test]
1458 async fn test_dot_prefix_commands() {
1459 let config = test_config();
1460 let mut ctx = SessionContext::default();
1461
1462 let result = execute_input(".help", &mut ctx, &config, &test_factory())
1464 .await
1465 .unwrap();
1466 assert!(!result);
1467
1468 execute_input(".chain polygon", &mut ctx, &config, &test_factory())
1469 .await
1470 .unwrap();
1471 assert_eq!(ctx.chain, "polygon");
1472
1473 execute_input(".format json", &mut ctx, &config, &test_factory())
1474 .await
1475 .unwrap();
1476 assert_eq!(ctx.format, OutputFormat::Json);
1477
1478 execute_input(".trace", &mut ctx, &config, &test_factory())
1479 .await
1480 .unwrap();
1481 assert!(ctx.trace);
1482
1483 execute_input(".decode", &mut ctx, &config, &test_factory())
1484 .await
1485 .unwrap();
1486 assert!(ctx.decode);
1487 }
1488
1489 #[tokio::test]
1490 async fn test_all_valid_chains() {
1491 let config = test_config();
1492 let valid_chains = [
1493 "ethereum", "polygon", "arbitrum", "optimism", "base", "bsc", "solana", "tron",
1494 ];
1495 for chain in valid_chains {
1496 let mut ctx = SessionContext::default();
1497 execute_input(
1498 &format!("chain {}", chain),
1499 &mut ctx,
1500 &config,
1501 &test_factory(),
1502 )
1503 .await
1504 .unwrap();
1505 assert_eq!(ctx.chain, chain);
1506 assert!(!ctx.is_auto_chain());
1507 }
1508 }
1509
1510 use crate::chains::mocks::MockClientFactory;
1515
1516 fn mock_factory() -> MockClientFactory {
1517 let mut factory = MockClientFactory::new();
1518 factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
1519 factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
1520 token: crate::chains::Token {
1521 contract_address: "0xtoken".to_string(),
1522 symbol: "TEST".to_string(),
1523 name: "Test Token".to_string(),
1524 decimals: 18,
1525 },
1526 balance: "1000".to_string(),
1527 formatted_balance: "0.001".to_string(),
1528 usd_value: None,
1529 }];
1530 factory
1531 }
1532
1533 #[tokio::test]
1534 async fn test_address_command_with_args() {
1535 let config = test_config();
1536 let factory = mock_factory();
1537 let mut ctx = SessionContext::default();
1538 let result = execute_input(
1539 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1540 &mut ctx,
1541 &config,
1542 &factory,
1543 )
1544 .await;
1545 assert!(result.is_ok());
1546 assert!(!result.unwrap());
1547 assert_eq!(
1548 ctx.last_address,
1549 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
1550 );
1551 }
1552
1553 #[tokio::test]
1554 async fn test_address_command_with_chain_override() {
1555 let config = test_config();
1556 let factory = mock_factory();
1557 let mut ctx = SessionContext::default();
1558 let result = execute_input(
1559 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon",
1560 &mut ctx,
1561 &config,
1562 &factory,
1563 )
1564 .await;
1565 assert!(result.is_ok());
1566 }
1567
1568 #[tokio::test]
1569 async fn test_address_command_with_tokens_flag() {
1570 let config = test_config();
1571 let factory = mock_factory();
1572 let mut ctx = SessionContext::default();
1573 let result = execute_input(
1574 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --tokens",
1575 &mut ctx,
1576 &config,
1577 &factory,
1578 )
1579 .await;
1580 assert!(result.is_ok());
1581 }
1582
1583 #[tokio::test]
1584 async fn test_address_command_with_txs_flag() {
1585 let config = test_config();
1586 let factory = mock_factory();
1587 let mut ctx = SessionContext::default();
1588 let result = execute_input(
1589 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --txs",
1590 &mut ctx,
1591 &config,
1592 &factory,
1593 )
1594 .await;
1595 assert!(result.is_ok());
1596 }
1597
1598 #[tokio::test]
1599 async fn test_address_reuses_last_address() {
1600 let config = test_config();
1601 let factory = mock_factory();
1602 let mut ctx = SessionContext {
1603 last_address: Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string()),
1604 ..Default::default()
1605 };
1606 let result = execute_input("address", &mut ctx, &config, &factory).await;
1607 assert!(result.is_ok());
1608 }
1609
1610 #[tokio::test]
1611 async fn test_address_auto_detects_solana() {
1612 let config = test_config();
1613 let factory = mock_factory();
1614 let mut ctx = SessionContext::default();
1615 let result = execute_input(
1617 "address DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy",
1618 &mut ctx,
1619 &config,
1620 &factory,
1621 )
1622 .await;
1623 assert!(result.is_ok());
1624 assert_eq!(ctx.chain, "auto");
1626 }
1627
1628 #[tokio::test]
1629 async fn test_tx_command_with_args() {
1630 let config = test_config();
1631 let factory = mock_factory();
1632 let mut ctx = SessionContext::default();
1633 let result = execute_input(
1634 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd",
1635 &mut ctx,
1636 &config,
1637 &factory,
1638 )
1639 .await;
1640 assert!(result.is_ok());
1641 assert_eq!(
1642 ctx.last_tx,
1643 Some("0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string())
1644 );
1645 }
1646
1647 #[tokio::test]
1648 async fn test_tx_command_with_trace_decode() {
1649 let config = test_config();
1650 let factory = mock_factory();
1651 let mut ctx = SessionContext::default();
1652 let result = execute_input(
1653 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --trace --decode",
1654 &mut ctx,
1655 &config,
1656 &factory,
1657 )
1658 .await;
1659 assert!(result.is_ok());
1660 }
1661
1662 #[tokio::test]
1663 async fn test_tx_command_with_chain_override() {
1664 let config = test_config();
1665 let factory = mock_factory();
1666 let mut ctx = SessionContext::default();
1667 let result = execute_input(
1668 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --chain=polygon",
1669 &mut ctx,
1670 &config,
1671 &factory,
1672 )
1673 .await;
1674 assert!(result.is_ok());
1675 }
1676
1677 #[tokio::test]
1678 async fn test_tx_reuses_last_tx() {
1679 let config = test_config();
1680 let factory = mock_factory();
1681 let mut ctx = SessionContext {
1682 last_tx: Some(
1683 "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
1684 ),
1685 ..Default::default()
1686 };
1687 let result = execute_input("tx", &mut ctx, &config, &factory).await;
1688 assert!(result.is_ok());
1689 }
1690
1691 #[tokio::test]
1692 async fn test_tx_auto_detects_tron() {
1693 let config = test_config();
1694 let factory = mock_factory();
1695 let mut ctx = SessionContext::default();
1696 let result = execute_input(
1697 "tx abc123def456789012345678901234567890123456789012345678901234abcd",
1698 &mut ctx,
1699 &config,
1700 &factory,
1701 )
1702 .await;
1703 assert!(result.is_ok());
1704 assert_eq!(ctx.chain, "auto");
1706 }
1707
1708 #[tokio::test]
1709 async fn test_crawl_command_with_args() {
1710 let config = test_config();
1711 let factory = mock_factory();
1712 let mut ctx = SessionContext::default();
1713 let result = execute_input(
1714 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
1715 &mut ctx,
1716 &config,
1717 &factory,
1718 )
1719 .await;
1720 assert!(result.is_ok());
1721 }
1722
1723 #[tokio::test]
1724 async fn test_crawl_command_with_period() {
1725 let config = test_config();
1726 let factory = mock_factory();
1727 let mut ctx = SessionContext::default();
1728 let result = execute_input(
1729 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d --no-charts",
1730 &mut ctx,
1731 &config,
1732 &factory,
1733 )
1734 .await;
1735 assert!(result.is_ok());
1736 }
1737
1738 #[tokio::test]
1739 async fn test_crawl_command_with_chain_flag() {
1740 let config = test_config();
1741 let factory = mock_factory();
1742 let mut ctx = SessionContext::default();
1743 let result = execute_input(
1744 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --chain polygon --no-charts",
1745 &mut ctx,
1746 &config,
1747 &factory,
1748 )
1749 .await;
1750 assert!(result.is_ok());
1751 }
1752
1753 #[tokio::test]
1754 async fn test_crawl_command_with_period_flag() {
1755 let config = test_config();
1756 let factory = mock_factory();
1757 let mut ctx = SessionContext::default();
1758 let result = execute_input(
1759 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h --no-charts",
1760 &mut ctx,
1761 &config,
1762 &factory,
1763 )
1764 .await;
1765 assert!(result.is_ok());
1766 }
1767
1768 #[tokio::test]
1769 async fn test_crawl_command_with_report() {
1770 let config = test_config();
1771 let factory = mock_factory();
1772 let mut ctx = SessionContext::default();
1773 let tmp = tempfile::NamedTempFile::new().unwrap();
1774 let result = execute_input(
1775 &format!(
1776 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report {} --no-charts",
1777 tmp.path().display()
1778 ),
1779 &mut ctx,
1780 &config,
1781 &factory,
1782 )
1783 .await;
1784 assert!(result.is_ok());
1785 }
1786
1787 #[tokio::test]
1788 async fn test_portfolio_list_command() {
1789 let tmp_dir = tempfile::tempdir().unwrap();
1790 let config = Config {
1791 address_book: crate::config::AddressBookConfig {
1792 data_dir: Some(tmp_dir.path().to_path_buf()),
1793 },
1794 ..Default::default()
1795 };
1796 let factory = mock_factory();
1797 let mut ctx = SessionContext::default();
1798 let result = execute_input("portfolio list", &mut ctx, &config, &factory).await;
1799 assert!(result.is_ok());
1800 }
1801
1802 #[tokio::test]
1803 async fn test_portfolio_add_command() {
1804 let tmp_dir = tempfile::tempdir().unwrap();
1805 let config = Config {
1806 address_book: crate::config::AddressBookConfig {
1807 data_dir: Some(tmp_dir.path().to_path_buf()),
1808 },
1809 ..Default::default()
1810 };
1811 let factory = mock_factory();
1812 let mut ctx = SessionContext::default();
1813 let result = execute_input(
1814 "portfolio add 0xtest --label mytest",
1815 &mut ctx,
1816 &config,
1817 &factory,
1818 )
1819 .await;
1820 assert!(result.is_ok());
1821 }
1822
1823 #[tokio::test]
1824 async fn test_portfolio_summary_command() {
1825 let tmp_dir = tempfile::tempdir().unwrap();
1826 let config = Config {
1827 address_book: crate::config::AddressBookConfig {
1828 data_dir: Some(tmp_dir.path().to_path_buf()),
1829 },
1830 ..Default::default()
1831 };
1832 let factory = mock_factory();
1833 let mut ctx = SessionContext::default();
1834 execute_input("portfolio add 0xtest", &mut ctx, &config, &factory)
1836 .await
1837 .unwrap();
1838 let result = execute_input("portfolio summary", &mut ctx, &config, &factory).await;
1840 assert!(result.is_ok());
1841 }
1842
1843 #[tokio::test]
1844 async fn test_portfolio_remove_command() {
1845 let tmp_dir = tempfile::tempdir().unwrap();
1846 let config = Config {
1847 address_book: crate::config::AddressBookConfig {
1848 data_dir: Some(tmp_dir.path().to_path_buf()),
1849 },
1850 ..Default::default()
1851 };
1852 let factory = mock_factory();
1853 let mut ctx = SessionContext::default();
1854 let result = execute_input("portfolio remove 0xtest", &mut ctx, &config, &factory).await;
1855 assert!(result.is_ok());
1856 }
1857
1858 #[tokio::test]
1859 async fn test_portfolio_no_subcommand() {
1860 let config = test_config();
1861 let factory = mock_factory();
1862 let mut ctx = SessionContext::default();
1863 let result = execute_input("portfolio", &mut ctx, &config, &factory).await;
1864 assert!(result.is_ok());
1865 }
1866
1867 #[tokio::test]
1868 async fn test_portfolio_unknown_subcommand() {
1869 let tmp_dir = tempfile::tempdir().unwrap();
1870 let config = Config {
1871 address_book: crate::config::AddressBookConfig {
1872 data_dir: Some(tmp_dir.path().to_path_buf()),
1873 },
1874 ..Default::default()
1875 };
1876 let factory = mock_factory();
1877 let mut ctx = SessionContext::default();
1878 let result = execute_input("portfolio foobar", &mut ctx, &config, &factory).await;
1879 assert!(result.is_ok());
1880 }
1881
1882 #[tokio::test]
1883 async fn test_tokens_command_list() {
1884 let config = test_config();
1885 let factory = mock_factory();
1886 let mut ctx = SessionContext::default();
1887 let result = execute_input("tokens list", &mut ctx, &config, &factory).await;
1888 assert!(result.is_ok());
1889 }
1890
1891 #[tokio::test]
1892 async fn test_tokens_command_no_args() {
1893 let config = test_config();
1894 let factory = mock_factory();
1895 let mut ctx = SessionContext::default();
1896 let result = execute_input("tokens", &mut ctx, &config, &factory).await;
1897 assert!(result.is_ok());
1898 }
1899
1900 #[tokio::test]
1901 async fn test_tokens_command_recent() {
1902 let config = test_config();
1903 let factory = mock_factory();
1904 let mut ctx = SessionContext::default();
1905 let result = execute_input("tokens recent", &mut ctx, &config, &factory).await;
1906 assert!(result.is_ok());
1907 }
1908
1909 #[tokio::test]
1910 async fn test_tokens_command_remove_no_args() {
1911 let config = test_config();
1912 let factory = mock_factory();
1913 let mut ctx = SessionContext::default();
1914 let result = execute_input("tokens remove", &mut ctx, &config, &factory).await;
1915 assert!(result.is_ok());
1916 }
1917
1918 #[tokio::test]
1919 async fn test_tokens_command_add_no_args() {
1920 let config = test_config();
1921 let factory = mock_factory();
1922 let mut ctx = SessionContext::default();
1923 let result = execute_input("tokens add", &mut ctx, &config, &factory).await;
1924 assert!(result.is_ok());
1925 }
1926
1927 #[tokio::test]
1928 async fn test_tokens_command_unknown() {
1929 let config = test_config();
1930 let factory = mock_factory();
1931 let mut ctx = SessionContext::default();
1932 let result = execute_input("tokens foobar", &mut ctx, &config, &factory).await;
1933 assert!(result.is_ok());
1934 }
1935
1936 #[tokio::test]
1937 async fn test_setup_command_status() {
1938 let config = test_config();
1939 let factory = mock_factory();
1940 let mut ctx = SessionContext::default();
1941 let result = execute_input("setup --status", &mut ctx, &config, &factory).await;
1942 assert!(result.is_ok());
1943 }
1944
1945 #[tokio::test]
1946 async fn test_transaction_alias() {
1947 let config = test_config();
1948 let factory = mock_factory();
1949 let mut ctx = SessionContext::default();
1950 let result = execute_input(
1951 "transaction 0xabc123def456789012345678901234567890123456789012345678901234abcd",
1952 &mut ctx,
1953 &config,
1954 &factory,
1955 )
1956 .await;
1957 assert!(result.is_ok());
1958 }
1959
1960 #[tokio::test]
1961 async fn test_token_alias_for_crawl() {
1962 let config = test_config();
1963 let factory = mock_factory();
1964 let mut ctx = SessionContext::default();
1965 let result = execute_input(
1966 "token 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
1967 &mut ctx,
1968 &config,
1969 &factory,
1970 )
1971 .await;
1972 assert!(result.is_ok());
1973 }
1974
1975 #[tokio::test]
1976 async fn test_port_alias_for_portfolio() {
1977 let tmp_dir = tempfile::tempdir().unwrap();
1978 let config = Config {
1979 address_book: crate::config::AddressBookConfig {
1980 data_dir: Some(tmp_dir.path().to_path_buf()),
1981 },
1982 ..Default::default()
1983 };
1984 let factory = mock_factory();
1985 let mut ctx = SessionContext::default();
1986 let result = execute_input("port list", &mut ctx, &config, &factory).await;
1987 assert!(result.is_ok());
1988 }
1989
1990 #[tokio::test]
1995 async fn test_execute_tokens_list_empty() {
1996 let result = execute_tokens_command(&[]).await;
1997 assert!(result.is_ok());
1998 }
1999
2000 #[tokio::test]
2001 async fn test_execute_tokens_list_subcommand() {
2002 let result = execute_tokens_command(&["list"]).await;
2003 assert!(result.is_ok());
2004 }
2005
2006 #[tokio::test]
2007 async fn test_execute_tokens_recent() {
2008 let result = execute_tokens_command(&["recent"]).await;
2009 assert!(result.is_ok());
2010 }
2011
2012 #[tokio::test]
2013 async fn test_execute_tokens_add_insufficient_args() {
2014 let result = execute_tokens_command(&["add"]).await;
2015 assert!(result.is_ok());
2016 }
2017
2018 #[tokio::test]
2019 async fn test_execute_tokens_add_success() {
2020 let result = execute_tokens_command(&[
2021 "add",
2022 "TEST_INTERACTIVE",
2023 "ethereum",
2024 "0xtest123456789",
2025 "Test Token",
2026 ])
2027 .await;
2028 assert!(result.is_ok());
2029 let _ = execute_tokens_command(&["remove", "TEST_INTERACTIVE"]).await;
2030 }
2031
2032 #[tokio::test]
2033 async fn test_execute_tokens_remove_no_args() {
2034 let result = execute_tokens_command(&["remove"]).await;
2035 assert!(result.is_ok());
2036 }
2037
2038 #[tokio::test]
2039 async fn test_execute_tokens_remove_with_symbol() {
2040 let _ =
2041 execute_tokens_command(&["add", "RMTEST", "ethereum", "0xrmtest", "Remove Test"]).await;
2042 let result = execute_tokens_command(&["remove", "RMTEST"]).await;
2043 assert!(result.is_ok());
2044 }
2045
2046 #[tokio::test]
2047 async fn test_execute_tokens_unknown_subcommand() {
2048 let result = execute_tokens_command(&["invalid"]).await;
2049 assert!(result.is_ok());
2050 }
2051
2052 #[test]
2057 fn test_session_context_serialization_roundtrip() {
2058 let ctx = SessionContext {
2059 chain: "solana".to_string(),
2060 include_tokens: true,
2061 limit: 25,
2062 last_address: Some("0xtest".to_string()),
2063 ..Default::default()
2064 };
2065
2066 let yaml = serde_yaml::to_string(&ctx).unwrap();
2067 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
2068 assert_eq!(deserialized.chain, "solana");
2069 assert!(deserialized.include_tokens);
2070 assert_eq!(deserialized.limit, 25);
2071 assert_eq!(deserialized.last_address, Some("0xtest".to_string()));
2072 }
2073
2074 #[tokio::test]
2079 async fn test_chain_show_explicit() {
2080 let config = test_config();
2081 let factory = test_factory();
2082 let mut context = SessionContext {
2083 chain: "polygon".to_string(),
2084 ..Default::default()
2085 };
2086
2087 let result = execute_input("chain", &mut context, &config, &factory).await;
2089 assert!(result.is_ok());
2090 assert!(!result.unwrap()); }
2092
2093 #[tokio::test]
2094 async fn test_address_with_explicit_chain() {
2095 let config = test_config();
2096 let factory = mock_factory();
2097 let mut context = SessionContext {
2098 chain: "polygon".to_string(),
2099 ..Default::default()
2100 };
2101
2102 let result = execute_input(
2104 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2105 &mut context,
2106 &config,
2107 &factory,
2108 )
2109 .await;
2110 assert!(result.is_ok() || result.is_err());
2112 }
2113
2114 #[tokio::test]
2115 async fn test_tx_with_explicit_chain() {
2116 let config = test_config();
2117 let factory = mock_factory();
2118 let mut context = SessionContext {
2119 chain: "polygon".to_string(),
2120 ..Default::default()
2121 };
2122
2123 let result = execute_input("tx 0xabc123def456789", &mut context, &config, &factory).await;
2125 assert!(result.is_ok() || result.is_err());
2126 }
2127
2128 #[tokio::test]
2129 async fn test_crawl_with_period_eq_flag() {
2130 let config = test_config();
2131 let factory = test_factory();
2132 let mut context = SessionContext::default();
2133
2134 let result = execute_input(
2136 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d",
2137 &mut context,
2138 &config,
2139 &factory,
2140 )
2141 .await;
2142 assert!(result.is_ok() || result.is_err());
2144 }
2145
2146 #[tokio::test]
2147 async fn test_crawl_with_period_space_flag() {
2148 let config = test_config();
2149 let factory = test_factory();
2150 let mut context = SessionContext::default();
2151
2152 let result = execute_input(
2154 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h",
2155 &mut context,
2156 &config,
2157 &factory,
2158 )
2159 .await;
2160 assert!(result.is_ok() || result.is_err());
2161 }
2162
2163 #[tokio::test]
2164 async fn test_crawl_with_chain_eq_flag() {
2165 let config = test_config();
2166 let factory = test_factory();
2167 let mut context = SessionContext::default();
2168
2169 let result = execute_input(
2171 "crawl 0xAddress --chain=polygon",
2172 &mut context,
2173 &config,
2174 &factory,
2175 )
2176 .await;
2177 assert!(result.is_ok() || result.is_err());
2178 }
2179
2180 #[tokio::test]
2181 async fn test_crawl_with_chain_space_flag() {
2182 let config = test_config();
2183 let factory = test_factory();
2184 let mut context = SessionContext::default();
2185
2186 let result = execute_input(
2188 "crawl 0xAddress --chain polygon",
2189 &mut context,
2190 &config,
2191 &factory,
2192 )
2193 .await;
2194 assert!(result.is_ok() || result.is_err());
2195 }
2196
2197 #[tokio::test]
2198 async fn test_crawl_with_report_flag() {
2199 let config = test_config();
2200 let factory = test_factory();
2201 let mut context = SessionContext::default();
2202
2203 let tmp = tempfile::NamedTempFile::new().unwrap();
2204 let path = tmp.path().to_string_lossy();
2205 let input = format!(
2206 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report={}",
2207 path
2208 );
2209 let result = execute_input(&input, &mut context, &config, &factory).await;
2210 assert!(result.is_ok() || result.is_err());
2211 }
2212
2213 #[tokio::test]
2214 async fn test_crawl_with_no_charts_flag() {
2215 let config = test_config();
2216 let factory = test_factory();
2217 let mut context = SessionContext::default();
2218
2219 let result = execute_input(
2220 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
2221 &mut context,
2222 &config,
2223 &factory,
2224 )
2225 .await;
2226 assert!(result.is_ok() || result.is_err());
2227 }
2228
2229 #[tokio::test]
2230 async fn test_crawl_with_explicit_chain() {
2231 let config = test_config();
2232 let factory = test_factory();
2233 let mut context = SessionContext {
2234 chain: "arbitrum".to_string(),
2235 ..Default::default()
2236 };
2237
2238 let result = execute_input("crawl 0xAddress", &mut context, &config, &factory).await;
2239 assert!(result.is_ok() || result.is_err());
2240 }
2241
2242 #[tokio::test]
2243 async fn test_portfolio_add_with_label_and_tags() {
2244 let tmp_dir = tempfile::tempdir().unwrap();
2245 let config = Config {
2246 address_book: crate::config::AddressBookConfig {
2247 data_dir: Some(tmp_dir.path().to_path_buf()),
2248 },
2249 ..Default::default()
2250 };
2251 let factory = mock_factory();
2252 let mut context = SessionContext::default();
2253
2254 let result = execute_input(
2255 "portfolio add 0xAbC123 --label MyWallet --tags defi,staking",
2256 &mut context,
2257 &config,
2258 &factory,
2259 )
2260 .await;
2261 assert!(result.is_ok());
2262 }
2263
2264 #[tokio::test]
2265 async fn test_portfolio_remove_no_args() {
2266 let tmp_dir = tempfile::tempdir().unwrap();
2267 let config = Config {
2268 address_book: crate::config::AddressBookConfig {
2269 data_dir: Some(tmp_dir.path().to_path_buf()),
2270 },
2271 ..Default::default()
2272 };
2273 let factory = mock_factory();
2274 let mut context = SessionContext::default();
2275
2276 let result = execute_input("portfolio remove", &mut context, &config, &factory).await;
2277 assert!(result.is_ok());
2278 }
2279
2280 #[tokio::test]
2281 async fn test_portfolio_summary_with_chain_and_tag() {
2282 let tmp_dir = tempfile::tempdir().unwrap();
2283 let config = Config {
2284 address_book: crate::config::AddressBookConfig {
2285 data_dir: Some(tmp_dir.path().to_path_buf()),
2286 },
2287 ..Default::default()
2288 };
2289 let factory = mock_factory();
2290 let mut context = SessionContext::default();
2291
2292 let result = execute_input(
2293 "portfolio summary --chain ethereum --tag defi --tokens",
2294 &mut context,
2295 &config,
2296 &factory,
2297 )
2298 .await;
2299 assert!(result.is_ok());
2300 }
2301
2302 #[tokio::test]
2303 async fn test_tokens_add_with_name() {
2304 let result = execute_tokens_command(&[
2305 "add",
2306 "USDC",
2307 "ethereum",
2308 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2309 "USD",
2310 "Coin",
2311 ])
2312 .await;
2313 assert!(result.is_ok());
2314 }
2315
2316 #[tokio::test]
2317 async fn test_tokens_remove_with_chain() {
2318 let result = execute_tokens_command(&["remove", "USDC", "--chain", "ethereum"]).await;
2319 assert!(result.is_ok());
2320 }
2321
2322 #[tokio::test]
2323 async fn test_tokens_add_then_list_nonempty() {
2324 let _ = execute_tokens_command(&[
2326 "add",
2327 "TEST_TOKEN_XYZ",
2328 "ethereum",
2329 "0x1234567890abcdef1234567890abcdef12345678",
2330 "Test",
2331 "Token",
2332 ])
2333 .await;
2334
2335 let result = execute_tokens_command(&["list"]).await;
2337 assert!(result.is_ok());
2338
2339 let result = execute_tokens_command(&["recent"]).await;
2341 assert!(result.is_ok());
2342
2343 let _ = execute_tokens_command(&["remove", "TEST_TOKEN_XYZ"]).await;
2345 }
2346
2347 #[tokio::test]
2348 async fn test_session_context_save_and_load() {
2349 let ctx = SessionContext {
2352 chain: "solana".to_string(),
2353 last_address: Some("0xabc".to_string()),
2354 last_tx: Some("0xdef".to_string()),
2355 ..Default::default()
2356 };
2357 let _ = ctx.save();
2359 let loaded = SessionContext::load();
2361 assert!(!loaded.chain.is_empty());
2363 }
2364
2365 #[tokio::test]
2370 async fn test_help_alias_question_mark() {
2371 let config = test_config();
2372 let mut ctx = SessionContext::default();
2373 let result = execute_input("?", &mut ctx, &config, &test_factory())
2374 .await
2375 .unwrap();
2376 assert!(!result);
2377 }
2378
2379 #[tokio::test]
2380 async fn test_context_alias() {
2381 let config = test_config();
2382 let mut ctx = SessionContext::default();
2383 let result = execute_input("context", &mut ctx, &config, &test_factory())
2384 .await
2385 .unwrap();
2386 assert!(!result);
2387 }
2388
2389 #[tokio::test]
2390 async fn test_dot_context_alias() {
2391 let config = test_config();
2392 let mut ctx = SessionContext::default();
2393 let result = execute_input(".context", &mut ctx, &config, &test_factory())
2394 .await
2395 .unwrap();
2396 assert!(!result);
2397 }
2398
2399 #[tokio::test]
2400 async fn test_reset_alias() {
2401 let config = test_config();
2402 let mut ctx = SessionContext {
2403 chain: "ethereum".to_string(),
2404 ..Default::default()
2405 };
2406 execute_input("reset", &mut ctx, &config, &test_factory())
2407 .await
2408 .unwrap();
2409 assert_eq!(ctx.chain, "auto");
2410 }
2411
2412 #[tokio::test]
2413 async fn test_dot_reset_alias() {
2414 let config = test_config();
2415 let mut ctx = SessionContext {
2416 chain: "base".to_string(),
2417 ..Default::default()
2418 };
2419 execute_input(".reset", &mut ctx, &config, &test_factory())
2420 .await
2421 .unwrap();
2422 assert_eq!(ctx.chain, "auto");
2423 }
2424
2425 #[tokio::test]
2426 async fn test_dot_clear_alias() {
2427 let config = test_config();
2428 let mut ctx = SessionContext {
2429 chain: "bsc".to_string(),
2430 ..Default::default()
2431 };
2432 execute_input(".clear", &mut ctx, &config, &test_factory())
2433 .await
2434 .unwrap();
2435 assert_eq!(ctx.chain, "auto");
2436 }
2437
2438 #[tokio::test]
2439 async fn test_showtokens_alias() {
2440 let config = test_config();
2441 let mut ctx = SessionContext::default();
2442 execute_input("showtokens", &mut ctx, &config, &test_factory())
2443 .await
2444 .unwrap();
2445 assert!(ctx.include_tokens);
2446 }
2447
2448 #[tokio::test]
2449 async fn test_showtxs_alias() {
2450 let config = test_config();
2451 let mut ctx = SessionContext::default();
2452 execute_input("showtxs", &mut ctx, &config, &test_factory())
2453 .await
2454 .unwrap();
2455 assert!(ctx.include_txs);
2456 }
2457
2458 #[tokio::test]
2459 async fn test_txs_alias() {
2460 let config = test_config();
2461 let mut ctx = SessionContext::default();
2462 execute_input("txs", &mut ctx, &config, &test_factory())
2463 .await
2464 .unwrap();
2465 assert!(ctx.include_txs);
2466 }
2467
2468 #[tokio::test]
2469 async fn test_dot_txs_alias() {
2470 let config = test_config();
2471 let mut ctx = SessionContext::default();
2472 execute_input(".txs", &mut ctx, &config, &test_factory())
2473 .await
2474 .unwrap();
2475 assert!(ctx.include_txs);
2476 }
2477
2478 #[tokio::test]
2479 async fn test_addr_alias() {
2480 let config = test_config();
2481 let factory = mock_factory();
2482 let mut ctx = SessionContext::default();
2483 let result = execute_input(
2484 "addr 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2485 &mut ctx,
2486 &config,
2487 &factory,
2488 )
2489 .await;
2490 assert!(result.is_ok());
2491 assert_eq!(
2492 ctx.last_address,
2493 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
2494 );
2495 }
2496
2497 #[test]
2498 fn test_session_context_is_auto_chain() {
2499 let auto_ctx = SessionContext::default();
2500 assert!(auto_ctx.is_auto_chain());
2501 let pinned_ctx = SessionContext {
2502 chain: "ethereum".to_string(),
2503 ..Default::default()
2504 };
2505 assert!(!pinned_ctx.is_auto_chain());
2506 }
2507
2508 #[test]
2509 fn test_print_help_no_panic() {
2510 print_help();
2511 }
2512
2513 #[tokio::test]
2518 async fn test_contract_no_args() {
2519 let config = test_config();
2520 let factory = mock_factory();
2521 let mut ctx = SessionContext::default();
2522 let result = execute_input("contract", &mut ctx, &config, &factory).await;
2523 assert!(result.is_ok());
2524 assert!(!result.unwrap());
2525 }
2526
2527 #[tokio::test]
2528 async fn test_contract_ct_alias_with_args() {
2529 let config = test_config();
2530 let factory = mock_factory();
2531 let mut ctx = SessionContext::default();
2532 let result = execute_input(
2533 "ct 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2534 &mut ctx,
2535 &config,
2536 &factory,
2537 )
2538 .await;
2539 if let Ok(should_exit) = result {
2540 assert!(!should_exit);
2541 }
2542 }
2543
2544 #[tokio::test]
2545 async fn test_contract_with_chain_and_json() {
2546 let config = test_config();
2547 let factory = mock_factory();
2548 let mut ctx = SessionContext::default();
2549 let result = execute_input(
2550 "contract 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon --json",
2551 &mut ctx,
2552 &config,
2553 &factory,
2554 )
2555 .await;
2556 if let Ok(should_exit) = result {
2557 assert!(!should_exit);
2558 }
2559 }
2560
2561 #[tokio::test]
2566 async fn test_address_book_list_command() {
2567 let tmp_dir = tempfile::tempdir().unwrap();
2568 let config = Config {
2569 address_book: crate::config::AddressBookConfig {
2570 data_dir: Some(tmp_dir.path().to_path_buf()),
2571 },
2572 ..Default::default()
2573 };
2574 let factory = mock_factory();
2575 let mut ctx = SessionContext::default();
2576 let result = execute_input("address-book list", &mut ctx, &config, &factory).await;
2577 assert!(result.is_ok());
2578 }
2579
2580 #[tokio::test]
2581 async fn test_address_book_underscore_list() {
2582 let tmp_dir = tempfile::tempdir().unwrap();
2583 let config = Config {
2584 address_book: crate::config::AddressBookConfig {
2585 data_dir: Some(tmp_dir.path().to_path_buf()),
2586 },
2587 ..Default::default()
2588 };
2589 let factory = mock_factory();
2590 let mut ctx = SessionContext::default();
2591 let result = execute_input("address_book list", &mut ctx, &config, &factory).await;
2592 assert!(result.is_ok());
2593 }
2594
2595 #[tokio::test]
2596 async fn test_address_book_add_insufficient_args() {
2597 let tmp_dir = tempfile::tempdir().unwrap();
2598 let config = Config {
2599 address_book: crate::config::AddressBookConfig {
2600 data_dir: Some(tmp_dir.path().to_path_buf()),
2601 },
2602 ..Default::default()
2603 };
2604 let factory = mock_factory();
2605 let mut ctx = SessionContext::default();
2606 let result = execute_input("address-book add", &mut ctx, &config, &factory).await;
2607 assert!(result.is_ok());
2608 }
2609
2610 #[tokio::test]
2611 async fn test_address_book_remove_insufficient_args() {
2612 let tmp_dir = tempfile::tempdir().unwrap();
2613 let config = Config {
2614 address_book: crate::config::AddressBookConfig {
2615 data_dir: Some(tmp_dir.path().to_path_buf()),
2616 },
2617 ..Default::default()
2618 };
2619 let factory = mock_factory();
2620 let mut ctx = SessionContext::default();
2621 let result = execute_input("address-book remove", &mut ctx, &config, &factory).await;
2622 assert!(result.is_ok());
2623 }
2624
2625 #[tokio::test]
2626 async fn test_address_book_empty_subcommand() {
2627 let tmp_dir = tempfile::tempdir().unwrap();
2628 let config = Config {
2629 address_book: crate::config::AddressBookConfig {
2630 data_dir: Some(tmp_dir.path().to_path_buf()),
2631 },
2632 ..Default::default()
2633 };
2634 let factory = mock_factory();
2635 let mut ctx = SessionContext::default();
2636 let result = execute_input("address-book", &mut ctx, &config, &factory).await;
2637 assert!(result.is_ok());
2638 }
2639
2640 #[tokio::test]
2645 async fn test_aliases_command() {
2646 let config = test_config();
2647 let factory = mock_factory();
2648 let mut ctx = SessionContext::default();
2649 let result = execute_input("aliases", &mut ctx, &config, &factory).await;
2650 assert!(result.is_ok());
2651 }
2652
2653 #[tokio::test]
2654 async fn test_config_alias() {
2655 let config = test_config();
2656 let factory = mock_factory();
2657 let mut ctx = SessionContext::default();
2658 let result = execute_input("config --status", &mut ctx, &config, &factory).await;
2659 assert!(result.is_ok());
2660 }
2661
2662 #[tokio::test]
2663 #[ignore = "setup --key prompts for API key input on stdin"]
2664 async fn test_setup_with_key_flag() {
2665 let config = test_config();
2666 let factory = mock_factory();
2667 let mut ctx = SessionContext::default();
2668 let result = execute_input("setup --key=etherscan", &mut ctx, &config, &factory).await;
2669 assert!(result.is_ok());
2670 }
2671
2672 #[tokio::test]
2673 async fn test_setup_with_key_short_flag() {
2674 let config = test_config();
2675 let factory = mock_factory();
2676 let mut ctx = SessionContext::default();
2677 let result = execute_input("setup -s", &mut ctx, &config, &factory).await;
2678 assert!(result.is_ok());
2679 }
2680
2681 #[tokio::test]
2685 #[ignore = "monitor starts TUI and blocks until exit"]
2686 async fn test_monitor_command_no_token() {
2687 let config = test_config();
2688 let factory = mock_factory();
2689 let mut ctx = SessionContext::default();
2690 let result = execute_input("monitor", &mut ctx, &config, &factory).await;
2691 assert!(result.is_ok() || result.is_err());
2692 }
2693
2694 #[tokio::test]
2695 #[ignore = "monitor starts TUI and blocks until exit"]
2696 async fn test_mon_alias() {
2697 let config = test_config();
2698 let factory = mock_factory();
2699 let mut ctx = SessionContext::default();
2700 let result = execute_input(
2701 "mon 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2702 &mut ctx,
2703 &config,
2704 &factory,
2705 )
2706 .await;
2707 assert!(result.is_ok() || result.is_err());
2708 }
2709
2710 #[tokio::test]
2715 async fn test_tokens_ls_alias() {
2716 let config = test_config();
2717 let factory = mock_factory();
2718 let mut ctx = SessionContext::default();
2719 let result = execute_input("tokens ls", &mut ctx, &config, &factory).await;
2720 assert!(result.is_ok());
2721 }
2722
2723 #[tokio::test]
2724 async fn test_execute_tokens_ls_alias() {
2725 let result = execute_tokens_command(&["ls"]).await;
2726 assert!(result.is_ok());
2727 }
2728
2729 #[tokio::test]
2730 async fn test_crawl_period_1h() {
2731 let config = test_config();
2732 let factory = mock_factory();
2733 let mut ctx = SessionContext::default();
2734 let result = execute_input(
2735 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=1h --no-charts",
2736 &mut ctx,
2737 &config,
2738 &factory,
2739 )
2740 .await;
2741 assert!(result.is_ok());
2742 }
2743
2744 #[tokio::test]
2745 async fn test_crawl_period_30d() {
2746 let config = test_config();
2747 let factory = mock_factory();
2748 let mut ctx = SessionContext::default();
2749 let result = execute_input(
2750 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=30d --no-charts",
2751 &mut ctx,
2752 &config,
2753 &factory,
2754 )
2755 .await;
2756 assert!(result.is_ok());
2757 }
2758
2759 #[tokio::test]
2760 async fn test_crawl_invalid_period_defaults() {
2761 let config = test_config();
2762 let factory = mock_factory();
2763 let mut ctx = SessionContext::default();
2764 let result = execute_input(
2765 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=invalid --no-charts",
2766 &mut ctx,
2767 &config,
2768 &factory,
2769 )
2770 .await;
2771 assert!(result.is_ok());
2772 }
2773
2774 #[tokio::test]
2775 async fn test_tokens_add_three_args_insufficient() {
2776 let result = execute_tokens_command(&["add", "SYM", "ethereum"]).await;
2777 assert!(result.is_ok());
2778 }
2779
2780 #[tokio::test]
2781 async fn test_format_show_when_csv() {
2782 let config = test_config();
2783 let mut ctx = SessionContext {
2784 format: OutputFormat::Csv,
2785 ..Default::default()
2786 };
2787 let result = execute_input("format", &mut ctx, &config, &test_factory())
2788 .await
2789 .unwrap();
2790 assert!(!result);
2791 }
2792}