1use 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#[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 #[command(subcommand)]
42 pub command: Commands<C, Ext, SubCmd>,
43
44 #[command(flatten)]
46 pub logs: LogArgs,
47
48 #[command(flatten)]
50 pub traces: TraceArgs,
51
52 #[arg(skip)]
54 pub _phantom: PhantomData<Rpc>,
55}
56
57impl Cli {
58 pub fn parse_args() -> Self {
60 Self::parse()
61 }
62
63 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 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 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 pub fn configure(self) -> CliApp<C, Ext, Rpc, SubCmd> {
113 CliApp::new(self)
114 }
115
116 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 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 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 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 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 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#[derive(Debug, Subcommand)]
284pub enum Commands<
285 C: ChainSpecParser,
286 Ext: clap::Args + fmt::Debug,
287 SubCmd: Subcommand + fmt::Debug = NoSubCmd,
288> {
289 #[command(name = "node")]
291 Node(Box<node::NodeCommand<C, Ext>>),
292 #[command(name = "init")]
294 Init(init_cmd::InitCommand<C>),
295 #[command(name = "init-state")]
297 InitState(init_state::InitStateCommand<C>),
298 #[command(name = "import")]
300 Import(import::ImportCommand<C>),
301 #[command(name = "import-era")]
303 ImportEra(import_era::ImportEraCommand<C>),
304 #[command(name = "export-era")]
306 ExportEra(export_era::ExportEraCommand<C>),
307 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
309 #[command(name = "db")]
311 Db(Box<db::Command<C>>),
312 #[command(name = "download")]
314 Download(download::DownloadCommand<C>),
315 #[command(name = "snapshot-manifest")]
317 SnapshotManifest(manifest_cmd::SnapshotManifestCommand),
318 #[command(name = "stage")]
320 Stage(stage::Command<C>),
321 #[command(name = "p2p")]
323 P2P(Box<p2p::Command<C>>),
324 #[cfg(feature = "dev")]
326 #[command(name = "test-vectors")]
327 TestVectors(reth_cli_commands::test_vectors::Command),
328 #[command(name = "config")]
330 Config(config_cmd::Command),
331 #[command(name = "prune")]
333 Prune(prune::PruneCommand<C>),
334 #[command(name = "re-execute")]
336 ReExecute(re_execute::Command<C>),
337 #[command(flatten)]
339 Ext(SubCmd),
340}
341
342#[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 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 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::chainspec::SUPPORTED_CHAINS;
397 use clap::CommandFactory;
398 use reth_chainspec::{EthChainSpec, SEPOLIA};
399 use reth_node_core::args::ColorMode;
400
401 #[test]
402 fn parse_color_mode() {
403 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
404 assert_eq!(reth.logs.color, ColorMode::Always);
405 }
406
407 #[test]
408 fn node_command_mut_accessor_returns_node_command() {
409 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
410
411 let node_command = reth.as_node_command_mut().expect("expected node command");
412 node_command.with_unused_ports = true;
413
414 assert!(reth.as_node_command_mut().unwrap().with_unused_ports);
415 }
416
417 #[test]
418 fn apply_node_command_only_runs_for_node_command() {
419 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
420 reth.apply_node_command(|node_command| node_command.with_unused_ports = true);
421 assert!(reth.as_node_command_mut().unwrap().with_unused_ports);
422
423 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
424 let mut applied = false;
425 reth.apply_node_command(|_| applied = true);
426
427 assert!(reth.as_node_command_mut().is_none());
428 assert!(!applied);
429 }
430
431 #[test]
435 fn test_parse_help_all_subcommands() {
436 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
437 for sub_command in reth.get_subcommands() {
438 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
439 .err()
440 .unwrap_or_else(|| {
441 panic!("Failed to parse help message {}", sub_command.get_name())
442 });
443
444 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
447 }
448 }
449
450 #[test]
453 fn parse_logs_path_node() {
454 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
455 if let Some(chain_spec) = reth.command.chain_spec() {
456 reth.logs.log_file_directory =
457 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
458 }
459 let log_dir = reth.logs.log_file_directory;
460 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
461 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
462
463 let mut iter = SUPPORTED_CHAINS.iter();
464 iter.next();
465 for chain in iter {
466 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
467 let chain =
468 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
469 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
470 let log_dir = reth.logs.log_file_directory;
471 let end = format!("reth/logs/{chain}");
472 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
473 }
474 }
475
476 #[test]
479 fn parse_logs_path_init() {
480 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
481 if let Some(chain_spec) = reth.command.chain_spec() {
482 reth.logs.log_file_directory =
483 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
484 }
485 let log_dir = reth.logs.log_file_directory;
486 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
487 println!("{log_dir:?}");
488 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
489 }
490
491 #[test]
493 fn parse_empty_logs_path() {
494 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
495 if let Some(chain_spec) = reth.command.chain_spec() {
496 reth.logs.log_file_directory =
497 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
498 }
499 let log_dir = reth.logs.log_file_directory;
500 let end = "reth/logs".to_string();
501 println!("{log_dir:?}");
502 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
503 }
504
505 #[test]
506 fn log_file_max_files_defaults() {
507 use reth_node_core::args::LogArgs;
508
509 let mut cli = Cli::try_parse_args_from(["reth", "node"]).unwrap();
512 assert!(cli.logs.log_file_max_files.is_none());
513 cli.logs.apply_node_defaults();
514 assert_eq!(cli.logs.log_file_max_files, Some(LogArgs::DEFAULT_MAX_LOG_FILES_NODE));
515
516 let cli = Cli::try_parse_args_from(["reth", "config"]).unwrap();
519 assert!(cli.logs.log_file_max_files.is_none());
520 assert_eq!(cli.logs.effective_log_file_max_files(), 0);
521
522 let mut cli =
524 Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "10"]).unwrap();
525 assert_eq!(cli.logs.log_file_max_files, Some(10));
526 cli.logs.apply_node_defaults();
527 assert_eq!(cli.logs.log_file_max_files, Some(10));
528
529 let cli =
531 Cli::try_parse_args_from(["reth", "config", "--log.file.max-files", "3"]).unwrap();
532 assert_eq!(cli.logs.log_file_max_files, Some(3));
533 assert_eq!(cli.logs.effective_log_file_max_files(), 3);
534
535 let cli = Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "0"]).unwrap();
537 assert_eq!(cli.logs.log_file_max_files, Some(0));
538 assert_eq!(cli.logs.effective_log_file_max_files(), 0);
539 }
540
541 #[test]
542 fn parse_env_filter_directives() {
543 let temp_dir = tempfile::tempdir().unwrap();
544
545 let reth = Cli::try_parse_args_from([
546 "reth",
547 "init",
548 "--datadir",
549 temp_dir.path().to_str().unwrap(),
550 "--log.file.filter",
551 "debug,net=trace",
552 ])
553 .unwrap();
554 assert!(reth.run(async move |_, _| Ok(())).is_ok());
555 }
556
557 #[test]
558 fn test_rpc_module_validation() {
559 use reth_rpc_server_types::RethRpcModule;
560
561 let cli =
563 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
564
565 if let Commands::Node(command) = &cli.command {
566 if let Some(http_api) = &command.rpc.http_api {
567 let modules = http_api.to_selection();
569 assert!(modules.contains(&RethRpcModule::Eth));
570 assert!(modules.contains(&RethRpcModule::Admin));
571 assert!(modules.contains(&RethRpcModule::Debug));
572 } else {
573 panic!("Expected http.api to be set");
574 }
575 } else {
576 panic!("Expected Node command");
577 }
578
579 let cli =
581 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
582
583 if let Commands::Node(command) = &cli.command {
584 if let Some(http_api) = &command.rpc.http_api {
585 let modules = http_api.to_selection();
586 assert!(modules.contains(&RethRpcModule::Eth));
587 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
588 } else {
589 panic!("Expected http.api to be set");
590 }
591 } else {
592 panic!("Expected Node command");
593 }
594 }
595
596 #[test]
597 fn test_rpc_module_unknown_rejected() {
598 use reth_cli_runner::CliRunner;
599
600 let cli =
602 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
603
604 let runner = CliRunner::try_default_runtime().unwrap();
606 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
607
608 assert!(result.is_err());
609 let err = result.unwrap_err();
610 let err_msg = err.to_string();
611
612 assert!(
614 err_msg.contains("Unknown RPC module"),
615 "Error should mention unknown module: {}",
616 err_msg
617 );
618 assert!(
619 err_msg.contains("'unknownmodule'"),
620 "Error should mention the module name: {}",
621 err_msg
622 );
623 }
624
625 #[test]
626 fn parse_unwind_chain() {
627 let cli = Cli::try_parse_args_from([
628 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
629 ])
630 .unwrap();
631 match cli.command {
632 Commands::Stage(cmd) => match cmd.command {
633 stage::Subcommands::Unwind(cmd) => {
634 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
635 }
636 _ => panic!("Expected Unwind command"),
637 },
638 _ => panic!("Expected Stage command"),
639 };
640 }
641
642 #[test]
643 fn parse_empty_supported_chains() {
644 #[derive(Debug, Clone, Default)]
645 struct FileChainSpecParser;
646
647 impl ChainSpecParser for FileChainSpecParser {
648 type ChainSpec = ChainSpec;
649
650 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
651
652 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
653 EthereumChainSpecParser::parse(s)
654 }
655 }
656
657 let cli = Cli::<FileChainSpecParser>::try_parse_from([
658 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
659 ])
660 .unwrap();
661 match cli.command {
662 Commands::Stage(cmd) => match cmd.command {
663 stage::Subcommands::Unwind(cmd) => {
664 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
665 }
666 _ => panic!("Expected Unwind command"),
667 },
668 _ => panic!("Expected Stage command"),
669 };
670 }
671
672 #[test]
673 fn test_extensible_subcommands() {
674 use crate::app::ExtendedCommand;
675 use reth_cli_runner::CliRunner;
676 use reth_rpc_server_types::DefaultRpcModuleValidator;
677 use std::sync::atomic::{AtomicBool, Ordering};
678
679 #[derive(Debug, Subcommand)]
680 enum CustomCommands {
681 #[command(name = "hello")]
683 Hello {
684 #[arg(long)]
686 name: String,
687 },
688 #[command(name = "goodbye")]
690 Goodbye,
691 }
692
693 static EXECUTED: AtomicBool = AtomicBool::new(false);
694
695 impl ExtendedCommand for CustomCommands {
696 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
697 match self {
698 Self::Hello { name } => {
699 assert_eq!(name, "world");
700 EXECUTED.store(true, Ordering::SeqCst);
701 Ok(())
702 }
703 Self::Goodbye => Ok(()),
704 }
705 }
706 }
707
708 let cli = Cli::<
710 EthereumChainSpecParser,
711 NoArgs,
712 DefaultRpcModuleValidator,
713 CustomCommands,
714 >::try_parse_from(["reth", "hello", "--name", "world"])
715 .unwrap();
716
717 match &cli.command {
718 Commands::Ext(CustomCommands::Hello { name }) => {
719 assert_eq!(name, "world");
720 }
721 _ => panic!("Expected Ext(Hello) command"),
722 }
723
724 let cli = Cli::<
726 EthereumChainSpecParser,
727 NoArgs,
728 DefaultRpcModuleValidator,
729 CustomCommands,
730 >::try_parse_from(["reth", "goodbye"])
731 .unwrap();
732
733 match &cli.command {
734 Commands::Ext(CustomCommands::Goodbye) => {}
735 _ => panic!("Expected Ext(Goodbye) command"),
736 }
737
738 let cli = Cli::<
740 EthereumChainSpecParser,
741 NoArgs,
742 DefaultRpcModuleValidator,
743 CustomCommands,
744 >::try_parse_from(["reth", "node"])
745 .unwrap();
746
747 match &cli.command {
748 Commands::Node(_) => {}
749 _ => panic!("Expected Node command"),
750 }
751
752 let cli = Cli::<
754 EthereumChainSpecParser,
755 NoArgs,
756 DefaultRpcModuleValidator,
757 CustomCommands,
758 >::try_parse_from(["reth", "hello", "--name", "world"])
759 .unwrap();
760
761 if let Commands::Ext(cmd) = cli.command {
762 let runner = CliRunner::try_default_runtime().unwrap();
763 cmd.execute(runner).unwrap();
764 assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
765 }
766 }
767}