diff --git a/crates/cli/src/opts/evm.rs b/crates/cli/src/opts/evm.rs index e2e22f7d343ff..1f08dc57f465f 100644 --- a/crates/cli/src/opts/evm.rs +++ b/crates/cli/src/opts/evm.rs @@ -14,6 +14,7 @@ use foundry_config::{ use serde::Serialize; use foundry_common::shell; +use crate::opts::EvmRpcOpts; /// `EvmArgs` and `EnvArgs` take the highest precedence in the Config/Figment hierarchy. /// @@ -40,31 +41,29 @@ use foundry_common::shell; #[derive(Clone, Debug, Default, Serialize, Parser)] #[command(next_help_heading = "EVM options", about = None, long_about = None)] // override doc pub struct EvmArgs { - /// Fetch state over a remote endpoint instead of starting from an empty state. - /// - /// If you want to fetch state from a specific block number, see --fork-block-number. - #[arg(long, short, visible_alias = "rpc-url", value_name = "URL")] - #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")] - pub fork_url: Option, + /// Common RPC options + #[command(flatten)] + #[serde(flatten)] + pub rpc: EvmRpcOpts, /// Fetch state from a specific block number over a remote endpoint. /// /// See --fork-url. - #[arg(long, requires = "fork_url", value_name = "BLOCK")] + #[arg(long, requires = "rpc.url", value_name = "BLOCK")] #[serde(skip_serializing_if = "Option::is_none")] pub fork_block_number: Option, /// Number of retries. /// /// See --fork-url. - #[arg(long, requires = "fork_url", value_name = "RETRIES")] + #[arg(long, requires = "rpc.url", value_name = "RETRIES")] #[serde(skip_serializing_if = "Option::is_none")] pub fork_retries: Option, /// Initial retry backoff on encountering errors. /// /// See --fork-url. - #[arg(long, requires = "fork_url", value_name = "BACKOFF")] + #[arg(long, requires = "rpc.url", value_name = "BACKOFF")] #[serde(skip_serializing_if = "Option::is_none")] pub fork_retry_backoff: Option, @@ -104,27 +103,6 @@ pub struct EvmArgs { #[serde(skip_serializing_if = "Option::is_none")] pub create2_deployer: Option
, - /// Sets the number of assumed available compute units per second for this provider - /// - /// default value: 330 - /// - /// See also --fork-url and - #[arg(long, alias = "cups", value_name = "CUPS", help_heading = "Fork config")] - #[serde(skip_serializing_if = "Option::is_none")] - pub compute_units_per_second: Option, - - /// Disables rate limiting for this node's provider. - /// - /// See also --fork-url and - #[arg( - long, - value_name = "NO_RATE_LIMITS", - help_heading = "Fork config", - visible_alias = "no-rate-limit" - )] - #[serde(skip)] - pub no_rpc_rate_limit: bool, - /// All ethereum environment related arguments #[command(flatten)] #[serde(flatten)] @@ -182,13 +160,8 @@ impl Provider for EvmArgs { dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into()); } - if self.no_rpc_rate_limit { - dict.insert("no_rpc_rate_limit".to_string(), self.no_rpc_rate_limit.into()); - } - - if let Some(fork_url) = &self.fork_url { - dict.insert("eth_rpc_url".to_string(), fork_url.clone().into()); - } + // Merge RPC options from the common structure + dict.extend(self.rpc.dict()); Ok(Map::from([(Config::selected_profile(), dict)])) } @@ -276,7 +249,7 @@ pub struct EnvArgs { impl EvmArgs { /// Ensures that fork url exists and returns its reference. pub fn ensure_fork_url(&self) -> eyre::Result<&String> { - self.fork_url.as_ref().wrap_err("Missing `--fork-url` field.") + self.rpc.url.as_ref().wrap_err("Missing `--fork-url` field.") } } @@ -308,12 +281,15 @@ mod tests { } #[test] - fn compute_units_per_second_present_when_some() { - let args = EvmArgs { compute_units_per_second: Some(1000), ..Default::default() }; + fn rpc_url_present_when_some() { + let args = EvmArgs { + rpc: EvmRpcOpts { url: Some("http://localhost:8545".to_string()), ..Default::default() }, + ..Default::default() + }; let data = args.data().expect("provider data"); let dict = data.get(&Config::selected_profile()).expect("profile dict"); - let val = dict.get("compute_units_per_second").expect("cups present"); - assert_eq!(val, &Value::from(1000u64)); + let val = dict.get("eth_rpc_url").expect("rpc url present"); + assert_eq!(val, &Value::from("http://localhost:8545")); } #[test] diff --git a/crates/cli/src/opts/mod.rs b/crates/cli/src/opts/mod.rs index b52464eae81f0..1914b39875fc8 100644 --- a/crates/cli/src/opts/mod.rs +++ b/crates/cli/src/opts/mod.rs @@ -4,6 +4,7 @@ mod dependency; mod evm; mod global; mod rpc; +mod rpc_common; mod transaction; pub use build::*; @@ -12,4 +13,5 @@ pub use dependency::*; pub use evm::*; pub use global::*; pub use rpc::*; +pub use rpc_common::*; pub use transaction::*; diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 4e9919966d1b9..b234479a2b169 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -1,4 +1,4 @@ -use crate::opts::ChainValueParser; +use crate::opts::{ChainValueParser, RpcCommonOpts}; use alloy_chains::ChainKind; use clap::Parser; use eyre::Result; @@ -18,16 +18,8 @@ const FLASHBOTS_URL: &str = "https://rpc.flashbots.net/fast"; #[derive(Clone, Debug, Default, Parser)] pub struct RpcOpts { - /// The RPC endpoint, default value is http://localhost:8545. - #[arg(short = 'r', long = "rpc-url", env = "ETH_RPC_URL")] - pub url: Option, - - /// Allow insecure RPC connections (accept invalid HTTPS certificates). - /// - /// When the provider's inner runtime transport variant is HTTP, this configures the reqwest - /// client to accept invalid certificates. - #[arg(short = 'k', long = "insecure", default_value = "false")] - pub accept_invalid_certs: bool, + #[command(flatten)] + pub common: RpcCommonOpts, /// Use the Flashbots RPC URL with fast mode (). /// @@ -36,30 +28,6 @@ pub struct RpcOpts { /// See: #[arg(long)] pub flashbots: bool, - - /// JWT Secret for the RPC endpoint. - /// - /// The JWT secret will be used to create a JWT for a RPC. For example, the following can be - /// used to simulate a CL `engine_forkchoiceUpdated` call: - /// - /// cast rpc --jwt-secret engine_forkchoiceUpdatedV2 - /// '["0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc", - /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc", - /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc"]' - #[arg(long, env = "ETH_RPC_JWT_SECRET")] - pub jwt_secret: Option, - - /// Timeout for the RPC request in seconds. - /// - /// The specified timeout will be used to override the default timeout for RPC requests. - /// - /// Default value: 45 - #[arg(long, env = "ETH_RPC_TIMEOUT")] - pub rpc_timeout: Option, - - /// Specify custom headers for RPC requests. - #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))] - pub rpc_headers: Option>, } impl_figment_convert_cast!(RpcOpts); @@ -77,7 +45,7 @@ impl figment::Provider for RpcOpts { impl RpcOpts { /// Returns the RPC endpoint. pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result>> { - let url = match (self.flashbots, self.url.as_deref(), config) { + let url = match (self.flashbots, self.common.url.as_deref(), config) { (true, ..) => Some(Cow::Borrowed(FLASHBOTS_URL)), (false, Some(url), _) => Some(Cow::Borrowed(url)), (false, None, Some(config)) => config.get_rpc_url().transpose()?, @@ -88,31 +56,12 @@ impl RpcOpts { /// Returns the JWT secret. pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result>> { - let jwt = match (self.jwt_secret.as_deref(), config) { - (Some(jwt), _) => Some(Cow::Borrowed(jwt)), - (None, Some(config)) => config.get_rpc_jwt_secret()?, - (None, None) => None, - }; - Ok(jwt) + self.common.jwt(config) } pub fn dict(&self) -> Dict { - let mut dict = Dict::new(); - if let Ok(Some(url)) = self.url(None) { - dict.insert("eth_rpc_url".into(), url.into_owned().into()); - } - if let Ok(Some(jwt)) = self.jwt(None) { - dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into()); - } - if let Some(rpc_timeout) = self.rpc_timeout { - dict.insert("eth_rpc_timeout".into(), rpc_timeout.into()); - } - if let Some(headers) = &self.rpc_headers { - dict.insert("eth_rpc_headers".into(), headers.clone().into()); - } - if self.accept_invalid_certs { - dict.insert("eth_rpc_accept_invalid_certs".into(), true.into()); - } + let dict = self.common.dict(); + // Flashbots URL is handled in the url() method, not in dict() dict } diff --git a/crates/cli/src/opts/rpc_common.rs b/crates/cli/src/opts/rpc_common.rs new file mode 100644 index 0000000000000..cceb57edc9781 --- /dev/null +++ b/crates/cli/src/opts/rpc_common.rs @@ -0,0 +1,157 @@ +//! Common RPC options shared between different CLI commands. + +use clap::Parser; +use foundry_config::{ + figment::{ + self, Metadata, Profile, + value::{Dict, Map}, + }, + Config, +}; +use serde::Serialize; + +/// Common RPC-related options that can be shared across different CLI commands. +#[derive(Clone, Debug, Default, Serialize, Parser)] +pub struct RpcCommonOpts { + /// The RPC endpoint URL. + #[arg(long, short, visible_alias = "rpc-url", value_name = "URL")] + #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")] + pub url: Option, + + /// Allow insecure RPC connections (accept invalid HTTPS certificates). + #[arg(short = 'k', long = "insecure", default_value = "false")] + pub accept_invalid_certs: bool, + + /// JWT Secret for the RPC endpoint. + #[arg(long, env = "ETH_RPC_JWT_SECRET")] + pub jwt_secret: Option, + + /// Timeout for the RPC request in seconds. + #[arg(long, env = "ETH_RPC_TIMEOUT")] + pub rpc_timeout: Option, + + /// Specify custom headers for RPC requests. + #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))] + pub rpc_headers: Option>, + + /// Sets the number of assumed available compute units per second for this provider. + #[arg(long, alias = "cups", value_name = "CUPS")] + #[serde(skip_serializing_if = "Option::is_none")] + pub compute_units_per_second: Option, + + /// Disables rate limiting for this node's provider. + #[arg(long, value_name = "NO_RATE_LIMITS", visible_alias = "no-rate-limit")] + #[serde(skip)] + pub no_rpc_rate_limit: bool, +} + +/// Limited RPC options for EVM-related commands that only need basic RPC functionality. +#[derive(Clone, Debug, Default, Serialize, Parser)] +pub struct EvmRpcOpts { + /// The RPC endpoint URL. + #[arg(long, short, visible_alias = "rpc-url", value_name = "URL")] + #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")] + pub url: Option, + + /// Allow insecure RPC connections (accept invalid HTTPS certificates). + #[arg(short = 'k', long = "insecure", default_value = "false")] + pub accept_invalid_certs: bool, + + /// Timeout for the RPC request in seconds. + #[arg(long, env = "ETH_RPC_TIMEOUT")] + pub rpc_timeout: Option, +} + +impl figment::Provider for RpcCommonOpts { + fn metadata(&self) -> Metadata { + Metadata::named("RpcCommonOpts") + } + + fn data(&self) -> Result, figment::Error> { + Ok(Map::from([(Config::selected_profile(), self.dict())])) + } +} + +impl figment::Provider for EvmRpcOpts { + fn metadata(&self) -> Metadata { + Metadata::named("EvmRpcOpts") + } + + fn data(&self) -> Result, figment::Error> { + Ok(Map::from([(Config::selected_profile(), self.dict())])) + } +} + +impl RpcCommonOpts { + /// Returns the RPC endpoint. + pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result>, eyre::Error> { + let url = match (self.url.as_deref(), config) { + (Some(url), _) => Some(std::borrow::Cow::Borrowed(url)), + (None, Some(config)) => config.get_rpc_url().transpose()?, + (None, None) => None, + }; + Ok(url) + } + + /// Returns the JWT secret. + pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result>, eyre::Error> { + let jwt = match (self.jwt_secret.as_deref(), config) { + (Some(jwt), _) => Some(std::borrow::Cow::Borrowed(jwt)), + (None, Some(config)) => config.get_rpc_jwt_secret()?, + (None, None) => None, + }; + Ok(jwt) + } + + pub fn dict(&self) -> Dict { + let mut dict = Dict::new(); + if let Ok(Some(url)) = self.url(None) { + dict.insert("eth_rpc_url".into(), url.into_owned().into()); + } + if let Ok(Some(jwt)) = self.jwt(None) { + dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into()); + } + if let Some(rpc_timeout) = self.rpc_timeout { + dict.insert("eth_rpc_timeout".into(), rpc_timeout.into()); + } + if let Some(headers) = &self.rpc_headers { + dict.insert("eth_rpc_headers".into(), headers.clone().into()); + } + if self.accept_invalid_certs { + dict.insert("eth_rpc_accept_invalid_certs".into(), true.into()); + } + if let Some(cups) = self.compute_units_per_second { + dict.insert("compute_units_per_second".into(), cups.into()); + } + if self.no_rpc_rate_limit { + dict.insert("no_rpc_rate_limit".into(), self.no_rpc_rate_limit.into()); + } + dict + } +} + +impl EvmRpcOpts { + /// Returns the RPC endpoint. + pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result>, eyre::Error> { + let url = match (self.url.as_deref(), config) { + (Some(url), _) => Some(std::borrow::Cow::Borrowed(url)), + (None, Some(config)) => config.get_rpc_url().transpose()?, + (None, None) => None, + }; + Ok(url) + } + + pub fn dict(&self) -> Dict { + let mut dict = Dict::new(); + if let Ok(Some(url)) = self.url(None) { + dict.insert("eth_rpc_url".into(), url.into_owned().into()); + } + if let Some(rpc_timeout) = self.rpc_timeout { + dict.insert("eth_rpc_timeout".into(), rpc_timeout.into()); + } + if self.accept_invalid_certs { + dict.insert("eth_rpc_accept_invalid_certs".into(), true.into()); + } + dict + } +}