From 47c038201acdbf20542479a4664d311c0b0aeb0f Mon Sep 17 00:00:00 2001 From: Luca Provini Date: Wed, 10 Jul 2024 14:44:18 +0200 Subject: [PATCH] feat: parsers (#9366) --- Cargo.lock | 14 ++++ Cargo.toml | 1 + crates/cli/cli/Cargo.toml | 6 +- crates/cli/cli/src/chainspec.rs | 2 +- crates/cli/cli/src/lib.rs | 13 ++-- crates/ethereum/cli/Cargo.toml | 13 ++++ crates/ethereum/cli/src/chainspec.rs | 90 ++++++++++++++++++++++++ crates/ethereum/cli/src/lib.rs | 3 + crates/node/core/Cargo.toml | 2 +- crates/optimism/cli/Cargo.toml | 18 +++-- crates/optimism/cli/src/chainspec.rs | 100 +++++++++++++++++++++++++++ crates/optimism/cli/src/lib.rs | 2 + 12 files changed, 248 insertions(+), 16 deletions(-) create mode 100644 crates/ethereum/cli/src/chainspec.rs create mode 100644 crates/optimism/cli/src/chainspec.rs diff --git a/Cargo.lock b/Cargo.lock index 3aecbb1c2..c5b15f278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7182,6 +7182,15 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" version = "1.0.1" +dependencies = [ + "alloy-genesis", + "clap", + "eyre", + "reth-chainspec", + "reth-cli", + "serde_json", + "shellexpand", +] [[package]] name = "reth-ethereum-consensus" @@ -7910,10 +7919,13 @@ dependencies = [ name = "reth-optimism-cli" version = "1.0.1" dependencies = [ + "alloy-genesis", "alloy-primitives", "clap", "eyre", "futures-util", + "reth-chainspec", + "reth-cli", "reth-cli-commands", "reth-config", "reth-consensus", @@ -7934,6 +7946,8 @@ dependencies = [ "reth-stages-types", "reth-static-file", "reth-static-file-types", + "serde_json", + "shellexpand", "tokio", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 64cb72a53..615ace704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -467,6 +467,7 @@ paste = "1.0" url = "2.3" backon = "0.4" boyer-moore-magiclen = "0.2.16" +shellexpand = "3.0.0" # metrics metrics = "0.23.0" diff --git a/crates/cli/cli/Cargo.toml b/crates/cli/cli/Cargo.toml index 83ea9da6f..8ddd9b301 100644 --- a/crates/cli/cli/Cargo.toml +++ b/crates/cli/cli/Cargo.toml @@ -8,13 +8,17 @@ homepage.workspace = true repository.workspace = true [lints] +workspace = true [dependencies] # reth reth-cli-runner.workspace = true reth-chainspec.workspace = true -eyre.workspace = true # misc clap.workspace = true +eyre.workspace = true + + + diff --git a/crates/cli/cli/src/chainspec.rs b/crates/cli/cli/src/chainspec.rs index 4c1b4372f..97d9cf471 100644 --- a/crates/cli/cli/src/chainspec.rs +++ b/crates/cli/cli/src/chainspec.rs @@ -21,5 +21,5 @@ pub trait ChainSpecParser: TypedValueParser> + Default { /// /// This function will return an error if the input string cannot be parsed into a valid /// [`ChainSpec`]. - fn parse(&self, s: &str) -> eyre::Result>; + fn parse(s: &str) -> eyre::Result>; } diff --git a/crates/cli/cli/src/lib.rs b/crates/cli/cli/src/lib.rs index 9e078e82f..1db5ebf86 100644 --- a/crates/cli/cli/src/lib.rs +++ b/crates/cli/cli/src/lib.rs @@ -8,12 +8,11 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use clap::{Error, Parser}; +use reth_cli_runner::CliRunner; use std::{borrow::Cow, ffi::OsString}; -use reth_cli_runner::CliRunner; - -use clap::{Error, Parser}; - +/// The chainspec module defines the different chainspecs that can be used by the node. pub mod chainspec; /// Reth based node cli. @@ -32,7 +31,7 @@ pub trait RethCli: Sized { /// Parse args from iterator from [`std::env::args_os()`]. fn parse_args() -> Result where - Self: Parser + Sized, + Self: Parser, { ::try_parse_from(std::env::args_os()) } @@ -40,7 +39,7 @@ pub trait RethCli: Sized { /// Parse args from the given iterator. fn try_parse_from(itr: I) -> Result where - Self: Parser + Sized, + Self: Parser, I: IntoIterator, T: Into + Clone, { @@ -60,7 +59,7 @@ pub trait RethCli: Sized { /// Parses and executes a command. fn execute(f: F) -> Result where - Self: Parser + Sized, + Self: Parser, F: FnOnce(Self, CliRunner) -> R, { let cli = Self::parse_args()?; diff --git a/crates/ethereum/cli/Cargo.toml b/crates/ethereum/cli/Cargo.toml index 18b5f9a47..c5a7e60d5 100644 --- a/crates/ethereum/cli/Cargo.toml +++ b/crates/ethereum/cli/Cargo.toml @@ -8,3 +8,16 @@ homepage.workspace = true repository.workspace = true [lints] +workspace = true + +[dependencies] +reth-cli.workspace = true +reth-chainspec.workspace = true + +alloy-genesis.workspace = true + +eyre.workspace = true + +shellexpand.workspace = true +serde_json.workspace = true +clap = { workspace = true, features = ["derive", "env"] } \ No newline at end of file diff --git a/crates/ethereum/cli/src/chainspec.rs b/crates/ethereum/cli/src/chainspec.rs new file mode 100644 index 000000000..6269e5c8e --- /dev/null +++ b/crates/ethereum/cli/src/chainspec.rs @@ -0,0 +1,90 @@ +use alloy_genesis::Genesis; +use clap::{builder::TypedValueParser, error::Result, Arg, Command}; +use reth_chainspec::{ChainSpec, DEV, HOLESKY, MAINNET, SEPOLIA}; +use reth_cli::chainspec::ChainSpecParser; +use std::{ffi::OsStr, fs, path::PathBuf, sync::Arc}; + +/// Clap value parser for [`ChainSpec`]s. +/// +/// The value parser matches either a known chain, the path +/// to a json file, or a json formatted string in-memory. The json needs to be a Genesis struct. +fn chain_value_parser(s: &str) -> eyre::Result, eyre::Error> { + Ok(match s { + "mainnet" => MAINNET.clone(), + "sepolia" => SEPOLIA.clone(), + "holesky" => HOLESKY.clone(), + "dev" => DEV.clone(), + _ => { + // try to read json from path first + let raw = match fs::read_to_string(PathBuf::from(shellexpand::full(s)?.into_owned())) { + Ok(raw) => raw, + Err(io_err) => { + // valid json may start with "\n", but must contain "{" + if s.contains('{') { + s.to_string() + } else { + return Err(io_err.into()) // assume invalid path + } + } + }; + + // both serialized Genesis and ChainSpec structs supported + let genesis: Genesis = serde_json::from_str(&raw)?; + + Arc::new(genesis.into()) + } + }) +} + +/// Ethereum chain specification parser. +#[derive(Debug, Clone, Default)] +pub struct EthChainSpecParser; + +impl ChainSpecParser for EthChainSpecParser { + const SUPPORTED_CHAINS: &'static [&'static str] = &["mainnet", "sepolia", "holesky", "dev"]; + + fn parse(s: &str) -> eyre::Result> { + chain_value_parser(s) + } +} + +impl TypedValueParser for EthChainSpecParser { + type Value = Arc; + + fn parse_ref( + &self, + _cmd: &Command, + arg: Option<&Arg>, + value: &OsStr, + ) -> Result { + let val = + value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?; + ::parse(val).map_err(|err| { + let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned()); + let possible_values = Self::SUPPORTED_CHAINS.join(","); + let msg = format!( + "Invalid value '{val}' for {arg}: {err}.\n [possible values: {possible_values}]" + ); + clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg) + }) + } + + fn possible_values( + &self, + ) -> Option + '_>> { + let values = Self::SUPPORTED_CHAINS.iter().map(clap::builder::PossibleValue::new); + Some(Box::new(values)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_known_chain_spec() { + for &chain in EthChainSpecParser::SUPPORTED_CHAINS { + assert!(::parse(chain).is_ok()); + } + } +} diff --git a/crates/ethereum/cli/src/lib.rs b/crates/ethereum/cli/src/lib.rs index c55b2ab38..b1db7fcc8 100644 --- a/crates/ethereum/cli/src/lib.rs +++ b/crates/ethereum/cli/src/lib.rs @@ -7,3 +7,6 @@ )] #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +/// Chain specification parser. +pub mod chainspec; diff --git a/crates/node/core/Cargo.toml b/crates/node/core/Cargo.toml index 997aacc63..42673c13e 100644 --- a/crates/node/core/Cargo.toml +++ b/crates/node/core/Cargo.toml @@ -65,7 +65,7 @@ once_cell.workspace = true # io dirs-next = "2.0.0" -shellexpand = "3.0.0" +shellexpand.workspace = true serde_json.workspace = true # http/rpc diff --git a/crates/optimism/cli/Cargo.toml b/crates/optimism/cli/Cargo.toml index b5cd12c33..ffe109379 100644 --- a/crates/optimism/cli/Cargo.toml +++ b/crates/optimism/cli/Cargo.toml @@ -12,7 +12,6 @@ workspace = true [dependencies] reth-static-file-types = { workspace = true, features = ["clap"] } -clap = { workspace = true, features = ["derive", "env"] } reth-cli-commands.workspace = true reth-consensus.workspace = true reth-db = { workspace = true, features = ["mdbx"] } @@ -26,18 +25,25 @@ reth-static-file.workspace = true reth-execution-types.workspace = true reth-node-core.workspace = true reth-primitives.workspace = true - - reth-stages-types.workspace = true reth-node-events.workspace = true reth-network-p2p.workspace = true reth-errors.workspace = true - reth-config.workspace = true -alloy-primitives.workspace = true -futures-util.workspace = true reth-evm-optimism.workspace = true +reth-cli.workspace = true +reth-chainspec.workspace = true +# eth +alloy-genesis.workspace = true +alloy-primitives.workspace = true + + +# misc +shellexpand.workspace = true +serde_json.workspace = true +futures-util.workspace = true +clap = { workspace = true, features = ["derive", "env"] } tokio = { workspace = true, features = [ diff --git a/crates/optimism/cli/src/chainspec.rs b/crates/optimism/cli/src/chainspec.rs new file mode 100644 index 000000000..1d87e808d --- /dev/null +++ b/crates/optimism/cli/src/chainspec.rs @@ -0,0 +1,100 @@ +use alloy_genesis::Genesis; +use clap::{builder::TypedValueParser, error::Result, Arg, Command}; +use reth_chainspec::{ChainSpec, BASE_MAINNET, BASE_SEPOLIA, DEV, OP_MAINNET, OP_SEPOLIA}; +use reth_cli::chainspec::ChainSpecParser; +use std::{ffi::OsStr, fs, path::PathBuf, sync::Arc}; + +/// Clap value parser for [`ChainSpec`]s. +/// +/// The value parser matches either a known chain, the path +/// to a json file, or a json formatted string in-memory. The json needs to be a Genesis struct. +fn chain_value_parser(s: &str) -> eyre::Result, eyre::Error> { + Ok(match s { + "dev" => DEV.clone(), + "optimism" => OP_MAINNET.clone(), + "optimism_sepolia" | "optimism-sepolia" => OP_SEPOLIA.clone(), + "base" => BASE_MAINNET.clone(), + "base_sepolia" | "base-sepolia" => BASE_SEPOLIA.clone(), + _ => { + // try to read json from path first + let raw = match fs::read_to_string(PathBuf::from(shellexpand::full(s)?.into_owned())) { + Ok(raw) => raw, + Err(io_err) => { + // valid json may start with "\n", but must contain "{" + if s.contains('{') { + s.to_string() + } else { + return Err(io_err.into()) // assume invalid path + } + } + }; + + // both serialized Genesis and ChainSpec structs supported + let genesis: Genesis = serde_json::from_str(&raw)?; + + Arc::new(genesis.into()) + } + }) +} + +/// Optimism chain specification parser. +#[derive(Debug, Clone, Default)] +pub struct OpChainSpecParser; + +impl ChainSpecParser for OpChainSpecParser { + const SUPPORTED_CHAINS: &'static [&'static str] = &[ + "optimism", + "optimism_sepolia", + "optimism-sepolia", + "base", + "base_sepolia", + "base-sepolia", + ]; + + fn parse(s: &str) -> eyre::Result> { + chain_value_parser(s) + } +} + +impl TypedValueParser for OpChainSpecParser { + type Value = Arc; + + fn parse_ref( + &self, + _cmd: &Command, + arg: Option<&Arg>, + value: &OsStr, + ) -> Result { + let val = + value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?; + ::parse(val).map_err(|err| { + let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned()); + let possible_values = Self::SUPPORTED_CHAINS.join(", "); + clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + format!( + "Invalid value '{val}' for {arg}: {err}. [possible values: {possible_values}]" + ), + ) + }) + } + + fn possible_values( + &self, + ) -> Option + '_>> { + let values = Self::SUPPORTED_CHAINS.iter().map(clap::builder::PossibleValue::new); + Some(Box::new(values)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_known_chain_spec() { + for &chain in OpChainSpecParser::SUPPORTED_CHAINS { + assert!(::parse(chain).is_ok()); + } + } +} diff --git a/crates/optimism/cli/src/lib.rs b/crates/optimism/cli/src/lib.rs index 67d0ccd61..6becc7455 100644 --- a/crates/optimism/cli/src/lib.rs +++ b/crates/optimism/cli/src/lib.rs @@ -10,6 +10,8 @@ // The `optimism` feature must be enabled to use this crate. #![cfg(feature = "optimism")] +/// Optimism chain specification parser. +pub mod chainspec; /// Optimism CLI commands. pub mod commands; pub use commands::{import::ImportOpCommand, import_receipts::ImportReceiptsOpCommand};