1use crate::chains::ChainClientFactory;
20use crate::config::{Config, OutputFormat};
21use crate::error::Result;
22use clap::Args;
23use rustyline::DefaultEditor;
24use rustyline::error::ReadlineError;
25use serde::{Deserialize, Serialize};
26use std::fmt;
27use std::path::PathBuf;
28
29use super::{AddressArgs, CrawlArgs, PortfolioArgs, TxArgs};
30use super::{address, crawl, monitor, portfolio, tx};
31
32#[derive(Debug, Clone, Args)]
34pub struct InteractiveArgs {
35 #[arg(long)]
37 pub no_banner: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SessionContext {
43 pub chain: String,
45
46 #[serde(default)]
48 pub chain_explicit: bool,
49
50 pub format: OutputFormat,
52
53 pub last_address: Option<String>,
55
56 pub last_tx: Option<String>,
58
59 pub include_tokens: bool,
61
62 pub include_txs: bool,
64
65 pub trace: bool,
67
68 pub decode: bool,
70
71 pub limit: u32,
73}
74
75impl Default for SessionContext {
76 fn default() -> Self {
77 Self {
78 chain: "ethereum".to_string(),
79 chain_explicit: false,
80 format: OutputFormat::Table,
81 last_address: None,
82 last_tx: None,
83 include_tokens: false,
84 include_txs: false,
85 trace: false,
86 decode: false,
87 limit: 100,
88 }
89 }
90}
91
92impl fmt::Display for SessionContext {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 writeln!(f, "Current Context:")?;
95 let chain_status = if self.chain_explicit { "" } else { " (auto)" };
96 writeln!(f, " Chain: {}{}", self.chain, chain_status)?;
97 writeln!(f, " Format: {:?}", self.format)?;
98 writeln!(f, " Include Tokens: {}", self.include_tokens)?;
99 writeln!(f, " Include TXs: {}", self.include_txs)?;
100 writeln!(f, " Trace: {}", self.trace)?;
101 writeln!(f, " Decode: {}", self.decode)?;
102 writeln!(f, " Limit: {}", self.limit)?;
103 if let Some(ref addr) = self.last_address {
104 writeln!(f, " Last Address: {}", addr)?;
105 }
106 if let Some(ref tx) = self.last_tx {
107 writeln!(f, " Last TX: {}", tx)?;
108 }
109 Ok(())
110 }
111}
112
113impl SessionContext {
114 fn context_path() -> Option<PathBuf> {
116 dirs::data_dir().map(|p| p.join("scope").join("session.yaml"))
117 }
118
119 pub fn load() -> Self {
121 Self::context_path()
122 .and_then(|path| std::fs::read_to_string(&path).ok())
123 .and_then(|contents| serde_yaml::from_str(&contents).ok())
124 .unwrap_or_default()
125 }
126
127 pub fn save(&self) -> Result<()> {
129 if let Some(path) = Self::context_path() {
130 if let Some(parent) = path.parent() {
131 std::fs::create_dir_all(parent)?;
132 }
133 let contents = serde_yaml::to_string(self)
134 .map_err(|e| crate::error::ScopeError::Export(e.to_string()))?;
135 std::fs::write(&path, contents)?;
136 }
137 Ok(())
138 }
139}
140
141pub async fn run(
143 args: InteractiveArgs,
144 config: &Config,
145 clients: &dyn ChainClientFactory,
146) -> Result<()> {
147 if !args.no_banner {
149 let banner = include_str!("../../assets/banner.txt");
150 eprintln!("{}", banner);
151 }
152
153 println!("Welcome to Scope Interactive Mode!");
154 println!("Type 'help' for available commands, 'exit' to quit.\n");
155
156 let mut context = SessionContext::load();
158
159 if context.chain == "ethereum" && context.format == OutputFormat::Table {
161 context.format = config.output.format;
162 }
163
164 let mut rl = DefaultEditor::new().map_err(|e| {
166 crate::error::ScopeError::Chain(format!("Failed to initialize readline: {}", e))
167 })?;
168
169 let history_path = dirs::data_dir().map(|p| p.join("scope").join("history.txt"));
171 if let Some(ref path) = history_path {
172 let _ = rl.load_history(path);
173 }
174
175 loop {
176 let prompt = format!("scope:{}> ", context.chain);
177
178 match rl.readline(&prompt) {
179 Ok(input_line) => {
180 let line = input_line.trim();
181 if line.is_empty() {
182 continue;
183 }
184
185 let _ = rl.add_history_entry(line);
187
188 match execute_input(line, &mut context, config, clients).await {
190 Ok(should_exit) => {
191 if should_exit {
192 break;
193 }
194 }
195 Err(e) => {
196 eprintln!("Error: {}", e);
197 }
198 }
199 }
200 Err(ReadlineError::Interrupted) => {
201 println!("^C");
202 continue;
203 }
204 Err(ReadlineError::Eof) => {
205 println!("exit");
206 break;
207 }
208 Err(err) => {
209 eprintln!("Error: {:?}", err);
210 break;
211 }
212 }
213 }
214
215 if let Some(ref path) = history_path {
217 if let Some(parent) = path.parent() {
218 let _ = std::fs::create_dir_all(parent);
219 }
220 let _ = rl.save_history(path);
221 }
222
223 if let Err(e) = context.save() {
225 tracing::warn!("Failed to save session context: {}", e);
226 }
227
228 println!("Goodbye!");
229 Ok(())
230}
231
232async fn execute_input(
234 input: &str,
235 context: &mut SessionContext,
236 config: &Config,
237 clients: &dyn ChainClientFactory,
238) -> Result<bool> {
239 let parts: Vec<&str> = input.split_whitespace().collect();
240 if parts.is_empty() {
241 return Ok(false);
242 }
243
244 let command = parts[0].to_lowercase();
245 let args = &parts[1..];
246
247 match command.as_str() {
248 "exit" | "quit" | ".exit" | ".quit" | "q" => {
250 return Ok(true);
251 }
252
253 "help" | "?" | ".help" => {
255 print_help();
256 }
257
258 "ctx" | "context" | ".ctx" | ".context" => {
260 println!("{}", context);
261 }
262
263 "clear" | ".clear" | "reset" | ".reset" => {
265 *context = SessionContext::default();
266 context.format = config.output.format;
267 println!("Context reset to defaults.");
268 }
269
270 "chain" | ".chain" => {
272 if args.is_empty() {
273 let status = if context.chain_explicit {
274 " (explicit)"
275 } else {
276 " (auto-detect enabled)"
277 };
278 println!("Current chain: {}{}", context.chain, status);
279 } else {
280 let new_chain = args[0].to_lowercase();
281 let valid_chains = [
283 "ethereum", "polygon", "arbitrum", "optimism", "base", "bsc", "aegis",
284 "solana", "tron",
285 ];
286 if valid_chains.contains(&new_chain.as_str()) {
287 context.chain = new_chain.clone();
288 context.chain_explicit = true;
289 println!("Chain set to: {} (auto-detect disabled)", new_chain);
290 } else if new_chain == "auto" {
291 context.chain = "ethereum".to_string();
293 context.chain_explicit = false;
294 println!("Chain auto-detection enabled (default: ethereum)");
295 } else {
296 eprintln!(
297 "Unknown chain: {}. Valid chains: {}, auto",
298 new_chain,
299 valid_chains.join(", ")
300 );
301 }
302 }
303 }
304
305 "format" | ".format" => {
307 if args.is_empty() {
308 println!("Current format: {:?}", context.format);
309 } else {
310 match args[0].to_lowercase().as_str() {
311 "table" => {
312 context.format = OutputFormat::Table;
313 println!("Format set to: table");
314 }
315 "json" => {
316 context.format = OutputFormat::Json;
317 println!("Format set to: json");
318 }
319 "csv" => {
320 context.format = OutputFormat::Csv;
321 println!("Format set to: csv");
322 }
323 other => {
324 eprintln!("Unknown format: {}. Valid formats: table, json, csv", other);
325 }
326 }
327 }
328 }
329
330 "+tokens" | "showtokens" => {
332 context.include_tokens = !context.include_tokens;
333 println!(
334 "Include tokens: {}",
335 if context.include_tokens { "on" } else { "off" }
336 );
337 }
338
339 "+txs" | "showtxs" | "txs" | ".txs" => {
340 context.include_txs = !context.include_txs;
341 println!(
342 "Include transactions: {}",
343 if context.include_txs { "on" } else { "off" }
344 );
345 }
346
347 "trace" | ".trace" => {
348 context.trace = !context.trace;
349 println!("Trace: {}", if context.trace { "on" } else { "off" });
350 }
351
352 "decode" | ".decode" => {
353 context.decode = !context.decode;
354 println!("Decode: {}", if context.decode { "on" } else { "off" });
355 }
356
357 "limit" | ".limit" => {
359 if args.is_empty() {
360 println!("Current limit: {}", context.limit);
361 } else if let Ok(n) = args[0].parse::<u32>() {
362 context.limit = n;
363 println!("Limit set to: {}", n);
364 } else {
365 eprintln!("Invalid limit: {}. Must be a positive integer.", args[0]);
366 }
367 }
368
369 "address" | "addr" => {
371 let addr = if args.is_empty() {
372 match &context.last_address {
374 Some(a) => a.clone(),
375 None => {
376 eprintln!("No address specified and no previous address in context.");
377 return Ok(false);
378 }
379 }
380 } else {
381 args[0].to_string()
382 };
383
384 let mut chain_override = None;
386 for arg in args.iter().skip(1) {
387 if arg.starts_with("--chain=") {
388 chain_override = Some(arg.trim_start_matches("--chain=").to_string());
389 }
390 }
391
392 let effective_chain = if let Some(chain) = chain_override {
394 chain
395 } else if !context.chain_explicit {
396 if let Some(inferred) = crate::chains::infer_chain_from_address(&addr) {
398 if inferred != context.chain {
399 println!("Auto-detected chain: {}", inferred);
400 context.chain = inferred.to_string();
402 }
403 inferred.to_string()
404 } else {
405 context.chain.clone()
406 }
407 } else {
408 context.chain.clone()
409 };
410
411 let mut address_args = AddressArgs {
413 address: addr.clone(),
414 chain: effective_chain,
415 format: Some(context.format),
416 include_txs: context.include_txs,
417 include_tokens: context.include_tokens,
418 limit: context.limit,
419 };
420
421 for arg in args.iter().skip(1) {
423 if *arg == "--tokens" {
424 address_args.include_tokens = true;
425 } else if *arg == "--txs" {
426 address_args.include_txs = true;
427 }
428 }
429
430 context.last_address = Some(addr);
432
433 address::run(address_args, config, clients).await?;
435 }
436
437 "tx" | "transaction" => {
439 let hash = if args.is_empty() {
440 match &context.last_tx {
442 Some(h) => h.clone(),
443 None => {
444 eprintln!("No transaction hash specified and no previous hash in context.");
445 return Ok(false);
446 }
447 }
448 } else {
449 args[0].to_string()
450 };
451
452 let mut chain_override = None;
454 for arg in args.iter().skip(1) {
455 if arg.starts_with("--chain=") {
456 chain_override = Some(arg.trim_start_matches("--chain=").to_string());
457 }
458 }
459
460 let effective_chain = if let Some(chain) = chain_override {
462 chain
463 } else if !context.chain_explicit {
464 if let Some(inferred) = crate::chains::infer_chain_from_hash(&hash) {
466 if inferred != context.chain {
467 println!("Auto-detected chain: {}", inferred);
468 context.chain = inferred.to_string();
470 }
471 inferred.to_string()
472 } else {
473 context.chain.clone()
474 }
475 } else {
476 context.chain.clone()
477 };
478
479 let mut tx_args = TxArgs {
480 hash: hash.clone(),
481 chain: effective_chain,
482 format: Some(context.format),
483 trace: context.trace,
484 decode: context.decode,
485 };
486
487 for arg in args.iter().skip(1) {
489 if *arg == "--trace" {
490 tx_args.trace = true;
491 } else if *arg == "--decode" {
492 tx_args.decode = true;
493 }
494 }
495
496 context.last_tx = Some(hash);
498
499 tx::run(tx_args, config, clients).await?;
501 }
502
503 "crawl" | "token" => {
505 if args.is_empty() {
506 eprintln!(
507 "Usage: crawl <token_address> [--period <1h|24h|7d|30d>] [--report <path>]"
508 );
509 return Ok(false);
510 }
511
512 let token = args[0].to_string();
513
514 let mut chain_override = None;
516 let mut period = crawl::Period::Hour24;
517 let mut report_path = None;
518 let mut no_charts = false;
519
520 let mut i = 1;
521 while i < args.len() {
522 if args[i].starts_with("--chain=") {
523 chain_override = Some(args[i].trim_start_matches("--chain=").to_string());
524 } else if args[i] == "--chain" && i + 1 < args.len() {
525 chain_override = Some(args[i + 1].to_string());
526 i += 1;
527 } else if args[i].starts_with("--period=") {
528 let p = args[i].trim_start_matches("--period=");
529 period = match p {
530 "1h" => crawl::Period::Hour1,
531 "24h" => crawl::Period::Hour24,
532 "7d" => crawl::Period::Day7,
533 "30d" => crawl::Period::Day30,
534 _ => crawl::Period::Hour24,
535 };
536 } else if args[i] == "--period" && i + 1 < args.len() {
537 period = match args[i + 1] {
538 "1h" => crawl::Period::Hour1,
539 "24h" => crawl::Period::Hour24,
540 "7d" => crawl::Period::Day7,
541 "30d" => crawl::Period::Day30,
542 _ => crawl::Period::Hour24,
543 };
544 i += 1;
545 } else if args[i].starts_with("--report=") {
546 report_path = Some(std::path::PathBuf::from(
547 args[i].trim_start_matches("--report="),
548 ));
549 } else if args[i] == "--report" && i + 1 < args.len() {
550 report_path = Some(std::path::PathBuf::from(args[i + 1]));
551 i += 1;
552 } else if args[i] == "--no-charts" {
553 no_charts = true;
554 }
555 i += 1;
556 }
557
558 let effective_chain = if let Some(chain) = chain_override {
560 chain
561 } else if !context.chain_explicit {
562 if let Some(inferred) = crate::chains::infer_chain_from_address(&token) {
563 if inferred != context.chain {
564 println!("Auto-detected chain: {}", inferred);
565 context.chain = inferred.to_string();
566 }
567 inferred.to_string()
568 } else {
569 context.chain.clone()
570 }
571 } else {
572 context.chain.clone()
573 };
574
575 let crawl_args = CrawlArgs {
576 token,
577 chain: effective_chain,
578 period,
579 holders_limit: 10,
580 format: context.format,
581 no_charts,
582 report: report_path,
583 yes: false, save: false, };
586
587 crawl::run(crawl_args, config, clients).await?;
588 }
589
590 "portfolio" | "port" => {
592 let portfolio_input = args.join(" ");
594 execute_portfolio(&portfolio_input, context, config, clients).await?;
595 }
596
597 "tokens" | "aliases" => {
599 execute_tokens_command(args).await?;
600 }
601
602 "setup" | "config" => {
604 use super::setup::{SetupArgs, run as setup_run};
605 let setup_args = SetupArgs {
606 status: args.contains(&"--status") || args.contains(&"-s"),
607 key: args
608 .iter()
609 .find(|a| a.starts_with("--key="))
610 .map(|a| a.trim_start_matches("--key=").to_string())
611 .or_else(|| {
612 args.iter()
613 .position(|&a| a == "--key" || a == "-k")
614 .and_then(|i| args.get(i + 1).map(|s| s.to_string()))
615 }),
616 reset: args.contains(&"--reset"),
617 };
618 setup_run(setup_args, config).await?;
619 }
620
621 "monitor" | "mon" => {
623 let token = args.first().map(|s| s.to_string());
624 monitor::run(token, context, config, clients).await?;
625 }
626
627 _ => {
629 eprintln!(
630 "Unknown command: {}. Type 'help' for available commands.",
631 command
632 );
633 }
634 }
635
636 Ok(false)
637}
638
639async fn execute_tokens_command(args: &[&str]) -> Result<()> {
641 use crate::tokens::TokenAliases;
642
643 let mut aliases = TokenAliases::load();
644
645 if args.is_empty() {
646 let tokens = aliases.list();
648 if tokens.is_empty() {
649 println!("No saved token aliases.");
650 println!("Use 'crawl <token_name> --save' to save a token alias.");
651 return Ok(());
652 }
653
654 println!("\nSaved Token Aliases\n{}\n", "=".repeat(60));
655 println!("{:<10} {:<12} {:<20} Address", "Symbol", "Chain", "Name");
656 println!("{}", "-".repeat(80));
657
658 for token in tokens {
659 println!(
660 "{:<10} {:<12} {:<20} {}",
661 token.symbol, token.chain, token.name, token.address
662 );
663 }
664 println!();
665 return Ok(());
666 }
667
668 let subcommand = args[0].to_lowercase();
669 match subcommand.as_str() {
670 "list" | "ls" => {
671 let tokens = aliases.list();
672 if tokens.is_empty() {
673 println!("No saved token aliases.");
674 return Ok(());
675 }
676
677 println!("\nSaved Token Aliases\n{}\n", "=".repeat(60));
678 println!("{:<10} {:<12} {:<20} Address", "Symbol", "Chain", "Name");
679 println!("{}", "-".repeat(80));
680
681 for token in tokens {
682 println!(
683 "{:<10} {:<12} {:<20} {}",
684 token.symbol, token.chain, token.name, token.address
685 );
686 }
687 println!();
688 }
689
690 "recent" => {
691 let recent = aliases.recent();
692 if recent.is_empty() {
693 println!("No recently used tokens.");
694 return Ok(());
695 }
696
697 println!("\nRecently Used Tokens\n{}\n", "=".repeat(60));
698 println!("{:<10} {:<12} {:<20} Address", "Symbol", "Chain", "Name");
699 println!("{}", "-".repeat(80));
700
701 for token in recent {
702 println!(
703 "{:<10} {:<12} {:<20} {}",
704 token.symbol, token.chain, token.name, token.address
705 );
706 }
707 println!();
708 }
709
710 "remove" | "rm" | "delete" => {
711 if args.len() < 2 {
712 eprintln!("Usage: tokens remove <symbol> [--chain <chain>]");
713 return Ok(());
714 }
715
716 let symbol = args[1];
717 let chain = if args.len() > 3 && args[2] == "--chain" {
718 Some(args[3])
719 } else {
720 None
721 };
722
723 aliases.remove(symbol, chain);
724 if let Err(e) = aliases.save() {
725 eprintln!("Failed to save: {}", e);
726 } else {
727 println!("Removed alias: {}", symbol);
728 }
729 }
730
731 "add" => {
732 if args.len() < 4 {
733 eprintln!("Usage: tokens add <symbol> <chain> <address> [name]");
734 return Ok(());
735 }
736
737 let symbol = args[1];
738 let chain = args[2];
739 let address = args[3];
740 let name = if args.len() > 4 {
741 args[4..].join(" ")
742 } else {
743 symbol.to_string()
744 };
745
746 aliases.add(symbol, chain, address, &name);
747 if let Err(e) = aliases.save() {
748 eprintln!("Failed to save: {}", e);
749 } else {
750 println!("Added alias: {} -> {} on {}", symbol, address, chain);
751 }
752 }
753
754 _ => {
755 eprintln!("Unknown tokens subcommand: {}", subcommand);
756 eprintln!("Available: list, recent, add, remove");
757 }
758 }
759
760 Ok(())
761}
762
763async fn execute_portfolio(
765 input: &str,
766 context: &SessionContext,
767 config: &Config,
768 clients: &dyn ChainClientFactory,
769) -> Result<()> {
770 let parts: Vec<&str> = input.split_whitespace().collect();
771 if parts.is_empty() {
772 eprintln!("Portfolio subcommand required: add, remove, list, summary");
773 return Ok(());
774 }
775
776 use super::portfolio::{AddArgs, PortfolioCommands, RemoveArgs, SummaryArgs};
777
778 let subcommand = parts[0].to_lowercase();
779
780 let portfolio_args = match subcommand.as_str() {
781 "add" => {
782 if parts.len() < 2 {
783 eprintln!("Usage: portfolio add <address> [--label <label>] [--tags <tags>]");
784 return Ok(());
785 }
786 let address = parts[1].to_string();
787 let mut label = None;
788 let mut tags = Vec::new();
789
790 let mut i = 2;
791 while i < parts.len() {
792 if parts[i] == "--label" && i + 1 < parts.len() {
793 label = Some(parts[i + 1].to_string());
794 i += 2;
795 } else if parts[i] == "--tags" && i + 1 < parts.len() {
796 tags = parts[i + 1]
797 .split(',')
798 .map(|s| s.trim().to_string())
799 .collect();
800 i += 2;
801 } else {
802 i += 1;
803 }
804 }
805
806 PortfolioArgs {
807 command: PortfolioCommands::Add(AddArgs {
808 address,
809 label,
810 chain: context.chain.clone(),
811 tags,
812 }),
813 format: Some(context.format),
814 }
815 }
816 "remove" | "rm" => {
817 if parts.len() < 2 {
818 eprintln!("Usage: portfolio remove <address>");
819 return Ok(());
820 }
821 PortfolioArgs {
822 command: PortfolioCommands::Remove(RemoveArgs {
823 address: parts[1].to_string(),
824 }),
825 format: Some(context.format),
826 }
827 }
828 "list" | "ls" => PortfolioArgs {
829 command: PortfolioCommands::List,
830 format: Some(context.format),
831 },
832 "summary" => {
833 let mut chain = None;
834 let mut tag = None;
835 let mut include_tokens = context.include_tokens;
836
837 let mut i = 1;
838 while i < parts.len() {
839 if parts[i] == "--chain" && i + 1 < parts.len() {
840 chain = Some(parts[i + 1].to_string());
841 i += 2;
842 } else if parts[i] == "--tag" && i + 1 < parts.len() {
843 tag = Some(parts[i + 1].to_string());
844 i += 2;
845 } else if parts[i] == "--tokens" {
846 include_tokens = true;
847 i += 1;
848 } else {
849 i += 1;
850 }
851 }
852
853 PortfolioArgs {
854 command: PortfolioCommands::Summary(SummaryArgs {
855 chain,
856 tag,
857 include_tokens,
858 }),
859 format: Some(context.format),
860 }
861 }
862 _ => {
863 eprintln!(
864 "Unknown portfolio subcommand: {}. Use: add, remove, list, summary",
865 subcommand
866 );
867 return Ok(());
868 }
869 };
870
871 portfolio::run(portfolio_args, config, clients).await
872}
873
874fn print_help() {
876 println!(
877 r#"
878Scope Interactive Mode - Available Commands
879==========================================
880
881Navigation & Control:
882 help, ? Show this help message
883 exit, quit, q Exit interactive mode
884 ctx, context Show current session context
885 clear, reset Reset context to defaults
886
887Context Settings:
888 chain [name] Set or show current chain
889 Valid: ethereum, polygon, arbitrum, optimism, base, bsc, aegis, solana, tron
890 format [fmt] Set or show output format (table, json, csv)
891 limit [n] Set or show transaction limit
892 +tokens Toggle include_tokens flag for address analysis
893 +txs Toggle include_txs flag
894 trace Toggle trace flag
895 decode Toggle decode flag
896
897Analysis Commands:
898 address <addr> Analyze an address (uses current chain/format)
899 addr Shorthand for address
900 tx <hash> Analyze a transaction (uses current chain/format)
901 crawl <token> Crawl token analytics (holders, volume, price)
902 token Shorthand for crawl
903 monitor <token> Live-updating charts for a token (TUI mode)
904 mon Shorthand for monitor
905
906Token Search:
907 crawl USDC Search for token by name/symbol (interactive selection)
908 crawl 0x... Use address directly (no search)
909 tokens List saved token aliases
910 tokens recent Show recently used tokens
911 tokens add <sym> <chain> <addr> [name] Add a token alias
912 tokens remove <sym> [--chain <chain>] Remove a token alias
913
914Portfolio Commands:
915 portfolio add <addr> [--label <name>] [--tags <t1,t2>]
916 portfolio remove <addr>
917 portfolio list
918 portfolio summary [--chain <name>] [--tag <tag>] [--tokens]
919
920Configuration:
921 setup Run the setup wizard to configure API keys
922 setup --status Show current configuration status
923 setup --key <provider> Configure a specific API key
924 config Alias for setup
925
926Inline Overrides:
927 address 0x... --chain=polygon --tokens
928 tx 0x... --chain=arbitrum --trace --decode
929 crawl USDC --chain=ethereum --period=7d --report=report.md
930
931Live Monitor:
932 monitor USDC Start live monitoring with real-time charts
933 mon 0x... Monitor by address
934 Time periods: [1]=15m [2]=1h [3]=6h [4]=24h [T]=cycle
935 Chart modes: [C]=toggle between Line and Candlestick
936 Controls: [Q]uit [R]efresh [P]ause [+/-]speed [Esc]exit
937 Data is cached to temp folder and persists between sessions (24h retention)
938
939Tips:
940 - Search by token name: 'crawl WETH' or 'crawl "wrapped ether"'
941 - Save aliases for quick access: select a token and choose to save
942 - Context persists: set chain once, use it for multiple commands
943 - Use Ctrl+C to cancel, Ctrl+D to exit
944"#
945 );
946}
947
948#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[test]
957 fn test_session_context_default() {
958 let ctx = SessionContext::default();
959 assert_eq!(ctx.chain, "ethereum");
960 assert_eq!(ctx.format, OutputFormat::Table);
961 assert!(!ctx.include_tokens);
962 assert!(!ctx.include_txs);
963 assert!(!ctx.trace);
964 assert!(!ctx.decode);
965 assert_eq!(ctx.limit, 100);
966 assert!(ctx.last_address.is_none());
967 assert!(ctx.last_tx.is_none());
968 }
969
970 #[test]
971 fn test_session_context_display() {
972 let ctx = SessionContext::default();
973 let display = format!("{}", ctx);
974 assert!(display.contains("ethereum"));
975 assert!(display.contains("Table"));
976 }
977
978 #[test]
979 fn test_interactive_args_default() {
980 let args = InteractiveArgs { no_banner: false };
981 assert!(!args.no_banner);
982 }
983
984 #[test]
989 fn test_session_context_serialization() {
990 let ctx = SessionContext {
991 chain: "polygon".to_string(),
992 chain_explicit: true,
993 format: OutputFormat::Json,
994 last_address: Some("0xabc".to_string()),
995 last_tx: Some("0xdef".to_string()),
996 include_tokens: true,
997 include_txs: true,
998 trace: true,
999 decode: true,
1000 limit: 50,
1001 };
1002
1003 let yaml = serde_yaml::to_string(&ctx).unwrap();
1004 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
1005 assert_eq!(deserialized.chain, "polygon");
1006 assert!(deserialized.chain_explicit);
1007 assert_eq!(deserialized.format, OutputFormat::Json);
1008 assert_eq!(deserialized.last_address.as_deref(), Some("0xabc"));
1009 assert_eq!(deserialized.last_tx.as_deref(), Some("0xdef"));
1010 assert!(deserialized.include_tokens);
1011 assert!(deserialized.include_txs);
1012 assert!(deserialized.trace);
1013 assert!(deserialized.decode);
1014 assert_eq!(deserialized.limit, 50);
1015 }
1016
1017 #[test]
1018 fn test_session_context_display_with_address_and_tx() {
1019 let ctx = SessionContext {
1020 chain_explicit: true,
1021 last_address: Some("0x1234".to_string()),
1022 last_tx: Some("0xabcd".to_string()),
1023 ..Default::default()
1024 };
1025 let display = format!("{}", ctx);
1026 assert!(display.contains("0x1234"));
1027 assert!(display.contains("0xabcd"));
1028 assert!(!display.contains("(auto)"));
1030 }
1031
1032 #[test]
1033 fn test_session_context_display_auto_chain() {
1034 let ctx = SessionContext::default();
1035 let display = format!("{}", ctx);
1036 assert!(display.contains("(auto)"));
1037 }
1038
1039 fn test_config() -> Config {
1044 Config::default()
1045 }
1046
1047 fn test_factory() -> crate::chains::DefaultClientFactory {
1048 crate::chains::DefaultClientFactory {
1049 chains_config: crate::config::ChainsConfig::default(),
1050 }
1051 }
1052
1053 #[tokio::test]
1054 async fn test_exit_commands() {
1055 let config = test_config();
1056 for cmd in &["exit", "quit", "q", ".exit", ".quit"] {
1057 let mut ctx = SessionContext::default();
1058 let result = execute_input(cmd, &mut ctx, &config, &test_factory())
1059 .await
1060 .unwrap();
1061 assert!(result, "'{cmd}' should return true (exit)");
1062 }
1063 }
1064
1065 #[tokio::test]
1066 async fn test_help_command() {
1067 let config = test_config();
1068 let mut ctx = SessionContext::default();
1069 let result = execute_input("help", &mut ctx, &config, &test_factory())
1070 .await
1071 .unwrap();
1072 assert!(!result);
1073 }
1074
1075 #[tokio::test]
1076 async fn test_context_command() {
1077 let config = test_config();
1078 let mut ctx = SessionContext::default();
1079 let result = execute_input("ctx", &mut ctx, &config, &test_factory())
1080 .await
1081 .unwrap();
1082 assert!(!result);
1083 }
1084
1085 #[tokio::test]
1086 async fn test_clear_command() {
1087 let config = test_config();
1088 let mut ctx = SessionContext {
1089 chain: "polygon".to_string(),
1090 include_tokens: true,
1091 limit: 42,
1092 ..Default::default()
1093 };
1094
1095 let result = execute_input("clear", &mut ctx, &config, &test_factory())
1096 .await
1097 .unwrap();
1098 assert!(!result);
1099 assert_eq!(ctx.chain, "ethereum");
1100 assert!(!ctx.include_tokens);
1101 assert_eq!(ctx.limit, 100);
1102 }
1103
1104 #[tokio::test]
1105 async fn test_chain_set_valid() {
1106 let config = test_config();
1107 let mut ctx = SessionContext::default();
1108
1109 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1110 .await
1111 .unwrap();
1112 assert_eq!(ctx.chain, "polygon");
1113 assert!(ctx.chain_explicit);
1114 }
1115
1116 #[tokio::test]
1117 async fn test_chain_set_solana() {
1118 let config = test_config();
1119 let mut ctx = SessionContext::default();
1120
1121 execute_input("chain solana", &mut ctx, &config, &test_factory())
1122 .await
1123 .unwrap();
1124 assert_eq!(ctx.chain, "solana");
1125 assert!(ctx.chain_explicit);
1126 }
1127
1128 #[tokio::test]
1129 async fn test_chain_auto() {
1130 let config = test_config();
1131 let mut ctx = SessionContext {
1132 chain: "polygon".to_string(),
1133 chain_explicit: true,
1134 ..Default::default()
1135 };
1136
1137 execute_input("chain auto", &mut ctx, &config, &test_factory())
1138 .await
1139 .unwrap();
1140 assert_eq!(ctx.chain, "ethereum");
1141 assert!(!ctx.chain_explicit);
1142 }
1143
1144 #[tokio::test]
1145 async fn test_chain_invalid() {
1146 let config = test_config();
1147 let mut ctx = SessionContext::default();
1148 execute_input("chain foobar", &mut ctx, &config, &test_factory())
1150 .await
1151 .unwrap();
1152 assert_eq!(ctx.chain, "ethereum");
1153 assert!(!ctx.chain_explicit);
1154 }
1155
1156 #[tokio::test]
1157 async fn test_chain_show() {
1158 let config = test_config();
1159 let mut ctx = SessionContext::default();
1160 let result = execute_input("chain", &mut ctx, &config, &test_factory())
1162 .await
1163 .unwrap();
1164 assert!(!result);
1165 assert_eq!(ctx.chain, "ethereum");
1166 }
1167
1168 #[tokio::test]
1169 async fn test_format_set_json() {
1170 let config = test_config();
1171 let mut ctx = SessionContext::default();
1172 execute_input("format json", &mut ctx, &config, &test_factory())
1173 .await
1174 .unwrap();
1175 assert_eq!(ctx.format, OutputFormat::Json);
1176 }
1177
1178 #[tokio::test]
1179 async fn test_format_set_csv() {
1180 let config = test_config();
1181 let mut ctx = SessionContext::default();
1182 execute_input("format csv", &mut ctx, &config, &test_factory())
1183 .await
1184 .unwrap();
1185 assert_eq!(ctx.format, OutputFormat::Csv);
1186 }
1187
1188 #[tokio::test]
1189 async fn test_format_set_table() {
1190 let config = test_config();
1191 let mut ctx = SessionContext {
1192 format: OutputFormat::Json,
1193 ..Default::default()
1194 };
1195 execute_input("format table", &mut ctx, &config, &test_factory())
1196 .await
1197 .unwrap();
1198 assert_eq!(ctx.format, OutputFormat::Table);
1199 }
1200
1201 #[tokio::test]
1202 async fn test_format_invalid() {
1203 let config = test_config();
1204 let mut ctx = SessionContext::default();
1205 execute_input("format xml", &mut ctx, &config, &test_factory())
1206 .await
1207 .unwrap();
1208 assert_eq!(ctx.format, OutputFormat::Table);
1210 }
1211
1212 #[tokio::test]
1213 async fn test_format_show() {
1214 let config = test_config();
1215 let mut ctx = SessionContext::default();
1216 let result = execute_input("format", &mut ctx, &config, &test_factory())
1217 .await
1218 .unwrap();
1219 assert!(!result);
1220 }
1221
1222 #[tokio::test]
1223 async fn test_toggle_tokens() {
1224 let config = test_config();
1225 let mut ctx = SessionContext::default();
1226 assert!(!ctx.include_tokens);
1227
1228 execute_input("+tokens", &mut ctx, &config, &test_factory())
1229 .await
1230 .unwrap();
1231 assert!(ctx.include_tokens);
1232
1233 execute_input("+tokens", &mut ctx, &config, &test_factory())
1234 .await
1235 .unwrap();
1236 assert!(!ctx.include_tokens);
1237 }
1238
1239 #[tokio::test]
1240 async fn test_toggle_txs() {
1241 let config = test_config();
1242 let mut ctx = SessionContext::default();
1243 assert!(!ctx.include_txs);
1244
1245 execute_input("+txs", &mut ctx, &config, &test_factory())
1246 .await
1247 .unwrap();
1248 assert!(ctx.include_txs);
1249
1250 execute_input("+txs", &mut ctx, &config, &test_factory())
1251 .await
1252 .unwrap();
1253 assert!(!ctx.include_txs);
1254 }
1255
1256 #[tokio::test]
1257 async fn test_toggle_trace() {
1258 let config = test_config();
1259 let mut ctx = SessionContext::default();
1260 assert!(!ctx.trace);
1261
1262 execute_input("trace", &mut ctx, &config, &test_factory())
1263 .await
1264 .unwrap();
1265 assert!(ctx.trace);
1266
1267 execute_input("trace", &mut ctx, &config, &test_factory())
1268 .await
1269 .unwrap();
1270 assert!(!ctx.trace);
1271 }
1272
1273 #[tokio::test]
1274 async fn test_toggle_decode() {
1275 let config = test_config();
1276 let mut ctx = SessionContext::default();
1277 assert!(!ctx.decode);
1278
1279 execute_input("decode", &mut ctx, &config, &test_factory())
1280 .await
1281 .unwrap();
1282 assert!(ctx.decode);
1283
1284 execute_input("decode", &mut ctx, &config, &test_factory())
1285 .await
1286 .unwrap();
1287 assert!(!ctx.decode);
1288 }
1289
1290 #[tokio::test]
1291 async fn test_limit_set_valid() {
1292 let config = test_config();
1293 let mut ctx = SessionContext::default();
1294 execute_input("limit 50", &mut ctx, &config, &test_factory())
1295 .await
1296 .unwrap();
1297 assert_eq!(ctx.limit, 50);
1298 }
1299
1300 #[tokio::test]
1301 async fn test_limit_set_invalid() {
1302 let config = test_config();
1303 let mut ctx = SessionContext::default();
1304 execute_input("limit abc", &mut ctx, &config, &test_factory())
1305 .await
1306 .unwrap();
1307 assert_eq!(ctx.limit, 100);
1309 }
1310
1311 #[tokio::test]
1312 async fn test_limit_show() {
1313 let config = test_config();
1314 let mut ctx = SessionContext::default();
1315 let result = execute_input("limit", &mut ctx, &config, &test_factory())
1316 .await
1317 .unwrap();
1318 assert!(!result);
1319 }
1320
1321 #[tokio::test]
1322 async fn test_unknown_command() {
1323 let config = test_config();
1324 let mut ctx = SessionContext::default();
1325 let result = execute_input("foobar", &mut ctx, &config, &test_factory())
1326 .await
1327 .unwrap();
1328 assert!(!result);
1329 }
1330
1331 #[tokio::test]
1332 async fn test_empty_input() {
1333 let config = test_config();
1334 let mut ctx = SessionContext::default();
1335 let result = execute_input("", &mut ctx, &config, &test_factory())
1336 .await
1337 .unwrap();
1338 assert!(!result);
1339 }
1340
1341 #[tokio::test]
1342 async fn test_address_no_arg_no_last() {
1343 let config = test_config();
1344 let mut ctx = SessionContext::default();
1345 let result = execute_input("address", &mut ctx, &config, &test_factory())
1347 .await
1348 .unwrap();
1349 assert!(!result);
1350 }
1351
1352 #[tokio::test]
1353 async fn test_tx_no_arg_no_last() {
1354 let config = test_config();
1355 let mut ctx = SessionContext::default();
1356 let result = execute_input("tx", &mut ctx, &config, &test_factory())
1358 .await
1359 .unwrap();
1360 assert!(!result);
1361 }
1362
1363 #[tokio::test]
1364 async fn test_crawl_no_arg() {
1365 let config = test_config();
1366 let mut ctx = SessionContext::default();
1367 let result = execute_input("crawl", &mut ctx, &config, &test_factory())
1369 .await
1370 .unwrap();
1371 assert!(!result);
1372 }
1373
1374 #[tokio::test]
1375 async fn test_multiple_context_commands() {
1376 let config = test_config();
1377 let mut ctx = SessionContext::default();
1378
1379 execute_input("chain polygon", &mut ctx, &config, &test_factory())
1381 .await
1382 .unwrap();
1383 execute_input("format json", &mut ctx, &config, &test_factory())
1384 .await
1385 .unwrap();
1386 execute_input("+tokens", &mut ctx, &config, &test_factory())
1387 .await
1388 .unwrap();
1389 execute_input("trace", &mut ctx, &config, &test_factory())
1390 .await
1391 .unwrap();
1392 execute_input("limit 25", &mut ctx, &config, &test_factory())
1393 .await
1394 .unwrap();
1395
1396 assert_eq!(ctx.chain, "polygon");
1397 assert_eq!(ctx.format, OutputFormat::Json);
1398 assert!(ctx.include_tokens);
1399 assert!(ctx.trace);
1400 assert_eq!(ctx.limit, 25);
1401
1402 execute_input("clear", &mut ctx, &config, &test_factory())
1404 .await
1405 .unwrap();
1406 assert_eq!(ctx.chain, "ethereum");
1407 assert!(!ctx.include_tokens);
1408 assert!(!ctx.trace);
1409 assert_eq!(ctx.limit, 100);
1410 }
1411
1412 #[tokio::test]
1413 async fn test_dot_prefix_commands() {
1414 let config = test_config();
1415 let mut ctx = SessionContext::default();
1416
1417 let result = execute_input(".help", &mut ctx, &config, &test_factory())
1419 .await
1420 .unwrap();
1421 assert!(!result);
1422
1423 execute_input(".chain polygon", &mut ctx, &config, &test_factory())
1424 .await
1425 .unwrap();
1426 assert_eq!(ctx.chain, "polygon");
1427
1428 execute_input(".format json", &mut ctx, &config, &test_factory())
1429 .await
1430 .unwrap();
1431 assert_eq!(ctx.format, OutputFormat::Json);
1432
1433 execute_input(".trace", &mut ctx, &config, &test_factory())
1434 .await
1435 .unwrap();
1436 assert!(ctx.trace);
1437
1438 execute_input(".decode", &mut ctx, &config, &test_factory())
1439 .await
1440 .unwrap();
1441 assert!(ctx.decode);
1442 }
1443
1444 #[tokio::test]
1445 async fn test_all_valid_chains() {
1446 let config = test_config();
1447 let valid_chains = [
1448 "ethereum", "polygon", "arbitrum", "optimism", "base", "bsc", "aegis", "solana", "tron",
1449 ];
1450 for chain in valid_chains {
1451 let mut ctx = SessionContext::default();
1452 execute_input(
1453 &format!("chain {}", chain),
1454 &mut ctx,
1455 &config,
1456 &test_factory(),
1457 )
1458 .await
1459 .unwrap();
1460 assert_eq!(ctx.chain, chain);
1461 assert!(ctx.chain_explicit);
1462 }
1463 }
1464
1465 use crate::chains::mocks::MockClientFactory;
1470
1471 fn mock_factory() -> MockClientFactory {
1472 let mut factory = MockClientFactory::new();
1473 factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
1474 factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
1475 token: crate::chains::Token {
1476 contract_address: "0xtoken".to_string(),
1477 symbol: "TEST".to_string(),
1478 name: "Test Token".to_string(),
1479 decimals: 18,
1480 },
1481 balance: "1000".to_string(),
1482 formatted_balance: "0.001".to_string(),
1483 usd_value: None,
1484 }];
1485 factory
1486 }
1487
1488 #[tokio::test]
1489 async fn test_address_command_with_args() {
1490 let config = test_config();
1491 let factory = mock_factory();
1492 let mut ctx = SessionContext::default();
1493 let result = execute_input(
1494 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1495 &mut ctx,
1496 &config,
1497 &factory,
1498 )
1499 .await;
1500 assert!(result.is_ok());
1501 assert!(!result.unwrap());
1502 assert_eq!(
1503 ctx.last_address,
1504 Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string())
1505 );
1506 }
1507
1508 #[tokio::test]
1509 async fn test_address_command_with_chain_override() {
1510 let config = test_config();
1511 let factory = mock_factory();
1512 let mut ctx = SessionContext::default();
1513 let result = execute_input(
1514 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain=polygon",
1515 &mut ctx,
1516 &config,
1517 &factory,
1518 )
1519 .await;
1520 assert!(result.is_ok());
1521 }
1522
1523 #[tokio::test]
1524 async fn test_address_command_with_tokens_flag() {
1525 let config = test_config();
1526 let factory = mock_factory();
1527 let mut ctx = SessionContext::default();
1528 let result = execute_input(
1529 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --tokens",
1530 &mut ctx,
1531 &config,
1532 &factory,
1533 )
1534 .await;
1535 assert!(result.is_ok());
1536 }
1537
1538 #[tokio::test]
1539 async fn test_address_command_with_txs_flag() {
1540 let config = test_config();
1541 let factory = mock_factory();
1542 let mut ctx = SessionContext::default();
1543 let result = execute_input(
1544 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --txs",
1545 &mut ctx,
1546 &config,
1547 &factory,
1548 )
1549 .await;
1550 assert!(result.is_ok());
1551 }
1552
1553 #[tokio::test]
1554 async fn test_address_reuses_last_address() {
1555 let config = test_config();
1556 let factory = mock_factory();
1557 let mut ctx = SessionContext {
1558 last_address: Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string()),
1559 ..Default::default()
1560 };
1561 let result = execute_input("address", &mut ctx, &config, &factory).await;
1562 assert!(result.is_ok());
1563 }
1564
1565 #[tokio::test]
1566 async fn test_address_auto_detects_solana() {
1567 let config = test_config();
1568 let factory = mock_factory();
1569 let mut ctx = SessionContext::default();
1570 let result = execute_input(
1572 "address DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy",
1573 &mut ctx,
1574 &config,
1575 &factory,
1576 )
1577 .await;
1578 assert!(result.is_ok());
1579 assert_eq!(ctx.chain, "solana");
1581 }
1582
1583 #[tokio::test]
1584 async fn test_tx_command_with_args() {
1585 let config = test_config();
1586 let factory = mock_factory();
1587 let mut ctx = SessionContext::default();
1588 let result = execute_input(
1589 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd",
1590 &mut ctx,
1591 &config,
1592 &factory,
1593 )
1594 .await;
1595 assert!(result.is_ok());
1596 assert_eq!(
1597 ctx.last_tx,
1598 Some("0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string())
1599 );
1600 }
1601
1602 #[tokio::test]
1603 async fn test_tx_command_with_trace_decode() {
1604 let config = test_config();
1605 let factory = mock_factory();
1606 let mut ctx = SessionContext::default();
1607 let result = execute_input(
1608 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --trace --decode",
1609 &mut ctx,
1610 &config,
1611 &factory,
1612 )
1613 .await;
1614 assert!(result.is_ok());
1615 }
1616
1617 #[tokio::test]
1618 async fn test_tx_command_with_chain_override() {
1619 let config = test_config();
1620 let factory = mock_factory();
1621 let mut ctx = SessionContext::default();
1622 let result = execute_input(
1623 "tx 0xabc123def456789012345678901234567890123456789012345678901234abcd --chain=polygon",
1624 &mut ctx,
1625 &config,
1626 &factory,
1627 )
1628 .await;
1629 assert!(result.is_ok());
1630 }
1631
1632 #[tokio::test]
1633 async fn test_tx_reuses_last_tx() {
1634 let config = test_config();
1635 let factory = mock_factory();
1636 let mut ctx = SessionContext {
1637 last_tx: Some(
1638 "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
1639 ),
1640 ..Default::default()
1641 };
1642 let result = execute_input("tx", &mut ctx, &config, &factory).await;
1643 assert!(result.is_ok());
1644 }
1645
1646 #[tokio::test]
1647 async fn test_tx_auto_detects_tron() {
1648 let config = test_config();
1649 let factory = mock_factory();
1650 let mut ctx = SessionContext::default();
1651 let result = execute_input(
1652 "tx abc123def456789012345678901234567890123456789012345678901234abcd",
1653 &mut ctx,
1654 &config,
1655 &factory,
1656 )
1657 .await;
1658 assert!(result.is_ok());
1659 assert_eq!(ctx.chain, "tron");
1660 }
1661
1662 #[tokio::test]
1663 async fn test_crawl_command_with_args() {
1664 let config = test_config();
1665 let factory = mock_factory();
1666 let mut ctx = SessionContext::default();
1667 let result = execute_input(
1668 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
1669 &mut ctx,
1670 &config,
1671 &factory,
1672 )
1673 .await;
1674 assert!(result.is_ok());
1675 }
1676
1677 #[tokio::test]
1678 async fn test_crawl_command_with_period() {
1679 let config = test_config();
1680 let factory = mock_factory();
1681 let mut ctx = SessionContext::default();
1682 let result = execute_input(
1683 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d --no-charts",
1684 &mut ctx,
1685 &config,
1686 &factory,
1687 )
1688 .await;
1689 assert!(result.is_ok());
1690 }
1691
1692 #[tokio::test]
1693 async fn test_crawl_command_with_chain_flag() {
1694 let config = test_config();
1695 let factory = mock_factory();
1696 let mut ctx = SessionContext::default();
1697 let result = execute_input(
1698 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --chain polygon --no-charts",
1699 &mut ctx,
1700 &config,
1701 &factory,
1702 )
1703 .await;
1704 assert!(result.is_ok());
1705 }
1706
1707 #[tokio::test]
1708 async fn test_crawl_command_with_period_flag() {
1709 let config = test_config();
1710 let factory = mock_factory();
1711 let mut ctx = SessionContext::default();
1712 let result = execute_input(
1713 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h --no-charts",
1714 &mut ctx,
1715 &config,
1716 &factory,
1717 )
1718 .await;
1719 assert!(result.is_ok());
1720 }
1721
1722 #[tokio::test]
1723 async fn test_crawl_command_with_report() {
1724 let config = test_config();
1725 let factory = mock_factory();
1726 let mut ctx = SessionContext::default();
1727 let tmp = tempfile::NamedTempFile::new().unwrap();
1728 let result = execute_input(
1729 &format!(
1730 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report {} --no-charts",
1731 tmp.path().display()
1732 ),
1733 &mut ctx,
1734 &config,
1735 &factory,
1736 )
1737 .await;
1738 assert!(result.is_ok());
1739 }
1740
1741 #[tokio::test]
1742 async fn test_portfolio_list_command() {
1743 let tmp_dir = tempfile::tempdir().unwrap();
1744 let config = Config {
1745 portfolio: crate::config::PortfolioConfig {
1746 data_dir: Some(tmp_dir.path().to_path_buf()),
1747 },
1748 ..Default::default()
1749 };
1750 let factory = mock_factory();
1751 let mut ctx = SessionContext::default();
1752 let result = execute_input("portfolio list", &mut ctx, &config, &factory).await;
1753 assert!(result.is_ok());
1754 }
1755
1756 #[tokio::test]
1757 async fn test_portfolio_add_command() {
1758 let tmp_dir = tempfile::tempdir().unwrap();
1759 let config = Config {
1760 portfolio: crate::config::PortfolioConfig {
1761 data_dir: Some(tmp_dir.path().to_path_buf()),
1762 },
1763 ..Default::default()
1764 };
1765 let factory = mock_factory();
1766 let mut ctx = SessionContext::default();
1767 let result = execute_input(
1768 "portfolio add 0xtest --label mytest",
1769 &mut ctx,
1770 &config,
1771 &factory,
1772 )
1773 .await;
1774 assert!(result.is_ok());
1775 }
1776
1777 #[tokio::test]
1778 async fn test_portfolio_summary_command() {
1779 let tmp_dir = tempfile::tempdir().unwrap();
1780 let config = Config {
1781 portfolio: crate::config::PortfolioConfig {
1782 data_dir: Some(tmp_dir.path().to_path_buf()),
1783 },
1784 ..Default::default()
1785 };
1786 let factory = mock_factory();
1787 let mut ctx = SessionContext::default();
1788 execute_input("portfolio add 0xtest", &mut ctx, &config, &factory)
1790 .await
1791 .unwrap();
1792 let result = execute_input("portfolio summary", &mut ctx, &config, &factory).await;
1794 assert!(result.is_ok());
1795 }
1796
1797 #[tokio::test]
1798 async fn test_portfolio_remove_command() {
1799 let tmp_dir = tempfile::tempdir().unwrap();
1800 let config = Config {
1801 portfolio: crate::config::PortfolioConfig {
1802 data_dir: Some(tmp_dir.path().to_path_buf()),
1803 },
1804 ..Default::default()
1805 };
1806 let factory = mock_factory();
1807 let mut ctx = SessionContext::default();
1808 let result = execute_input("portfolio remove 0xtest", &mut ctx, &config, &factory).await;
1809 assert!(result.is_ok());
1810 }
1811
1812 #[tokio::test]
1813 async fn test_portfolio_no_subcommand() {
1814 let config = test_config();
1815 let factory = mock_factory();
1816 let mut ctx = SessionContext::default();
1817 let result = execute_input("portfolio", &mut ctx, &config, &factory).await;
1818 assert!(result.is_ok());
1819 }
1820
1821 #[tokio::test]
1822 async fn test_portfolio_unknown_subcommand() {
1823 let tmp_dir = tempfile::tempdir().unwrap();
1824 let config = Config {
1825 portfolio: crate::config::PortfolioConfig {
1826 data_dir: Some(tmp_dir.path().to_path_buf()),
1827 },
1828 ..Default::default()
1829 };
1830 let factory = mock_factory();
1831 let mut ctx = SessionContext::default();
1832 let result = execute_input("portfolio foobar", &mut ctx, &config, &factory).await;
1833 assert!(result.is_ok());
1834 }
1835
1836 #[tokio::test]
1837 async fn test_tokens_command_list() {
1838 let config = test_config();
1839 let factory = mock_factory();
1840 let mut ctx = SessionContext::default();
1841 let result = execute_input("tokens list", &mut ctx, &config, &factory).await;
1842 assert!(result.is_ok());
1843 }
1844
1845 #[tokio::test]
1846 async fn test_tokens_command_no_args() {
1847 let config = test_config();
1848 let factory = mock_factory();
1849 let mut ctx = SessionContext::default();
1850 let result = execute_input("tokens", &mut ctx, &config, &factory).await;
1851 assert!(result.is_ok());
1852 }
1853
1854 #[tokio::test]
1855 async fn test_tokens_command_recent() {
1856 let config = test_config();
1857 let factory = mock_factory();
1858 let mut ctx = SessionContext::default();
1859 let result = execute_input("tokens recent", &mut ctx, &config, &factory).await;
1860 assert!(result.is_ok());
1861 }
1862
1863 #[tokio::test]
1864 async fn test_tokens_command_remove_no_args() {
1865 let config = test_config();
1866 let factory = mock_factory();
1867 let mut ctx = SessionContext::default();
1868 let result = execute_input("tokens remove", &mut ctx, &config, &factory).await;
1869 assert!(result.is_ok());
1870 }
1871
1872 #[tokio::test]
1873 async fn test_tokens_command_add_no_args() {
1874 let config = test_config();
1875 let factory = mock_factory();
1876 let mut ctx = SessionContext::default();
1877 let result = execute_input("tokens add", &mut ctx, &config, &factory).await;
1878 assert!(result.is_ok());
1879 }
1880
1881 #[tokio::test]
1882 async fn test_tokens_command_unknown() {
1883 let config = test_config();
1884 let factory = mock_factory();
1885 let mut ctx = SessionContext::default();
1886 let result = execute_input("tokens foobar", &mut ctx, &config, &factory).await;
1887 assert!(result.is_ok());
1888 }
1889
1890 #[tokio::test]
1891 async fn test_setup_command_status() {
1892 let config = test_config();
1893 let factory = mock_factory();
1894 let mut ctx = SessionContext::default();
1895 let result = execute_input("setup --status", &mut ctx, &config, &factory).await;
1896 assert!(result.is_ok());
1897 }
1898
1899 #[tokio::test]
1900 async fn test_transaction_alias() {
1901 let config = test_config();
1902 let factory = mock_factory();
1903 let mut ctx = SessionContext::default();
1904 let result = execute_input(
1905 "transaction 0xabc123def456789012345678901234567890123456789012345678901234abcd",
1906 &mut ctx,
1907 &config,
1908 &factory,
1909 )
1910 .await;
1911 assert!(result.is_ok());
1912 }
1913
1914 #[tokio::test]
1915 async fn test_token_alias_for_crawl() {
1916 let config = test_config();
1917 let factory = mock_factory();
1918 let mut ctx = SessionContext::default();
1919 let result = execute_input(
1920 "token 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
1921 &mut ctx,
1922 &config,
1923 &factory,
1924 )
1925 .await;
1926 assert!(result.is_ok());
1927 }
1928
1929 #[tokio::test]
1930 async fn test_port_alias_for_portfolio() {
1931 let tmp_dir = tempfile::tempdir().unwrap();
1932 let config = Config {
1933 portfolio: crate::config::PortfolioConfig {
1934 data_dir: Some(tmp_dir.path().to_path_buf()),
1935 },
1936 ..Default::default()
1937 };
1938 let factory = mock_factory();
1939 let mut ctx = SessionContext::default();
1940 let result = execute_input("port list", &mut ctx, &config, &factory).await;
1941 assert!(result.is_ok());
1942 }
1943
1944 #[tokio::test]
1949 async fn test_execute_tokens_list_empty() {
1950 let result = execute_tokens_command(&[]).await;
1951 assert!(result.is_ok());
1952 }
1953
1954 #[tokio::test]
1955 async fn test_execute_tokens_list_subcommand() {
1956 let result = execute_tokens_command(&["list"]).await;
1957 assert!(result.is_ok());
1958 }
1959
1960 #[tokio::test]
1961 async fn test_execute_tokens_recent() {
1962 let result = execute_tokens_command(&["recent"]).await;
1963 assert!(result.is_ok());
1964 }
1965
1966 #[tokio::test]
1967 async fn test_execute_tokens_add_insufficient_args() {
1968 let result = execute_tokens_command(&["add"]).await;
1969 assert!(result.is_ok());
1970 }
1971
1972 #[tokio::test]
1973 async fn test_execute_tokens_add_success() {
1974 let result = execute_tokens_command(&[
1975 "add",
1976 "TEST_INTERACTIVE",
1977 "ethereum",
1978 "0xtest123456789",
1979 "Test Token",
1980 ])
1981 .await;
1982 assert!(result.is_ok());
1983 let _ = execute_tokens_command(&["remove", "TEST_INTERACTIVE"]).await;
1984 }
1985
1986 #[tokio::test]
1987 async fn test_execute_tokens_remove_no_args() {
1988 let result = execute_tokens_command(&["remove"]).await;
1989 assert!(result.is_ok());
1990 }
1991
1992 #[tokio::test]
1993 async fn test_execute_tokens_remove_with_symbol() {
1994 let _ =
1995 execute_tokens_command(&["add", "RMTEST", "ethereum", "0xrmtest", "Remove Test"]).await;
1996 let result = execute_tokens_command(&["remove", "RMTEST"]).await;
1997 assert!(result.is_ok());
1998 }
1999
2000 #[tokio::test]
2001 async fn test_execute_tokens_unknown_subcommand() {
2002 let result = execute_tokens_command(&["invalid"]).await;
2003 assert!(result.is_ok());
2004 }
2005
2006 #[test]
2011 fn test_session_context_serialization_roundtrip() {
2012 let ctx = SessionContext {
2013 chain: "solana".to_string(),
2014 include_tokens: true,
2015 limit: 25,
2016 last_address: Some("0xtest".to_string()),
2017 ..Default::default()
2018 };
2019
2020 let yaml = serde_yaml::to_string(&ctx).unwrap();
2021 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
2022 assert_eq!(deserialized.chain, "solana");
2023 assert!(deserialized.include_tokens);
2024 assert_eq!(deserialized.limit, 25);
2025 assert_eq!(deserialized.last_address, Some("0xtest".to_string()));
2026 }
2027
2028 #[tokio::test]
2033 async fn test_chain_show_explicit() {
2034 let config = test_config();
2035 let factory = test_factory();
2036 let mut context = SessionContext {
2037 chain: "polygon".to_string(),
2038 chain_explicit: true,
2039 ..Default::default()
2040 };
2041
2042 let result = execute_input("chain", &mut context, &config, &factory).await;
2044 assert!(result.is_ok());
2045 assert!(!result.unwrap()); }
2047
2048 #[tokio::test]
2049 async fn test_address_with_explicit_chain() {
2050 let config = test_config();
2051 let factory = test_factory();
2052 let mut context = SessionContext {
2053 chain: "polygon".to_string(),
2054 chain_explicit: true,
2055 ..Default::default()
2056 };
2057
2058 let result = execute_input(
2060 "address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2061 &mut context,
2062 &config,
2063 &factory,
2064 )
2065 .await;
2066 assert!(result.is_ok() || result.is_err());
2068 }
2069
2070 #[tokio::test]
2071 async fn test_tx_with_explicit_chain() {
2072 let config = test_config();
2073 let factory = test_factory();
2074 let mut context = SessionContext {
2075 chain: "polygon".to_string(),
2076 chain_explicit: true,
2077 ..Default::default()
2078 };
2079
2080 let result = execute_input("tx 0xabc123def456789", &mut context, &config, &factory).await;
2082 assert!(result.is_ok() || result.is_err());
2083 }
2084
2085 #[tokio::test]
2086 async fn test_crawl_with_period_eq_flag() {
2087 let config = test_config();
2088 let factory = test_factory();
2089 let mut context = SessionContext::default();
2090
2091 let result = execute_input(
2093 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period=7d",
2094 &mut context,
2095 &config,
2096 &factory,
2097 )
2098 .await;
2099 assert!(result.is_ok() || result.is_err());
2101 }
2102
2103 #[tokio::test]
2104 async fn test_crawl_with_period_space_flag() {
2105 let config = test_config();
2106 let factory = test_factory();
2107 let mut context = SessionContext::default();
2108
2109 let result = execute_input(
2111 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --period 1h",
2112 &mut context,
2113 &config,
2114 &factory,
2115 )
2116 .await;
2117 assert!(result.is_ok() || result.is_err());
2118 }
2119
2120 #[tokio::test]
2121 async fn test_crawl_with_chain_eq_flag() {
2122 let config = test_config();
2123 let factory = test_factory();
2124 let mut context = SessionContext::default();
2125
2126 let result = execute_input(
2128 "crawl 0xAddress --chain=polygon",
2129 &mut context,
2130 &config,
2131 &factory,
2132 )
2133 .await;
2134 assert!(result.is_ok() || result.is_err());
2135 }
2136
2137 #[tokio::test]
2138 async fn test_crawl_with_chain_space_flag() {
2139 let config = test_config();
2140 let factory = test_factory();
2141 let mut context = SessionContext::default();
2142
2143 let result = execute_input(
2145 "crawl 0xAddress --chain polygon",
2146 &mut context,
2147 &config,
2148 &factory,
2149 )
2150 .await;
2151 assert!(result.is_ok() || result.is_err());
2152 }
2153
2154 #[tokio::test]
2155 async fn test_crawl_with_report_flag() {
2156 let config = test_config();
2157 let factory = test_factory();
2158 let mut context = SessionContext::default();
2159
2160 let tmp = tempfile::NamedTempFile::new().unwrap();
2161 let path = tmp.path().to_string_lossy();
2162 let input = format!(
2163 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --report={}",
2164 path
2165 );
2166 let result = execute_input(&input, &mut context, &config, &factory).await;
2167 assert!(result.is_ok() || result.is_err());
2168 }
2169
2170 #[tokio::test]
2171 async fn test_crawl_with_no_charts_flag() {
2172 let config = test_config();
2173 let factory = test_factory();
2174 let mut context = SessionContext::default();
2175
2176 let result = execute_input(
2177 "crawl 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --no-charts",
2178 &mut context,
2179 &config,
2180 &factory,
2181 )
2182 .await;
2183 assert!(result.is_ok() || result.is_err());
2184 }
2185
2186 #[tokio::test]
2187 async fn test_crawl_with_explicit_chain() {
2188 let config = test_config();
2189 let factory = test_factory();
2190 let mut context = SessionContext {
2191 chain_explicit: true,
2192 chain: "arbitrum".to_string(),
2193 ..Default::default()
2194 };
2195
2196 let result = execute_input("crawl 0xAddress", &mut context, &config, &factory).await;
2197 assert!(result.is_ok() || result.is_err());
2198 }
2199
2200 #[tokio::test]
2201 async fn test_portfolio_add_with_label_and_tags() {
2202 let tmp_dir = tempfile::tempdir().unwrap();
2203 let config = Config {
2204 portfolio: crate::config::PortfolioConfig {
2205 data_dir: Some(tmp_dir.path().to_path_buf()),
2206 },
2207 ..Default::default()
2208 };
2209 let factory = mock_factory();
2210 let mut context = SessionContext::default();
2211
2212 let result = execute_input(
2213 "portfolio add 0xAbC123 --label MyWallet --tags defi,staking",
2214 &mut context,
2215 &config,
2216 &factory,
2217 )
2218 .await;
2219 assert!(result.is_ok());
2220 }
2221
2222 #[tokio::test]
2223 async fn test_portfolio_remove_no_args() {
2224 let tmp_dir = tempfile::tempdir().unwrap();
2225 let config = Config {
2226 portfolio: crate::config::PortfolioConfig {
2227 data_dir: Some(tmp_dir.path().to_path_buf()),
2228 },
2229 ..Default::default()
2230 };
2231 let factory = mock_factory();
2232 let mut context = SessionContext::default();
2233
2234 let result = execute_input("portfolio remove", &mut context, &config, &factory).await;
2235 assert!(result.is_ok());
2236 }
2237
2238 #[tokio::test]
2239 async fn test_portfolio_summary_with_chain_and_tag() {
2240 let tmp_dir = tempfile::tempdir().unwrap();
2241 let config = Config {
2242 portfolio: crate::config::PortfolioConfig {
2243 data_dir: Some(tmp_dir.path().to_path_buf()),
2244 },
2245 ..Default::default()
2246 };
2247 let factory = mock_factory();
2248 let mut context = SessionContext::default();
2249
2250 let result = execute_input(
2251 "portfolio summary --chain ethereum --tag defi --tokens",
2252 &mut context,
2253 &config,
2254 &factory,
2255 )
2256 .await;
2257 assert!(result.is_ok());
2258 }
2259
2260 #[tokio::test]
2261 async fn test_tokens_add_with_name() {
2262 let result = execute_tokens_command(&[
2263 "add",
2264 "USDC",
2265 "ethereum",
2266 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
2267 "USD",
2268 "Coin",
2269 ])
2270 .await;
2271 assert!(result.is_ok());
2272 }
2273
2274 #[tokio::test]
2275 async fn test_tokens_remove_with_chain() {
2276 let result = execute_tokens_command(&["remove", "USDC", "--chain", "ethereum"]).await;
2277 assert!(result.is_ok());
2278 }
2279
2280 #[tokio::test]
2281 async fn test_tokens_add_then_list_nonempty() {
2282 let _ = execute_tokens_command(&[
2284 "add",
2285 "TEST_TOKEN_XYZ",
2286 "ethereum",
2287 "0x1234567890abcdef1234567890abcdef12345678",
2288 "Test",
2289 "Token",
2290 ])
2291 .await;
2292
2293 let result = execute_tokens_command(&["list"]).await;
2295 assert!(result.is_ok());
2296
2297 let result = execute_tokens_command(&["recent"]).await;
2299 assert!(result.is_ok());
2300
2301 let _ = execute_tokens_command(&["remove", "TEST_TOKEN_XYZ"]).await;
2303 }
2304
2305 #[tokio::test]
2306 async fn test_session_context_save_and_load() {
2307 let ctx = SessionContext {
2310 chain: "solana".to_string(),
2311 last_address: Some("0xabc".to_string()),
2312 last_tx: Some("0xdef".to_string()),
2313 ..Default::default()
2314 };
2315 let _ = ctx.save();
2317 let loaded = SessionContext::load();
2319 assert!(!loaded.chain.is_empty());
2321 }
2322}