Skip to main content

scope/cli/
interactive.rs

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