1use crate::{
4 app::{run_commands_with, CliApp},
5 chainspec::EthereumChainSpecParser,
6};
7use clap::{Parser, Subcommand};
8use reth_chainspec::{ChainSpec, EthChainSpec, Hardforks};
9use reth_cli::chainspec::ChainSpecParser;
10use reth_cli_commands::{
11 common::{CliComponentsBuilder, CliHeader, CliNodeTypes},
12 config_cmd, db, download, dump_genesis, export_era, import, import_era, init_cmd, init_state,
13 launcher::FnLauncher,
14 node::{self, NoArgs},
15 p2p, prune, re_execute, stage,
16};
17use reth_cli_runner::CliRunner;
18use reth_db::DatabaseEnv;
19use reth_node_api::NodePrimitives;
20use reth_node_builder::{NodeBuilder, WithLaunchContext};
21use reth_node_core::{args::LogArgs, version::version_metadata};
22use reth_node_metrics::recorder::install_prometheus_recorder;
23use reth_rpc_server_types::{DefaultRpcModuleValidator, RpcModuleValidator};
24use reth_tracing::FileWorkerGuard;
25use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
26use tracing::info;
27
28#[derive(Debug, Parser)]
32#[command(author, version =version_metadata().short_version.as_ref(), long_version = version_metadata().long_version.as_ref(), about = "Reth", long_about = None)]
33pub struct Cli<
34 C: ChainSpecParser = EthereumChainSpecParser,
35 Ext: clap::Args + fmt::Debug = NoArgs,
36 Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
37> {
38 #[command(subcommand)]
40 pub command: Commands<C, Ext>,
41
42 #[command(flatten)]
44 pub logs: LogArgs,
45
46 #[arg(skip)]
48 pub _phantom: PhantomData<Rpc>,
49}
50
51impl Cli {
52 pub fn parse_args() -> Self {
54 Self::parse()
55 }
56
57 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
59 where
60 I: IntoIterator<Item = T>,
61 T: Into<OsString> + Clone,
62 {
63 Self::try_parse_from(itr)
64 }
65}
66
67impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> Cli<C, Ext, Rpc> {
68 pub fn configure(self) -> CliApp<C, Ext, Rpc>
73 where
74 C: ChainSpecParser<ChainSpec = ChainSpec>,
75 {
76 CliApp::new(self)
77 }
78
79 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
122 where
123 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
124 Fut: Future<Output = eyre::Result<()>>,
125 C: ChainSpecParser<ChainSpec = ChainSpec>,
126 {
127 self.with_runner(CliRunner::try_default_runtime()?, launcher)
128 }
129
130 pub fn run_with_components<N>(
137 self,
138 components: impl CliComponentsBuilder<N>,
139 launcher: impl AsyncFnOnce(
140 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
141 Ext,
142 ) -> eyre::Result<()>,
143 ) -> eyre::Result<()>
144 where
145 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
146 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
147 {
148 self.with_runner_and_components(CliRunner::try_default_runtime()?, components, launcher)
149 }
150
151 pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
171 where
172 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
173 Fut: Future<Output = eyre::Result<()>>,
174 C: ChainSpecParser<ChainSpec = ChainSpec>,
175 {
176 let mut app = self.configure();
177 app.set_runner(runner);
178 app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
179 }
180
181 pub fn with_runner_and_components<N>(
184 mut self,
185 runner: CliRunner,
186 components: impl CliComponentsBuilder<N>,
187 launcher: impl AsyncFnOnce(
188 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
189 Ext,
190 ) -> eyre::Result<()>,
191 ) -> eyre::Result<()>
192 where
193 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
194 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
195 {
196 if let Some(chain_spec) = self.command.chain_spec() {
198 self.logs.log_file_directory =
199 self.logs.log_file_directory.join(chain_spec.chain().to_string());
200 }
201 let _guard = self.init_tracing()?;
202 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
203
204 let _ = install_prometheus_recorder();
206
207 run_commands_with::<C, Ext, Rpc, N>(self, runner, components, launcher)
209 }
210
211 pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
216 let guard = self.logs.init_tracing()?;
217 Ok(guard)
218 }
219}
220
221#[derive(Debug, Subcommand)]
223pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
224 #[command(name = "node")]
226 Node(Box<node::NodeCommand<C, Ext>>),
227 #[command(name = "init")]
229 Init(init_cmd::InitCommand<C>),
230 #[command(name = "init-state")]
232 InitState(init_state::InitStateCommand<C>),
233 #[command(name = "import")]
235 Import(import::ImportCommand<C>),
236 #[command(name = "import-era")]
238 ImportEra(import_era::ImportEraCommand<C>),
239 #[command(name = "export-era")]
241 ExportEra(export_era::ExportEraCommand<C>),
242 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
244 #[command(name = "db")]
246 Db(Box<db::Command<C>>),
247 #[command(name = "download")]
249 Download(download::DownloadCommand<C>),
250 #[command(name = "stage")]
252 Stage(stage::Command<C>),
253 #[command(name = "p2p")]
255 P2P(Box<p2p::Command<C>>),
256 #[cfg(feature = "dev")]
258 #[command(name = "test-vectors")]
259 TestVectors(reth_cli_commands::test_vectors::Command),
260 #[command(name = "config")]
262 Config(config_cmd::Command),
263 #[command(name = "prune")]
265 Prune(prune::PruneCommand<C>),
266 #[command(name = "re-execute")]
268 ReExecute(re_execute::Command<C>),
269}
270
271impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
272 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
274 match self {
275 Self::Node(cmd) => cmd.chain_spec(),
276 Self::Init(cmd) => cmd.chain_spec(),
277 Self::InitState(cmd) => cmd.chain_spec(),
278 Self::Import(cmd) => cmd.chain_spec(),
279 Self::ExportEra(cmd) => cmd.chain_spec(),
280 Self::ImportEra(cmd) => cmd.chain_spec(),
281 Self::DumpGenesis(cmd) => cmd.chain_spec(),
282 Self::Db(cmd) => cmd.chain_spec(),
283 Self::Download(cmd) => cmd.chain_spec(),
284 Self::Stage(cmd) => cmd.chain_spec(),
285 Self::P2P(cmd) => cmd.chain_spec(),
286 #[cfg(feature = "dev")]
287 Self::TestVectors(_) => None,
288 Self::Config(_) => None,
289 Self::Prune(cmd) => cmd.chain_spec(),
290 Self::ReExecute(cmd) => cmd.chain_spec(),
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::chainspec::SUPPORTED_CHAINS;
299 use clap::CommandFactory;
300 use reth_chainspec::SEPOLIA;
301 use reth_node_core::args::ColorMode;
302
303 #[test]
304 fn parse_color_mode() {
305 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
306 assert_eq!(reth.logs.color, ColorMode::Always);
307 }
308
309 #[test]
313 fn test_parse_help_all_subcommands() {
314 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
315 for sub_command in reth.get_subcommands() {
316 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
317 .err()
318 .unwrap_or_else(|| {
319 panic!("Failed to parse help message {}", sub_command.get_name())
320 });
321
322 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
325 }
326 }
327
328 #[test]
331 fn parse_logs_path_node() {
332 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
333 if let Some(chain_spec) = reth.command.chain_spec() {
334 reth.logs.log_file_directory =
335 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
336 }
337 let log_dir = reth.logs.log_file_directory;
338 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
339 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
340
341 let mut iter = SUPPORTED_CHAINS.iter();
342 iter.next();
343 for chain in iter {
344 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
345 let chain =
346 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
347 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
348 let log_dir = reth.logs.log_file_directory;
349 let end = format!("reth/logs/{chain}");
350 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
351 }
352 }
353
354 #[test]
357 fn parse_logs_path_init() {
358 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
359 if let Some(chain_spec) = reth.command.chain_spec() {
360 reth.logs.log_file_directory =
361 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
362 }
363 let log_dir = reth.logs.log_file_directory;
364 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
365 println!("{log_dir:?}");
366 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
367 }
368
369 #[test]
371 fn parse_empty_logs_path() {
372 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
373 if let Some(chain_spec) = reth.command.chain_spec() {
374 reth.logs.log_file_directory =
375 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
376 }
377 let log_dir = reth.logs.log_file_directory;
378 let end = "reth/logs".to_string();
379 println!("{log_dir:?}");
380 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
381 }
382
383 #[test]
384 fn parse_env_filter_directives() {
385 let temp_dir = tempfile::tempdir().unwrap();
386
387 unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
388 let reth = Cli::try_parse_args_from([
389 "reth",
390 "init",
391 "--datadir",
392 temp_dir.path().to_str().unwrap(),
393 "--log.file.filter",
394 "debug,net=trace",
395 ])
396 .unwrap();
397 assert!(reth.run(async move |_, _| Ok(())).is_ok());
398 }
399
400 #[test]
401 fn test_rpc_module_validation() {
402 use reth_rpc_server_types::RethRpcModule;
403
404 let cli =
406 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
407
408 if let Commands::Node(command) = &cli.command {
409 if let Some(http_api) = &command.rpc.http_api {
410 let modules = http_api.to_selection();
412 assert!(modules.contains(&RethRpcModule::Eth));
413 assert!(modules.contains(&RethRpcModule::Admin));
414 assert!(modules.contains(&RethRpcModule::Debug));
415 } else {
416 panic!("Expected http.api to be set");
417 }
418 } else {
419 panic!("Expected Node command");
420 }
421
422 let cli =
424 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
425
426 if let Commands::Node(command) = &cli.command {
427 if let Some(http_api) = &command.rpc.http_api {
428 let modules = http_api.to_selection();
429 assert!(modules.contains(&RethRpcModule::Eth));
430 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
431 } else {
432 panic!("Expected http.api to be set");
433 }
434 } else {
435 panic!("Expected Node command");
436 }
437 }
438
439 #[test]
440 fn test_rpc_module_unknown_rejected() {
441 use reth_cli_runner::CliRunner;
442
443 let cli =
445 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
446
447 let runner = CliRunner::try_default_runtime().unwrap();
449 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
450
451 assert!(result.is_err());
452 let err = result.unwrap_err();
453 let err_msg = err.to_string();
454
455 assert!(
457 err_msg.contains("Unknown RPC module"),
458 "Error should mention unknown module: {}",
459 err_msg
460 );
461 assert!(
462 err_msg.contains("'unknownmodule'"),
463 "Error should mention the module name: {}",
464 err_msg
465 );
466 }
467
468 #[test]
469 fn parse_unwind_chain() {
470 let cli = Cli::try_parse_args_from([
471 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
472 ])
473 .unwrap();
474 match cli.command {
475 Commands::Stage(cmd) => match cmd.command {
476 stage::Subcommands::Unwind(cmd) => {
477 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
478 }
479 _ => panic!("Expected Unwind command"),
480 },
481 _ => panic!("Expected Stage command"),
482 };
483 }
484
485 #[test]
486 fn parse_empty_supported_chains() {
487 #[derive(Debug, Clone, Default)]
488 struct FileChainSpecParser;
489
490 impl ChainSpecParser for FileChainSpecParser {
491 type ChainSpec = ChainSpec;
492
493 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
494
495 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
496 EthereumChainSpecParser::parse(s)
497 }
498 }
499
500 let cli = Cli::<FileChainSpecParser>::try_parse_from([
501 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
502 ])
503 .unwrap();
504 match cli.command {
505 Commands::Stage(cmd) => match cmd.command {
506 stage::Subcommands::Unwind(cmd) => {
507 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
508 }
509 _ => panic!("Expected Unwind command"),
510 },
511 _ => panic!("Expected Stage command"),
512 };
513 }
514}