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::{
22 args::{LogArgs, TraceArgs},
23 version::version_metadata,
24};
25use reth_node_metrics::recorder::install_prometheus_recorder;
26use reth_rpc_server_types::{DefaultRpcModuleValidator, RpcModuleValidator};
27use reth_tracing::FileWorkerGuard;
28use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
29use tracing::info;
30
31#[derive(Debug, Parser)]
35#[command(author, version =version_metadata().short_version.as_ref(), long_version = version_metadata().long_version.as_ref(), about = "Reth", long_about = None)]
36pub struct Cli<
37 C: ChainSpecParser = EthereumChainSpecParser,
38 Ext: clap::Args + fmt::Debug = NoArgs,
39 Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
40> {
41 #[command(subcommand)]
43 pub command: Commands<C, Ext>,
44
45 #[command(flatten)]
47 pub logs: LogArgs,
48
49 #[command(flatten)]
51 pub traces: TraceArgs,
52
53 #[arg(skip)]
55 pub _phantom: PhantomData<Rpc>,
56}
57
58impl Cli {
59 pub fn parse_args() -> Self {
61 Self::parse()
62 }
63
64 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
66 where
67 I: IntoIterator<Item = T>,
68 T: Into<OsString> + Clone,
69 {
70 Self::try_parse_from(itr)
71 }
72}
73
74impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> Cli<C, Ext, Rpc> {
75 pub fn configure(self) -> CliApp<C, Ext, Rpc>
80 where
81 C: ChainSpecParser<ChainSpec = ChainSpec>,
82 {
83 CliApp::new(self)
84 }
85
86 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
129 where
130 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
131 Fut: Future<Output = eyre::Result<()>>,
132 C: ChainSpecParser<ChainSpec = ChainSpec>,
133 {
134 self.with_runner(CliRunner::try_default_runtime()?, launcher)
135 }
136
137 pub fn run_with_components<N>(
144 self,
145 components: impl CliComponentsBuilder<N>,
146 launcher: impl AsyncFnOnce(
147 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
148 Ext,
149 ) -> eyre::Result<()>,
150 ) -> eyre::Result<()>
151 where
152 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
153 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
154 {
155 self.with_runner_and_components(CliRunner::try_default_runtime()?, components, launcher)
156 }
157
158 pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
178 where
179 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
180 Fut: Future<Output = eyre::Result<()>>,
181 C: ChainSpecParser<ChainSpec = ChainSpec>,
182 {
183 let mut app = self.configure();
184 app.set_runner(runner);
185 app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
186 }
187
188 pub fn with_runner_and_components<N>(
191 mut self,
192 runner: CliRunner,
193 components: impl CliComponentsBuilder<N>,
194 launcher: impl AsyncFnOnce(
195 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
196 Ext,
197 ) -> eyre::Result<()>,
198 ) -> eyre::Result<()>
199 where
200 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
201 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
202 {
203 if let Some(chain_spec) = self.command.chain_spec() {
205 self.logs.log_file_directory =
206 self.logs.log_file_directory.join(chain_spec.chain().to_string());
207 }
208 let _guard = self.init_tracing()?;
209 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
210
211 let _ = install_prometheus_recorder();
213
214 run_commands_with::<C, Ext, Rpc, N>(self, runner, components, launcher)
216 }
217
218 pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
224 let layers = reth_tracing::Layers::new();
225
226 let guard = self.logs.init_tracing_with_layers(layers)?;
227 Ok(guard)
228 }
229}
230
231#[derive(Debug, Subcommand)]
233pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
234 #[command(name = "node")]
236 Node(Box<node::NodeCommand<C, Ext>>),
237 #[command(name = "init")]
239 Init(init_cmd::InitCommand<C>),
240 #[command(name = "init-state")]
242 InitState(init_state::InitStateCommand<C>),
243 #[command(name = "import")]
245 Import(import::ImportCommand<C>),
246 #[command(name = "import-era")]
248 ImportEra(import_era::ImportEraCommand<C>),
249 #[command(name = "export-era")]
251 ExportEra(export_era::ExportEraCommand<C>),
252 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
254 #[command(name = "db")]
256 Db(Box<db::Command<C>>),
257 #[command(name = "download")]
259 Download(download::DownloadCommand<C>),
260 #[command(name = "stage")]
262 Stage(stage::Command<C>),
263 #[command(name = "p2p")]
265 P2P(Box<p2p::Command<C>>),
266 #[cfg(feature = "dev")]
268 #[command(name = "test-vectors")]
269 TestVectors(reth_cli_commands::test_vectors::Command),
270 #[command(name = "config")]
272 Config(config_cmd::Command),
273 #[command(name = "prune")]
275 Prune(prune::PruneCommand<C>),
276 #[command(name = "re-execute")]
278 ReExecute(re_execute::Command<C>),
279}
280
281impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
282 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
284 match self {
285 Self::Node(cmd) => cmd.chain_spec(),
286 Self::Init(cmd) => cmd.chain_spec(),
287 Self::InitState(cmd) => cmd.chain_spec(),
288 Self::Import(cmd) => cmd.chain_spec(),
289 Self::ExportEra(cmd) => cmd.chain_spec(),
290 Self::ImportEra(cmd) => cmd.chain_spec(),
291 Self::DumpGenesis(cmd) => cmd.chain_spec(),
292 Self::Db(cmd) => cmd.chain_spec(),
293 Self::Download(cmd) => cmd.chain_spec(),
294 Self::Stage(cmd) => cmd.chain_spec(),
295 Self::P2P(cmd) => cmd.chain_spec(),
296 #[cfg(feature = "dev")]
297 Self::TestVectors(_) => None,
298 Self::Config(_) => None,
299 Self::Prune(cmd) => cmd.chain_spec(),
300 Self::ReExecute(cmd) => cmd.chain_spec(),
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::chainspec::SUPPORTED_CHAINS;
309 use clap::CommandFactory;
310 use reth_chainspec::SEPOLIA;
311 use reth_node_core::args::ColorMode;
312
313 #[test]
314 fn parse_color_mode() {
315 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
316 assert_eq!(reth.logs.color, ColorMode::Always);
317 }
318
319 #[test]
323 fn test_parse_help_all_subcommands() {
324 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
325 for sub_command in reth.get_subcommands() {
326 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
327 .err()
328 .unwrap_or_else(|| {
329 panic!("Failed to parse help message {}", sub_command.get_name())
330 });
331
332 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
335 }
336 }
337
338 #[test]
341 fn parse_logs_path_node() {
342 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
343 if let Some(chain_spec) = reth.command.chain_spec() {
344 reth.logs.log_file_directory =
345 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
346 }
347 let log_dir = reth.logs.log_file_directory;
348 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
349 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
350
351 let mut iter = SUPPORTED_CHAINS.iter();
352 iter.next();
353 for chain in iter {
354 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
355 let chain =
356 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
357 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
358 let log_dir = reth.logs.log_file_directory;
359 let end = format!("reth/logs/{chain}");
360 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
361 }
362 }
363
364 #[test]
367 fn parse_logs_path_init() {
368 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
369 if let Some(chain_spec) = reth.command.chain_spec() {
370 reth.logs.log_file_directory =
371 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
372 }
373 let log_dir = reth.logs.log_file_directory;
374 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
375 println!("{log_dir:?}");
376 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
377 }
378
379 #[test]
381 fn parse_empty_logs_path() {
382 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
383 if let Some(chain_spec) = reth.command.chain_spec() {
384 reth.logs.log_file_directory =
385 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
386 }
387 let log_dir = reth.logs.log_file_directory;
388 let end = "reth/logs".to_string();
389 println!("{log_dir:?}");
390 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
391 }
392
393 #[test]
394 fn parse_env_filter_directives() {
395 let temp_dir = tempfile::tempdir().unwrap();
396
397 unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
398 let reth = Cli::try_parse_args_from([
399 "reth",
400 "init",
401 "--datadir",
402 temp_dir.path().to_str().unwrap(),
403 "--log.file.filter",
404 "debug,net=trace",
405 ])
406 .unwrap();
407 assert!(reth.run(async move |_, _| Ok(())).is_ok());
408 }
409
410 #[test]
411 fn test_rpc_module_validation() {
412 use reth_rpc_server_types::RethRpcModule;
413
414 let cli =
416 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
417
418 if let Commands::Node(command) = &cli.command {
419 if let Some(http_api) = &command.rpc.http_api {
420 let modules = http_api.to_selection();
422 assert!(modules.contains(&RethRpcModule::Eth));
423 assert!(modules.contains(&RethRpcModule::Admin));
424 assert!(modules.contains(&RethRpcModule::Debug));
425 } else {
426 panic!("Expected http.api to be set");
427 }
428 } else {
429 panic!("Expected Node command");
430 }
431
432 let cli =
434 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
435
436 if let Commands::Node(command) = &cli.command {
437 if let Some(http_api) = &command.rpc.http_api {
438 let modules = http_api.to_selection();
439 assert!(modules.contains(&RethRpcModule::Eth));
440 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
441 } else {
442 panic!("Expected http.api to be set");
443 }
444 } else {
445 panic!("Expected Node command");
446 }
447 }
448
449 #[test]
450 fn test_rpc_module_unknown_rejected() {
451 use reth_cli_runner::CliRunner;
452
453 let cli =
455 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
456
457 let runner = CliRunner::try_default_runtime().unwrap();
459 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
460
461 assert!(result.is_err());
462 let err = result.unwrap_err();
463 let err_msg = err.to_string();
464
465 assert!(
467 err_msg.contains("Unknown RPC module"),
468 "Error should mention unknown module: {}",
469 err_msg
470 );
471 assert!(
472 err_msg.contains("'unknownmodule'"),
473 "Error should mention the module name: {}",
474 err_msg
475 );
476 }
477
478 #[test]
479 fn parse_unwind_chain() {
480 let cli = Cli::try_parse_args_from([
481 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
482 ])
483 .unwrap();
484 match cli.command {
485 Commands::Stage(cmd) => match cmd.command {
486 stage::Subcommands::Unwind(cmd) => {
487 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
488 }
489 _ => panic!("Expected Unwind command"),
490 },
491 _ => panic!("Expected Stage command"),
492 };
493 }
494
495 #[test]
496 fn parse_empty_supported_chains() {
497 #[derive(Debug, Clone, Default)]
498 struct FileChainSpecParser;
499
500 impl ChainSpecParser for FileChainSpecParser {
501 type ChainSpec = ChainSpec;
502
503 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
504
505 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
506 EthereumChainSpecParser::parse(s)
507 }
508 }
509
510 let cli = Cli::<FileChainSpecParser>::try_parse_from([
511 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
512 ])
513 .unwrap();
514 match cli.command {
515 Commands::Stage(cmd) => match cmd.command {
516 stage::Subcommands::Unwind(cmd) => {
517 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
518 }
519 _ => panic!("Expected Unwind command"),
520 },
521 _ => panic!("Expected Stage command"),
522 };
523 }
524}