Skip to main content

reth_ethereum_cli/
interface.rs

1//! CLI definition and entrypoint to executable
2
3use crate::{app::CliApp, chainspec::EthereumChainSpecParser};
4use clap::{Parser, Subcommand};
5use reth_chainspec::{ChainSpec, Hardforks};
6use reth_cli::chainspec::ChainSpecParser;
7use reth_cli_commands::{
8    common::{CliComponentsBuilder, CliNodeTypes, HeaderMut},
9    config_cmd, db, download,
10    download::manifest_cmd,
11    dump_genesis, export_era, import, import_era, init_cmd, init_state,
12    launcher::FnLauncher,
13    node::{self, NoArgs},
14    p2p, prune, re_execute, stage,
15};
16use reth_cli_runner::CliRunner;
17use reth_db::DatabaseEnv;
18use reth_node_api::NodePrimitives;
19use reth_node_builder::{NodeBuilder, WithLaunchContext};
20use reth_node_core::{
21    args::{LogArgs, OtlpInitStatus, OtlpLogsStatus, TraceArgs},
22    version::version_metadata,
23};
24use reth_rpc_server_types::{DefaultRpcModuleValidator, RethRpcModule, RpcModuleValidator};
25use reth_tracing::{Layers, TracingGuards};
26use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
27use tracing::{info, warn};
28
29/// The main reth cli interface.
30///
31/// This is the entrypoint to the executable.
32#[derive(Debug, Parser)]
33#[command(author, name = version_metadata().name_client.as_ref(), version = version_metadata().short_version.as_ref(), long_version = version_metadata().long_version.as_ref(), about = "Reth", long_about = None)]
34pub struct Cli<
35    C: ChainSpecParser = EthereumChainSpecParser,
36    Ext: clap::Args + fmt::Debug = NoArgs,
37    Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
38    SubCmd: Subcommand + fmt::Debug = NoSubCmd,
39> {
40    /// The command to run
41    #[command(subcommand)]
42    pub command: Commands<C, Ext, SubCmd>,
43
44    /// The logging configuration for the CLI.
45    #[command(flatten)]
46    pub logs: LogArgs,
47
48    /// The tracing configuration for the CLI.
49    #[command(flatten)]
50    pub traces: TraceArgs,
51
52    /// Type marker for the RPC module validator
53    #[arg(skip)]
54    pub _phantom: PhantomData<Rpc>,
55}
56
57impl Cli {
58    /// Parsers only the default CLI arguments
59    pub fn parse_args() -> Self {
60        Self::parse()
61    }
62
63    /// Parsers only the default CLI arguments from the given iterator
64    pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
65    where
66        I: IntoIterator<Item = T>,
67        T: Into<OsString> + Clone,
68    {
69        Self::try_parse_from(itr)
70    }
71}
72
73impl<C, Ext, Rpc, SubCmd> Cli<C, Ext, Rpc, SubCmd>
74where
75    C: ChainSpecParser,
76    Ext: clap::Args + fmt::Debug,
77    Rpc: RpcModuleValidator,
78    SubCmd: Subcommand + fmt::Debug,
79{
80    /// Returns the node command, if this CLI was invoked with `node`.
81    pub fn as_node_command_mut(&mut self) -> Option<&mut node::NodeCommand<C, Ext>> {
82        match &mut self.command {
83            Commands::Node(command) => Some(command.as_mut()),
84            _ => None,
85        }
86    }
87
88    /// Applies a closure to the node command, if this CLI was invoked with `node`.
89    pub fn apply_node_command(
90        &mut self,
91        f: impl FnOnce(&mut node::NodeCommand<C, Ext>),
92    ) -> &mut Self {
93        if let Some(command) = self.as_node_command_mut() {
94            f(command);
95        }
96
97        self
98    }
99}
100
101impl<
102        C: ChainSpecParser,
103        Ext: clap::Args + fmt::Debug,
104        Rpc: RpcModuleValidator,
105        SubCmd: crate::app::ExtendedCommand + Subcommand + fmt::Debug,
106    > Cli<C, Ext, Rpc, SubCmd>
107{
108    /// Configures the CLI and returns a [`CliApp`] instance.
109    ///
110    /// This method is used to prepare the CLI for execution by wrapping it in a
111    /// [`CliApp`] that can be further configured before running.
112    pub fn configure(self) -> CliApp<C, Ext, Rpc, SubCmd> {
113        CliApp::new(self)
114    }
115
116    /// Execute the configured cli command.
117    ///
118    /// This accepts a closure that is used to launch the node via the
119    /// [`NodeCommand`](node::NodeCommand).
120    ///
121    /// This command will be run on the default tokio runtime.
122    ///
123    ///
124    /// # Example
125    ///
126    /// ```no_run
127    /// use reth_ethereum_cli::interface::Cli;
128    /// use reth_node_ethereum::EthereumNode;
129    ///
130    /// Cli::parse_args()
131    ///     .run(async move |builder, _| {
132    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
133    ///
134    ///         handle.wait_for_node_exit().await
135    ///     })
136    ///     .unwrap();
137    /// ```
138    ///
139    /// # Example
140    ///
141    /// Parse additional CLI arguments for the node command and use it to configure the node.
142    ///
143    /// ```no_run
144    /// use clap::Parser;
145    /// use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, interface::Cli};
146    ///
147    /// #[derive(Debug, Parser)]
148    /// pub struct MyArgs {
149    ///     pub enable: bool,
150    /// }
151    ///
152    /// Cli::<EthereumChainSpecParser, MyArgs>::parse()
153    ///     .run(async move |builder, my_args: MyArgs|
154    ///         // launch the node
155    ///         Ok(()))
156    ///     .unwrap();
157    /// ````
158    pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
159    where
160        L: FnOnce(WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>, Ext) -> Fut,
161        Fut: Future<Output = eyre::Result<()>>,
162        C: ChainSpecParser<ChainSpec = ChainSpec>,
163    {
164        self.configure()
165            .run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
166    }
167
168    /// Execute the configured cli command with the provided [`CliComponentsBuilder`].
169    ///
170    /// This accepts a closure that is used to launch the node via the
171    /// [`NodeCommand`](node::NodeCommand).
172    ///
173    /// This command will be run on the default tokio runtime.
174    pub fn run_with_components<N>(
175        self,
176        components: impl CliComponentsBuilder<N>,
177        launcher: impl AsyncFnOnce(
178            WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>,
179            Ext,
180        ) -> eyre::Result<()>,
181    ) -> eyre::Result<()>
182    where
183        N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
184        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
185    {
186        self.configure().run_with_components(components, launcher)
187    }
188
189    /// Execute the configured cli command with the provided [`CliRunner`].
190    ///
191    ///
192    /// # Example
193    ///
194    /// ```no_run
195    /// use reth_cli_runner::CliRunner;
196    /// use reth_ethereum_cli::interface::Cli;
197    /// use reth_node_ethereum::EthereumNode;
198    ///
199    /// let runner = CliRunner::try_default_runtime().unwrap();
200    ///
201    /// Cli::parse_args()
202    ///     .with_runner(runner, |builder, _| async move {
203    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
204    ///         handle.wait_for_node_exit().await
205    ///     })
206    ///     .unwrap();
207    /// ```
208    pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
209    where
210        L: FnOnce(WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>, Ext) -> Fut,
211        Fut: Future<Output = eyre::Result<()>>,
212        C: ChainSpecParser<ChainSpec = ChainSpec>,
213    {
214        let mut app = self.configure();
215        app.set_runner(runner);
216        app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
217    }
218
219    /// Execute the configured cli command with the provided [`CliRunner`] and
220    /// [`CliComponentsBuilder`].
221    pub fn with_runner_and_components<N>(
222        self,
223        runner: CliRunner,
224        components: impl CliComponentsBuilder<N>,
225        launcher: impl AsyncFnOnce(
226            WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>,
227            Ext,
228        ) -> eyre::Result<()>,
229    ) -> eyre::Result<()>
230    where
231        N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
232        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
233    {
234        let mut app = self.configure();
235        app.set_runner(runner);
236        app.run_with_components(components, launcher)
237    }
238
239    /// Initializes tracing with the configured options.
240    ///
241    /// Returns tracing guards that must be kept alive to ensure outputs are flushed to disk.
242    ///
243    /// If an OTLP endpoint is specified, it will export traces and logs to the configured
244    /// collector.
245    pub fn init_tracing(
246        &mut self,
247        runner: &CliRunner,
248        mut layers: Layers,
249    ) -> eyre::Result<TracingGuards> {
250        let otlp_status = runner.block_on(self.traces.init_otlp_tracing(&mut layers))?;
251        let otlp_logs_status = runner.block_on(self.traces.init_otlp_logs(&mut layers))?;
252
253        // Enable reload support if debug RPC namespace is available
254        let enable_reload = self.command.debug_namespace_enabled();
255        let guards = self.logs.init_tracing_with_layers(layers, enable_reload)?;
256        info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
257
258        match otlp_status {
259            OtlpInitStatus::Started(endpoint) => {
260                info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.traces.protocol);
261            }
262            OtlpInitStatus::NoFeature => {
263                warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature")
264            }
265            OtlpInitStatus::Disabled => {}
266        }
267
268        match otlp_logs_status {
269            OtlpLogsStatus::Started(endpoint) => {
270                info!(target: "reth::cli", "Started OTLP {:?} logs export to {endpoint}", self.traces.protocol);
271            }
272            OtlpLogsStatus::NoFeature => {
273                warn!(target: "reth::cli", "Provided OTLP logs arguments do not have effect, compile with the `otlp-logs` feature")
274            }
275            OtlpLogsStatus::Disabled => {}
276        }
277
278        Ok(guards)
279    }
280}
281
282/// Commands to be executed
283#[derive(Debug, Subcommand)]
284pub enum Commands<
285    C: ChainSpecParser,
286    Ext: clap::Args + fmt::Debug,
287    SubCmd: Subcommand + fmt::Debug = NoSubCmd,
288> {
289    /// Start the node
290    #[command(name = "node")]
291    Node(Box<node::NodeCommand<C, Ext>>),
292    /// Initialize the database from a genesis file.
293    #[command(name = "init")]
294    Init(init_cmd::InitCommand<C>),
295    /// Initialize the database from a state dump file.
296    #[command(name = "init-state")]
297    InitState(init_state::InitStateCommand<C>),
298    /// This syncs RLP encoded blocks from a file or files.
299    #[command(name = "import")]
300    Import(import::ImportCommand<C>),
301    /// This syncs ERA encoded blocks from a directory.
302    #[command(name = "import-era")]
303    ImportEra(import_era::ImportEraCommand<C>),
304    /// Exports block to era1 files in a specified directory.
305    #[command(name = "export-era")]
306    ExportEra(export_era::ExportEraCommand<C>),
307    /// Dumps genesis block JSON configuration to stdout.
308    DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
309    /// Database debugging utilities
310    #[command(name = "db")]
311    Db(Box<db::Command<C>>),
312    /// Download public node snapshots
313    #[command(name = "download")]
314    Download(download::DownloadCommand<C>),
315    /// Generate a snapshot manifest from local archive files.
316    #[command(name = "snapshot-manifest")]
317    SnapshotManifest(manifest_cmd::SnapshotManifestCommand),
318    /// Manipulate individual stages.
319    #[command(name = "stage")]
320    Stage(stage::Command<C>),
321    /// P2P Debugging utilities
322    #[command(name = "p2p")]
323    P2P(Box<p2p::Command<C>>),
324    /// Generate Test Vectors
325    #[cfg(feature = "dev")]
326    #[command(name = "test-vectors")]
327    TestVectors(reth_cli_commands::test_vectors::Command),
328    /// Write config to stdout
329    #[command(name = "config")]
330    Config(config_cmd::Command),
331    /// Prune according to the configuration without any limits
332    #[command(name = "prune")]
333    Prune(prune::PruneCommand<C>),
334    /// Re-execute blocks in parallel to verify historical sync correctness.
335    #[command(name = "re-execute")]
336    ReExecute(re_execute::Command<C>),
337    /// Extension subcommands provided by consumers.
338    #[command(flatten)]
339    Ext(SubCmd),
340}
341
342/// A no-op subcommand type for when no extension subcommands are needed.
343///
344/// This is the default type parameter for `Commands` when consumers don't need
345/// to add custom subcommands.
346#[derive(Debug, Subcommand)]
347pub enum NoSubCmd {}
348
349impl crate::app::ExtendedCommand for NoSubCmd {
350    fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
351        match self {}
352    }
353}
354
355impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, SubCmd: Subcommand + fmt::Debug>
356    Commands<C, Ext, SubCmd>
357{
358    /// Returns the underlying chain being used for commands
359    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
360        match self {
361            Self::Node(cmd) => cmd.chain_spec(),
362            Self::Init(cmd) => cmd.chain_spec(),
363            Self::InitState(cmd) => cmd.chain_spec(),
364            Self::Import(cmd) => cmd.chain_spec(),
365            Self::ExportEra(cmd) => cmd.chain_spec(),
366            Self::ImportEra(cmd) => cmd.chain_spec(),
367            Self::DumpGenesis(cmd) => cmd.chain_spec(),
368            Self::Db(cmd) => cmd.chain_spec(),
369            Self::Download(cmd) => cmd.chain_spec(),
370            Self::SnapshotManifest(_) => None,
371            Self::Stage(cmd) => cmd.chain_spec(),
372            Self::P2P(cmd) => cmd.chain_spec(),
373            #[cfg(feature = "dev")]
374            Self::TestVectors(_) => None,
375            Self::Config(_) => None,
376            Self::Prune(cmd) => cmd.chain_spec(),
377            Self::ReExecute(cmd) => cmd.chain_spec(),
378            Self::Ext(_) => None,
379        }
380    }
381
382    /// Returns `true` if this is a node command with debug RPC namespace enabled.
383    ///
384    /// This is used to determine whether to enable runtime log level changes.
385    pub fn debug_namespace_enabled(&self) -> bool {
386        match self {
387            Self::Node(cmd) => cmd.rpc.is_namespace_enabled(RethRpcModule::Debug),
388            _ => false,
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::{app::ExtendedCommand, chainspec::SUPPORTED_CHAINS};
397    use clap::CommandFactory;
398    use reth_chainspec::{EthChainSpec, SEPOLIA};
399    use reth_node_core::args::ColorMode;
400    use std::sync::atomic::{AtomicBool, Ordering};
401
402    #[test]
403    fn parse_color_mode() {
404        let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
405        assert_eq!(reth.logs.color, ColorMode::Always);
406    }
407
408    #[test]
409    fn node_command_mut_accessor_returns_node_command() {
410        let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
411
412        let node_command = reth.as_node_command_mut().expect("expected node command");
413        node_command.with_unused_ports = true;
414
415        assert!(reth.as_node_command_mut().unwrap().with_unused_ports);
416    }
417
418    #[test]
419    fn apply_node_command_only_runs_for_node_command() {
420        let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
421        reth.apply_node_command(|node_command| node_command.with_unused_ports = true);
422        assert!(reth.as_node_command_mut().unwrap().with_unused_ports);
423
424        let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
425        let mut applied = false;
426        reth.apply_node_command(|_| applied = true);
427
428        assert!(reth.as_node_command_mut().is_none());
429        assert!(!applied);
430    }
431
432    /// Tests that the help message is parsed correctly. This ensures that clap args are configured
433    /// correctly and no conflicts are introduced via attributes that would result in a panic at
434    /// runtime
435    #[test]
436    fn test_parse_help_all_subcommands() {
437        let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
438        for sub_command in reth.get_subcommands() {
439            let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
440                .err()
441                .unwrap_or_else(|| {
442                    panic!("Failed to parse help message {}", sub_command.get_name())
443                });
444
445            // --help is treated as error, but
446            // > Not a true "error" as it means --help or similar was used. The help message will be sent to stdout.
447            assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
448        }
449    }
450
451    /// Tests that the log directory is parsed correctly when using the node command. It's
452    /// always tied to the specific chain's name.
453    #[test]
454    fn parse_logs_path_node() {
455        let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
456        if let Some(chain_spec) = reth.command.chain_spec() {
457            reth.logs.log_file_directory =
458                reth.logs.log_file_directory.join(chain_spec.chain.to_string());
459        }
460        let log_dir = reth.logs.log_file_directory;
461        let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
462        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
463
464        let mut iter = SUPPORTED_CHAINS.iter();
465        iter.next();
466        for chain in iter {
467            let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
468            let chain =
469                reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
470            reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
471            let log_dir = reth.logs.log_file_directory;
472            let end = format!("reth/logs/{chain}");
473            assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
474        }
475    }
476
477    /// Tests that the log directory is parsed correctly when using the init command. It
478    /// uses the underlying environment in command to get the chain.
479    #[test]
480    fn parse_logs_path_init() {
481        let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
482        if let Some(chain_spec) = reth.command.chain_spec() {
483            reth.logs.log_file_directory =
484                reth.logs.log_file_directory.join(chain_spec.chain.to_string());
485        }
486        let log_dir = reth.logs.log_file_directory;
487        let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
488        println!("{log_dir:?}");
489        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
490    }
491
492    /// Tests that the config command does not return any chain spec leading to empty chain id.
493    #[test]
494    fn parse_empty_logs_path() {
495        let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
496        if let Some(chain_spec) = reth.command.chain_spec() {
497            reth.logs.log_file_directory =
498                reth.logs.log_file_directory.join(chain_spec.chain.to_string());
499        }
500        let log_dir = reth.logs.log_file_directory;
501        let end = "reth/logs".to_string();
502        println!("{log_dir:?}");
503        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
504    }
505
506    #[test]
507    fn log_file_max_files_defaults() {
508        use reth_node_core::args::LogArgs;
509
510        // Node command without explicit --log.file.max-files should get Some(5) after
511        // apply_node_defaults
512        let mut cli = Cli::try_parse_args_from(["reth", "node"]).unwrap();
513        assert!(cli.logs.log_file_max_files.is_none());
514        cli.logs.apply_node_defaults();
515        assert_eq!(cli.logs.log_file_max_files, Some(LogArgs::DEFAULT_MAX_LOG_FILES_NODE));
516
517        // Non-node command without explicit --log.file.max-files should be None and
518        // effective_log_file_max_files returns 0
519        let cli = Cli::try_parse_args_from(["reth", "config"]).unwrap();
520        assert!(cli.logs.log_file_max_files.is_none());
521        assert_eq!(cli.logs.effective_log_file_max_files(), 0);
522
523        // Explicitly set value should be preserved for node command
524        let mut cli =
525            Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "10"]).unwrap();
526        assert_eq!(cli.logs.log_file_max_files, Some(10));
527        cli.logs.apply_node_defaults();
528        assert_eq!(cli.logs.log_file_max_files, Some(10));
529
530        // Explicitly set value should be preserved for non-node command
531        let cli =
532            Cli::try_parse_args_from(["reth", "config", "--log.file.max-files", "3"]).unwrap();
533        assert_eq!(cli.logs.log_file_max_files, Some(3));
534        assert_eq!(cli.logs.effective_log_file_max_files(), 3);
535
536        // Setting to 0 explicitly should work
537        let cli = Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "0"]).unwrap();
538        assert_eq!(cli.logs.log_file_max_files, Some(0));
539        assert_eq!(cli.logs.effective_log_file_max_files(), 0);
540    }
541
542    #[test]
543    fn parse_env_filter_directives() {
544        let temp_dir = tempfile::tempdir().unwrap();
545
546        let reth = Cli::try_parse_args_from([
547            "reth",
548            "init",
549            "--datadir",
550            temp_dir.path().to_str().unwrap(),
551            "--log.file.filter",
552            "debug,net=trace",
553        ])
554        .unwrap();
555        assert!(reth.run(async move |_, _| Ok(())).is_ok());
556    }
557
558    #[test]
559    fn test_rpc_module_validation() {
560        use reth_rpc_server_types::RethRpcModule;
561
562        // Test that standard modules are accepted
563        let cli =
564            Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
565
566        if let Commands::Node(command) = &cli.command {
567            if let Some(http_api) = &command.rpc.http_api {
568                // Should contain the expected modules
569                let modules = http_api.to_selection();
570                assert!(modules.contains(&RethRpcModule::Eth));
571                assert!(modules.contains(&RethRpcModule::Admin));
572                assert!(modules.contains(&RethRpcModule::Debug));
573            } else {
574                panic!("Expected http.api to be set");
575            }
576        } else {
577            panic!("Expected Node command");
578        }
579
580        // Test that unknown modules are parsed as Other variant
581        let cli =
582            Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
583
584        if let Commands::Node(command) = &cli.command {
585            if let Some(http_api) = &command.rpc.http_api {
586                let modules = http_api.to_selection();
587                assert!(modules.contains(&RethRpcModule::Eth));
588                assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
589            } else {
590                panic!("Expected http.api to be set");
591            }
592        } else {
593            panic!("Expected Node command");
594        }
595    }
596
597    #[test]
598    fn test_rpc_module_unknown_rejected() {
599        use reth_cli_runner::CliRunner;
600
601        // Test that unknown module names are rejected during validation
602        let cli =
603            Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
604
605        // When we try to run the CLI with validation, it should fail
606        let runner = CliRunner::try_default_runtime().unwrap();
607        let result = cli.with_runner(runner, |_, _| async { Ok(()) });
608
609        assert!(result.is_err());
610        let err = result.unwrap_err();
611        let err_msg = err.to_string();
612
613        // The error should mention it's an unknown module
614        assert!(
615            err_msg.contains("Unknown RPC module"),
616            "Error should mention unknown module: {}",
617            err_msg
618        );
619        assert!(
620            err_msg.contains("'unknownmodule'"),
621            "Error should mention the module name: {}",
622            err_msg
623        );
624    }
625
626    #[test]
627    fn parse_unwind_chain() {
628        let cli = Cli::try_parse_args_from([
629            "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
630        ])
631        .unwrap();
632        match cli.command {
633            Commands::Stage(cmd) => match cmd.command {
634                stage::Subcommands::Unwind(cmd) => {
635                    assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
636                }
637                _ => panic!("Expected Unwind command"),
638            },
639            _ => panic!("Expected Stage command"),
640        };
641    }
642
643    #[test]
644    fn parse_empty_supported_chains() {
645        #[derive(Debug, Clone, Default)]
646        struct FileChainSpecParser;
647
648        impl ChainSpecParser for FileChainSpecParser {
649            type ChainSpec = ChainSpec;
650
651            const SUPPORTED_CHAINS: &'static [&'static str] = &[];
652
653            fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
654                EthereumChainSpecParser::parse(s)
655            }
656        }
657
658        let cli = Cli::<FileChainSpecParser>::try_parse_from([
659            "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
660        ])
661        .unwrap();
662        match cli.command {
663            Commands::Stage(cmd) => match cmd.command {
664                stage::Subcommands::Unwind(cmd) => {
665                    assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
666                }
667                _ => panic!("Expected Unwind command"),
668            },
669            _ => panic!("Expected Stage command"),
670        };
671    }
672
673    #[test]
674    fn test_extensible_subcommands() {
675        use reth_cli_runner::CliRunner;
676        use reth_rpc_server_types::DefaultRpcModuleValidator;
677
678        #[derive(Debug, Subcommand)]
679        enum CustomCommands {
680            /// A custom hello command
681            #[command(name = "hello")]
682            Hello {
683                /// Name to greet
684                #[arg(long)]
685                name: String,
686            },
687            /// Another custom command
688            #[command(name = "goodbye")]
689            Goodbye,
690        }
691
692        static EXECUTED: AtomicBool = AtomicBool::new(false);
693
694        impl ExtendedCommand for CustomCommands {
695            fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
696                match self {
697                    Self::Hello { name } => {
698                        assert_eq!(name, "world");
699                        EXECUTED.store(true, Ordering::SeqCst);
700                        Ok(())
701                    }
702                    Self::Goodbye => Ok(()),
703                }
704            }
705        }
706
707        // Test parsing the custom "hello" command
708        let cli = Cli::<
709            EthereumChainSpecParser,
710            NoArgs,
711            DefaultRpcModuleValidator,
712            CustomCommands,
713        >::try_parse_from(["reth", "hello", "--name", "world"])
714        .unwrap();
715
716        match &cli.command {
717            Commands::Ext(CustomCommands::Hello { name }) => {
718                assert_eq!(name, "world");
719            }
720            _ => panic!("Expected Ext(Hello) command"),
721        }
722
723        // Test parsing the custom "goodbye" command
724        let cli = Cli::<
725            EthereumChainSpecParser,
726            NoArgs,
727            DefaultRpcModuleValidator,
728            CustomCommands,
729        >::try_parse_from(["reth", "goodbye"])
730        .unwrap();
731
732        match &cli.command {
733            Commands::Ext(CustomCommands::Goodbye) => {}
734            _ => panic!("Expected Ext(Goodbye) command"),
735        }
736
737        // Test that built-in commands still work alongside custom ones
738        let cli = Cli::<
739            EthereumChainSpecParser,
740            NoArgs,
741            DefaultRpcModuleValidator,
742            CustomCommands,
743        >::try_parse_from(["reth", "node"])
744        .unwrap();
745
746        match &cli.command {
747            Commands::Node(_) => {}
748            _ => panic!("Expected Node command"),
749        }
750
751        // Test executing the custom command
752        let cli = Cli::<
753            EthereumChainSpecParser,
754            NoArgs,
755            DefaultRpcModuleValidator,
756            CustomCommands,
757        >::try_parse_from(["reth", "hello", "--name", "world"])
758        .unwrap();
759
760        if let Commands::Ext(cmd) = cli.command {
761            let runner = CliRunner::try_default_runtime().unwrap();
762            cmd.execute(runner).unwrap();
763            assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
764        }
765    }
766}