diff --git a/cli/src/cast.rs b/cli/src/cast.rs index a3af979ce3aa5..aaee3ed032b8c 100644 --- a/cli/src/cast.rs +++ b/cli/src/cast.rs @@ -133,7 +133,7 @@ async fn main() -> eyre::Result<()> { } Subcommands::ToUnit { value, unit } => { let val = unwrap_or_stdin(value)?; - println!("{}", SimpleCast::to_unit(val, unit.unwrap_or_else(|| String::from("wei")))?); + println!("{}", SimpleCast::to_unit(val, unit)?); } Subcommands::ToWei { value, unit } => { let val = unwrap_or_stdin(value)?; @@ -159,7 +159,7 @@ async fn main() -> eyre::Result<()> { let provider = Provider::try_from(eth.rpc_url()?)?; let mut builder = TxBuilder::new( &provider, - eth.from.unwrap_or(Address::zero()), + eth.wallet.from.unwrap_or(Address::zero()), address, eth.chain, false, @@ -322,7 +322,7 @@ async fn main() -> eyre::Result<()> { .await?; } } - } else if let Some(from) = eth.from { + } else if let Some(from) = eth.wallet.from { if resend { nonce = Some(provider.get_transaction_count(from, None).await?); } @@ -391,8 +391,8 @@ async fn main() -> eyre::Result<()> { Subcommands::AbiEncode { sig, args } => { println!("{}", SimpleCast::abi_encode(&sig, &args)?); } - Subcommands::Index { from_type, to_type, from_value, slot_number } => { - let encoded = SimpleCast::index(&from_type, &to_type, &from_value, &slot_number)?; + Subcommands::Index { key_type, value_type, key, slot_number } => { + let encoded = SimpleCast::index(&key_type, &value_type, &key, &slot_number)?; println!("{}", encoded); } Subcommands::FourByte { selector } => { @@ -669,7 +669,6 @@ async fn main() -> eyre::Result<()> { // TODO: Figure out better way to get wallet only. let wallet = EthereumOpts { wallet, - from: None, rpc_url: Some("http://localhost:8545".to_string()), flashbots: false, chain: Chain::Mainnet, @@ -690,7 +689,6 @@ async fn main() -> eyre::Result<()> { // TODO: Figure out better way to get wallet only. let wallet = EthereumOpts { wallet, - from: None, rpc_url: Some("http://localhost:8545".to_string()), flashbots: false, chain: Chain::Mainnet, diff --git a/cli/src/cmd/cast/call.rs b/cli/src/cmd/cast/call.rs index 3b5cc81cdb36d..28f72973f8f14 100644 --- a/cli/src/cmd/cast/call.rs +++ b/cli/src/cmd/cast/call.rs @@ -49,7 +49,7 @@ impl figment::Provider for CallArgs { dict.insert("eth_rpc_url".to_string(), Value::from(rpc_url.to_string())); } - if let Some(from) = self.eth.from { + if let Some(from) = self.eth.wallet.from { dict.insert("sender".to_string(), Value::from(format!("{:?}", from))); } diff --git a/cli/src/cmd/forge/bind.rs b/cli/src/cmd/forge/bind.rs index 4956d46025d3a..b5df2001023bb 100644 --- a/cli/src/cmd/forge/bind.rs +++ b/cli/src/cmd/forge/bind.rs @@ -40,7 +40,7 @@ pub struct BindArgs { #[clap( long = "crate-name", - help = "The name of the rust crate to generate. This should be a valid crates.io crate name. However, it is not currently validated by this command.", + help = "The name of the Rust crate to generate. This should be a valid crates.io crate name. However, it is not currently validated by this command.", default_value = DEFAULT_CRATE_NAME, )] #[serde(skip)] @@ -48,7 +48,7 @@ pub struct BindArgs { #[clap( long = "crate-version", - help = "The version of the rust crate to generate. This should be a standard semver version string. However, it is not currently validated by this command.", + help = "The version of the Rust crate to generate. This should be a standard semver version string. However, it is not currently validated by this command.", default_value = DEFAULT_CRATE_VERSION, )] #[serde(skip)] @@ -59,7 +59,7 @@ pub struct BindArgs { #[clap( long = "overwrite", - help = "Overwrite existing generated bindings. If set to false, the command will check that the bindings are correct, and then exit. If set to true, it will instead delete and overwrite the bindings." + help = "Overwrite existing generated bindings. By default, the command will check that the bindings are correct, and then exit. If --overwrite is passed, it will instead delete and overwrite the bindings." )] #[serde(skip)] overwrite: bool, diff --git a/cli/src/cmd/forge/build.rs b/cli/src/cmd/forge/build.rs index 60dd56e803e27..b8050246bf5d9 100644 --- a/cli/src/cmd/forge/build.rs +++ b/cli/src/cmd/forge/build.rs @@ -17,14 +17,14 @@ use foundry_config::{ value::{Dict, Map, Value}, Figment, Metadata, Profile, Provider, }, - find_project_root_path, remappings_from_env_var, Config, + find_project_root_path, impl_figment_convert, remappings_from_env_var, Config, }; use serde::Serialize; use watchexec::config::{InitConfig, RuntimeConfig}; // Loads project's figment and merges the build cli arguments into it -impl<'a> From<&'a BuildArgs> for Figment { - fn from(args: &'a BuildArgs) -> Self { +impl<'a> From<&'a CoreBuildArgs> for Figment { + fn from(args: &'a CoreBuildArgs) -> Self { let figment = if let Some(ref config_path) = args.config_path { if !config_path.exists() { panic!("error: config-path `{}` does not exist", config_path.display()) @@ -35,11 +35,11 @@ impl<'a> From<&'a BuildArgs> for Figment { let config_path = canonicalized(config_path); Config::figment_with_root(config_path.parent().unwrap()) } else { - Config::figment_with_root(args.project_root()) + Config::figment_with_root(args.project_paths.project_root()) }; // remappings should stack - let mut remappings = args.get_remappings(); + let mut remappings = args.project_paths.get_remappings(); remappings .extend(figment.extract_inner::>("remappings").unwrap_or_default()); remappings.sort_by(|a, b| a.name.cmp(&b.name)); @@ -48,169 +48,201 @@ impl<'a> From<&'a BuildArgs> for Figment { } } -impl<'a> From<&'a BuildArgs> for Config { - fn from(args: &'a BuildArgs) -> Self { +impl<'a> From<&'a CoreBuildArgs> for Config { + fn from(args: &'a CoreBuildArgs) -> Self { let figment: Figment = args.into(); let mut config = Config::from_provider(figment).sanitized(); // if `--config-path` is set we need to adjust the config's root path to the actual root // path for the project, otherwise it will the parent dir of the `--config-path` if args.config_path.is_some() { - config.__root = args.project_root().into(); + config.__root = args.project_paths.project_root().into(); } config } } -// All `forge build` related arguments -// -// CLI arguments take the highest precedence in the Config/Figment hierarchy. -// In order to override them in the foundry `Config` they need to be merged into an existing -// `figment::Provider`, like `foundry_config::Config` is. -// -// # Example -// -// ```ignore -// use foundry_config::Config; -// # fn t(args: BuildArgs) { -// let config = Config::from(&args); -// # } -// ``` -// -// `BuildArgs` implements `figment::Provider` in which all config related fields are serialized and -// then merged into an existing `Config`, effectively overwriting them. -// -// Some arguments are marked as `#[serde(skip)]` and require manual processing in -// `figment::Provider` implementation #[derive(Debug, Clone, Parser, Serialize)] -pub struct BuildArgs { +pub struct CoreBuildArgs { #[clap( - help = "the project's root path. By default, this is the root directory of the current Git repository or the current working directory if it is not part of a Git repository", - long, - value_hint = ValueHint::DirPath - )] - #[serde(skip)] - pub root: Option, - - #[clap( - env = "DAPP_SRC", - help = "the directory relative to the root under which the smart contracts are", - long, - short, - value_hint = ValueHint::DirPath + help_heading = "CACHE OPTIONS", + help = "Clear the cache and artifacts folder and recompile.", + long )] - #[serde(rename = "src", skip_serializing_if = "Option::is_none")] - pub contracts: Option, - - #[clap(help = "the remappings", long, short)] #[serde(skip)] - pub remappings: Vec, - - #[clap(help = "the env var that holds remappings", long = "remappings-env")] - #[serde(skip)] - pub remappings_env: Option, - - #[clap( - help = "the path where cached compiled contracts are stored", - long = "cache-path", - value_hint = ValueHint::DirPath - )] - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_path: Option, + pub force: bool, #[clap( - help = "the paths where your libraries are installed", + help_heading = "LINKER OPTIONS", + help = "Set pre-linked libraries.", long, - value_hint = ValueHint::DirPath + env = "DAPP_LIBRARIES" )] - #[serde(rename = "libs", skip_serializing_if = "Vec::is_empty")] - pub lib_paths: Vec, - - #[clap( - help = "path to where the contract artifacts are stored", - long = "out", - short, - value_hint = ValueHint::DirPath - )] - #[serde(rename = "out", skip_serializing_if = "Option::is_none")] - pub out_path: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub libraries: Vec, - #[clap(flatten)] + #[clap(flatten, next_help_heading = "COMPILER OPTIONS")] #[serde(flatten)] pub compiler: CompilerArgs, - #[clap(help = "print compiled contract names", long = "names")] - #[serde(skip)] - pub names: bool, - - #[clap(help = "print compiled contract sizes", long = "sizes")] - #[serde(skip)] - pub sizes: bool, - - #[clap(help = "ignore warnings with specific error codes", long)] + #[clap(help_heading = "COMPILER OPTIONS", help = "Ignore solc warnings by error code.", long)] #[serde(skip_serializing_if = "Vec::is_empty")] pub ignored_error_codes: Vec, - #[clap( - help = "if set to true, skips auto-detecting solc and uses what is in the user's $PATH ", - long - )] + #[clap(help_heading = "COMPILER OPTIONS", help = "Do not auto-detect solc.", long)] #[serde(skip)] pub no_auto_detect: bool, - #[clap( - help = "specify the solc version or path to a local solc to run with.\ - This accepts values of the form `x.y.z`, `solc:x.y.z` or `path/to/existing/solc`", - value_name = "use", - long = "use" - )] + /// Specify the solc version, or a path to a local solc, to build with. + /// + /// Valid values are in the format `x.y.z`, `solc:x.y.z` or `path/to/solc`. + #[clap(help_heading = "COMPILER OPTIONS", value_name = "use", long = "use")] #[serde(skip)] pub use_solc: Option, #[clap( - help = "if set to true, runs without accessing the network (missing solc versions will not be installed)", + help_heading = "COMPILER OPTIONS", + help = "Do not access the network.", + long_help = "Do not access the network. Missing solc versions will not be installed.", long )] #[serde(skip)] pub offline: bool, #[clap( - help = "force recompilation of the project, deletes the cache and artifacts folders", + help_heading = "COMPILER OPTIONS", + help = "Use the Yul intermediate representation compilation pipeline.", long )] #[serde(skip)] - pub force: bool, + pub via_ir: bool, + + #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] + #[serde(flatten)] + pub project_paths: ProjectPathsArgs, #[clap( - help = "uses hardhat style project layout. This a convenience flag and is the same as `--contracts contracts --lib-paths node_modules`", - long, - conflicts_with = "contracts", - alias = "hh" + help_heading = "PROJECT OPTIONS", + help = "The path to the contract artifacts folder.", + long = "out", + short, + value_hint = ValueHint::DirPath + )] + #[serde(rename = "out", skip_serializing_if = "Option::is_none")] + pub out_path: Option, + + #[clap( + help_heading = "PROJECT OPTIONS", + help = "Path to the config file.", + long = "config-path", + value_hint = ValueHint::FilePath )] #[serde(skip)] - pub hardhat: bool, + pub config_path: Option, +} - #[clap(help = "add linked libraries", long, env = "DAPP_LIBRARIES")] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub libraries: Vec, +impl CoreBuildArgs { + /// Returns the `Project` for the current workspace + /// + /// This loads the `foundry_config::Config` for the current workspace (see + /// [`utils::find_project_root_path`] and merges the cli `BuildArgs` into it before returning + /// [`foundry_config::Config::project()`] + pub fn project(&self) -> eyre::Result { + let config: Config = self.into(); + Ok(config.project()?) + } + + /// Returns the remappings to add to the config + #[deprecated(note = "Use ProjectPathsArgs::get_remappings() instead")] + pub fn get_remappings(&self) -> Vec { + self.project_paths.get_remappings() + } +} + +impl Provider for CoreBuildArgs { + fn metadata(&self) -> Metadata { + Metadata::named("Core Build Args Provider") + } + fn data(&self) -> Result, figment::Error> { + let value = Value::serialize(self)?; + let error = InvalidType(value.to_actual(), "map".into()); + let mut dict = value.into_dict().ok_or(error)?; + + if self.no_auto_detect { + dict.insert("auto_detect_solc".to_string(), false.into()); + } + + if let Some(ref solc) = self.use_solc { + dict.insert("solc".to_string(), solc.trim_start_matches("solc:").into()); + } + + if self.offline { + dict.insert("offline".to_string(), true.into()); + } + + if self.via_ir { + dict.insert("via_ir".to_string(), true.into()); + } + + if self.force { + dict.insert("force".to_string(), self.force.into()); + } + + if self.compiler.optimize { + dict.insert("optimizer".to_string(), self.compiler.optimize.into()); + } + + if let Some(ref extra) = self.compiler.extra_output { + let selection: Vec<_> = extra.iter().map(|s| s.to_string()).collect(); + dict.insert("extra_output".to_string(), selection.into()); + } + + if let Some(ref extra) = self.compiler.extra_output_files { + let selection: Vec<_> = extra.iter().map(|s| s.to_string()).collect(); + dict.insert("extra_output_files".to_string(), selection.into()); + } + + Ok(Map::from([(Config::selected_profile(), dict)])) + } +} + +// All `forge build` related arguments +// +// CLI arguments take the highest precedence in the Config/Figment hierarchy. +// In order to override them in the foundry `Config` they need to be merged into an existing +// `figment::Provider`, like `foundry_config::Config` is. +// +// # Example +// +// ```ignore +// use foundry_config::Config; +// # fn t(args: BuildArgs) { +// let config = Config::from(&args); +// # } +// ``` +// +// `BuildArgs` implements `figment::Provider` in which all config related fields are serialized and +// then merged into an existing `Config`, effectively overwriting them. +// +// Some arguments are marked as `#[serde(skip)]` and require manual processing in +// `figment::Provider` implementation +#[derive(Debug, Clone, Parser, Serialize)] +pub struct BuildArgs { #[clap(flatten)] + #[serde(flatten)] + pub args: CoreBuildArgs, + + #[clap(help = "Print compiled contract names.", long = "names")] #[serde(skip)] - pub watch: WatchArgs, + pub names: bool, - #[clap( - help = "if set to true, changes compilation pipeline to go through the Yul intermediate representation.", - long - )] + #[clap(help = "Print compiled contract sizes.", long = "sizes")] #[serde(skip)] - pub via_ir: bool, + pub sizes: bool, - #[clap( - help = "path to the foundry.toml", - long = "config-path", - value_hint = ValueHint::FilePath - )] + #[clap(flatten, next_help_heading = "WATCH OPTIONS")] #[serde(skip)] - pub config_path: Option, + pub watch: WatchArgs, } impl Cmd for BuildArgs { @@ -228,15 +260,7 @@ impl BuildArgs { /// [`utils::find_project_root_path`] and merges the cli `BuildArgs` into it before returning /// [`foundry_config::Config::project()`] pub fn project(&self) -> eyre::Result { - let config: Config = self.into(); - Ok(config.project()?) - } - - /// Returns the root directory to use for configuring the [Project] - /// - /// This will be the `--root` argument if provided, otherwise see [find_project_root_path()] - fn project_root(&self) -> PathBuf { - self.root.clone().unwrap_or_else(|| find_project_root_path().unwrap()) + self.args.project() } /// Returns whether `BuildArgs` was configured with `--watch` @@ -250,6 +274,96 @@ impl BuildArgs { // use the path arguments or if none where provided the `src` dir self.watch.watchexec_config(|| Config::from(self).src) } +} + +// Make this args a `figment::Provider` so that it can be merged into the `Config` +impl Provider for BuildArgs { + fn metadata(&self) -> Metadata { + Metadata::named("Build Args Provider") + } + + fn data(&self) -> Result, figment::Error> { + let value = Value::serialize(self)?; + let error = InvalidType(value.to_actual(), "map".into()); + let mut dict = value.into_dict().ok_or(error)?; + + if self.names { + dict.insert("names".to_string(), true.into()); + } + + if self.sizes { + dict.insert("sizes".to_string(), true.into()); + } + + Ok(Map::from([(Config::selected_profile(), dict)])) + } +} + +impl_figment_convert!(BuildArgs, args); + +#[derive(Debug, Clone, Parser, Serialize)] +pub struct ProjectPathsArgs { + #[clap( + help = "The project's root path.", + long_help = "The project's root path. By default, this is the root directory of the current Git repository, or the current working directory.", + long, + value_hint = ValueHint::DirPath + )] + #[serde(skip)] + pub root: Option, + + #[clap( + env = "DAPP_SRC", + help = "The contracts source directory.", + long, + short, + value_hint = ValueHint::DirPath + )] + #[serde(rename = "src", skip_serializing_if = "Option::is_none")] + pub contracts: Option, + + #[clap(help = "The project's remappings.", long, short)] + #[serde(skip)] + pub remappings: Vec, + + #[clap(help = "The project's remappings from the environment.", long = "remappings-env")] + #[serde(skip)] + pub remappings_env: Option, + + #[clap( + help = "The path to the compiler cache.", + long = "cache-path", + value_hint = ValueHint::DirPath + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_path: Option, + + #[clap( + help = "The path to the library folder.", + long, + value_hint = ValueHint::DirPath + )] + #[serde(rename = "libs", skip_serializing_if = "Vec::is_empty")] + pub lib_paths: Vec, + + #[clap( + help = "Use the Hardhat-style project layout.", + long_help = "This a convenience flag and is the same as passing `--contracts contracts --lib-paths node_modules`.", + long, + conflicts_with = "contracts", + alias = "hh" + )] + #[serde(skip)] + pub hardhat: bool, +} + +impl ProjectPathsArgs { + /// Returns the root directory to use for configuring the [Project] + /// + /// This will be the `--root` argument if provided, otherwise see [find_project_root_path()] + fn project_root(&self) -> PathBuf { + self.root.clone().unwrap_or_else(|| find_project_root_path().unwrap()) + } /// Returns the remappings to add to the config pub fn get_remappings(&self) -> Vec { @@ -264,9 +378,9 @@ impl BuildArgs { } // Make this args a `figment::Provider` so that it can be merged into the `Config` -impl Provider for BuildArgs { +impl Provider for ProjectPathsArgs { fn metadata(&self) -> Metadata { - Metadata::named("Build Args Provider") + Metadata::named("Project Paths Args Provider") } fn data(&self) -> Result, figment::Error> { @@ -276,6 +390,7 @@ impl Provider for BuildArgs { let mut libs = self.lib_paths.iter().map(|p| format!("{}", p.display())).collect::>(); + if self.hardhat { dict.insert("src".to_string(), "contracts".to_string().into()); libs.push("node_modules".to_string()); @@ -285,48 +400,8 @@ impl Provider for BuildArgs { dict.insert("libs".to_string(), libs.into()); } - if self.no_auto_detect { - dict.insert("auto_detect_solc".to_string(), false.into()); - } - - if let Some(ref solc) = self.use_solc { - dict.insert("solc".to_string(), solc.trim_start_matches("solc:").into()); - } - - if self.offline { - dict.insert("offline".to_string(), true.into()); - } - - if self.via_ir { - dict.insert("via_ir".to_string(), true.into()); - } - - if self.names { - dict.insert("names".to_string(), true.into()); - } - - if self.sizes { - dict.insert("sizes".to_string(), true.into()); - } - - if self.force { - dict.insert("force".to_string(), self.force.into()); - } - - if self.compiler.optimize { - dict.insert("optimizer".to_string(), self.compiler.optimize.into()); - } - - if let Some(ref extra) = self.compiler.extra_output { - let selection: Vec<_> = extra.iter().map(|s| s.to_string()).collect(); - dict.insert("extra_output".to_string(), selection.into()); - } - - if let Some(ref extra) = self.compiler.extra_output_files { - let selection: Vec<_> = extra.iter().map(|s| s.to_string()).collect(); - dict.insert("extra_output_files".to_string(), selection.into()); - } - Ok(Map::from([(Config::selected_profile(), dict)])) } } + +impl_figment_convert!(ProjectPathsArgs); diff --git a/cli/src/cmd/forge/create.rs b/cli/src/cmd/forge/create.rs index 23b5801f16ea4..aa4d590d5a696 100644 --- a/cli/src/cmd/forge/create.rs +++ b/cli/src/cmd/forge/create.rs @@ -1,31 +1,30 @@ //! Create command - use crate::{ - cmd::{forge::build::BuildArgs, Cmd}, - opts::{EthereumOpts, WalletType}, - utils::parse_u256, + cmd::{forge::build::CoreBuildArgs, Cmd}, + compile, + opts::{forge::ContractInfo, EthereumOpts, WalletType}, + utils::{parse_ether_value, parse_u256}, }; +use clap::{Parser, ValueHint}; use ethers::{ abi::{Abi, Constructor, Token}, prelude::{artifacts::BytecodeObject, ContractFactory, Http, Middleware, Provider}, types::{transaction::eip2718::TypedTransaction, Chain, U256}, }; - use eyre::{Context, Result}; use foundry_utils::parse_tokens; -use std::fs; - -use crate::{compile, opts::forge::ContractInfo}; -use clap::{Parser, ValueHint}; use serde_json::json; -use std::{path::PathBuf, sync::Arc}; +use std::{fs, path::PathBuf, sync::Arc}; #[derive(Debug, Clone, Parser)] pub struct CreateArgs { + #[clap(help = "The contract identifier in the form `:`.")] + contract: ContractInfo, + #[clap( long, multiple_values = true, - help = "constructor args calldata arguments", + help = "The constructor arguments.", name = "constructor_args", conflicts_with = "constructor_args_path" )] @@ -33,41 +32,70 @@ pub struct CreateArgs { #[clap( long, - help = "path to a file containing the constructor args", + help = "The path to a file containing the constructor arguments.", value_hint = ValueHint::FilePath, name = "constructor_args_path", conflicts_with = "constructor_args", )] constructor_args_path: Option, - #[clap(flatten)] - opts: BuildArgs, - - #[clap(flatten)] - eth: EthereumOpts, - - #[clap(help = "contract source info `:` or ``")] - contract: ContractInfo, - #[clap( long, - help = "use legacy transactions instead of EIP1559 ones. this is auto-enabled for common networks without EIP1559" + help_heading = "TRANSACTION OPTIONS", + help = "Send a legacy transaction instead of an EIP1559 transaction.", + long_help = r#"Send a legacy transaction instead of an EIP1559 transaction. + +This is automatically enabled for common networks without EIP1559."# )] legacy: bool, - #[clap(long = "gas-price", help = "gas price for legacy txs or maxFeePerGas for EIP1559 txs", env = "ETH_GAS_PRICE", parse(try_from_str = parse_u256))] + #[clap( + long = "gas-price", + help_heading = "TRANSACTION OPTIONS", + help = "Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.", + env = "ETH_GAS_PRICE", + parse(try_from_str = parse_ether_value) + )] gas_price: Option, - #[clap(long = "gas-limit", help = "maximum amount of gas that can be consumed for txs", env = "ETH_GAS_LIMIT", parse(try_from_str = parse_u256))] + #[clap( + long = "gas-limit", + help_heading = "TRANSACTION OPTIONS", + help = "Gas limit for the transaction.", + env = "ETH_GAS_LIMIT", + parse(try_from_str = parse_u256) + )] gas_limit: Option, - #[clap(long = "priority-fee", help = "gas priority fee for EIP1559 txs", env = "ETH_GAS_PRIORITY_FEE", parse(try_from_str = parse_u256))] + #[clap( + long = "priority-fee", + help_heading = "TRANSACTION OPTIONS", + help = "Gas priority fee for EIP1559 transactions.", + env = "ETH_GAS_PRIORITY_FEE", parse(try_from_str = parse_ether_value) + )] priority_fee: Option, + #[clap( + long, + help_heading = "TRANSACTION OPTIONS", + help = "Ether to send in the transaction.", + long_help = r#"Ether to send in the transaction, either specified in wei, or as a string with a unit type. - #[clap(long = "value", help = "value to send with the contract creation tx", env = "ETH_VALUE", parse(try_from_str = parse_u256))] +Examples: 1ether, 10gwei, 0.01ether"#, + parse(try_from_str = parse_ether_value) + )] value: Option, - #[clap(long = "json", help = "print the deployment information as json")] + #[clap(flatten, next_help_heading = "BUILD OPTIONS")] + opts: CoreBuildArgs, + + #[clap(flatten, next_help_heading = "ETHEREUM OPTIONS")] + eth: EthereumOpts, + + #[clap( + long = "json", + help_heading = "DISPLAY OPTIONS", + help = "Print the deployment information as JSON." + )] json: bool, } @@ -81,7 +109,7 @@ impl Cmd for CreateArgs { // Supress compile stdout messages when printing json output compile::suppress_compile(&project)? } else { - compile::compile(&project, self.opts.names, self.opts.sizes)? + compile::compile(&project, false, false)? }; // Get ABI and BIN @@ -201,10 +229,14 @@ impl CreateArgs { let (deployed_contract, receipt) = deployer.send_with_receipt().await?; if self.json { - let output = json!({"deployer": deployer_address, "deployedTo": deployed_contract.address(), "transactionHash": receipt.transaction_hash}); - println!("{}", output); + let output = json!({ + "deployer": deployer_address, + "deployedTo": deployed_contract.address(), + "transactionHash": receipt.transaction_hash + }); + println!("{output}"); } else { - println!("Deployer: {:?}", deployer_address); + println!("Deployer: {deployer_address:?}"); println!("Deployed to: {:?}", deployed_contract.address()); println!("Transaction hash: {:?}", receipt.transaction_hash); } diff --git a/cli/src/cmd/forge/flatten.rs b/cli/src/cmd/forge/flatten.rs index 07d4a73757391..8311968ec9ba4 100644 --- a/cli/src/cmd/forge/flatten.rs +++ b/cli/src/cmd/forge/flatten.rs @@ -1,94 +1,43 @@ -use std::path::PathBuf; - -use ethers::solc::remappings::Remapping; - -use crate::cmd::{forge::build::BuildArgs, Cmd}; +use super::build::{CoreBuildArgs, ProjectPathsArgs}; +use crate::cmd::Cmd; use clap::{Parser, ValueHint}; use foundry_config::Config; +use std::path::PathBuf; #[derive(Debug, Clone, Parser)] -pub struct CoreFlattenArgs { - #[clap( - help = "the project's root path. By default, this is the root directory of the current Git repository or the current working directory if it is not part of a Git repository", - long, - value_hint = ValueHint::DirPath - )] - pub root: Option, - - #[clap( - env = "DAPP_SRC", - help = "the directory relative to the root under which the smart contracts are", - long, - short, - value_hint = ValueHint::DirPath - )] - pub contracts: Option, - - #[clap(help = "the remappings", long, short)] - pub remappings: Vec, - #[clap(long = "remappings-env", env = "DAPP_REMAPPINGS")] - pub remappings_env: Option, - - #[clap( - help = "the path where cached compiled contracts are stored", - long = "cache-path", - value_hint = ValueHint::DirPath - )] - pub cache_path: Option, - - #[clap( - help = "the paths where your libraries are installed", - long, - value_hint = ValueHint::DirPath - )] - pub lib_paths: Vec, +pub struct FlattenArgs { + #[clap(help = "The path to the contract to flatten.", value_hint = ValueHint::FilePath)] + pub target_path: PathBuf, #[clap( - help = "uses hardhat style project layout. This a convenience flag and is the same as `--contracts contracts --lib-paths node_modules`", long, - conflicts_with = "contracts", - alias = "hh" + short, + help = "The path to output the flattened contract.", + long_help = "The path to output the flattened contract. If not specified, the flattened contract will be output to stdout.", + value_hint = ValueHint::FilePath )] - pub hardhat: bool, -} - -#[derive(Debug, Clone, Parser)] -pub struct FlattenArgs { - #[clap(help = "the path to the contract to flatten", value_hint = ValueHint::FilePath)] - pub target_path: PathBuf, - - #[clap(long, short, help = "output path for the flattened contract", value_hint = ValueHint::FilePath)] pub output: Option, - #[clap(flatten)] - core_flatten_args: CoreFlattenArgs, + #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] + project_paths: ProjectPathsArgs, } impl Cmd for FlattenArgs { type Output = (); fn run(self) -> eyre::Result { - let FlattenArgs { target_path, output, core_flatten_args } = self; + let FlattenArgs { target_path, output, project_paths } = self; // flatten is a subset of `BuildArgs` so we can reuse that to get the config - let build_args = BuildArgs { - root: core_flatten_args.root, - contracts: core_flatten_args.contracts, - remappings: core_flatten_args.remappings, - remappings_env: core_flatten_args.remappings_env, - cache_path: core_flatten_args.cache_path, - lib_paths: core_flatten_args.lib_paths, - out_path: None, + let build_args = CoreBuildArgs { + project_paths, + out_path: Default::default(), compiler: Default::default(), - names: false, - sizes: false, ignored_error_codes: vec![], no_auto_detect: false, use_solc: None, offline: false, force: false, - hardhat: core_flatten_args.hardhat, libraries: vec![], - watch: Default::default(), via_ir: false, config_path: None, }; @@ -99,7 +48,7 @@ impl Cmd for FlattenArgs { let target_path = dunce::canonicalize(target_path)?; let flattened = paths .flatten(&target_path) - .map_err(|err| eyre::Error::msg(format!("failed to flatten the file: {}", err)))?; + .map_err(|err| eyre::Error::msg(format!("Failed to flatten the file: {}", err)))?; match output { Some(output) => { diff --git a/cli/src/cmd/forge/init.rs b/cli/src/cmd/forge/init.rs index 178da95e148e7..096bf711f66e5 100644 --- a/cli/src/cmd/forge/init.rs +++ b/cli/src/cmd/forge/init.rs @@ -21,33 +21,33 @@ use std::{ #[derive(Debug, Clone, Parser)] pub struct InitArgs { #[clap( - help = "the project's root path, default being the current working directory", - value_hint = ValueHint::DirPath + help = "The root directory of the new project. Defaults to the current working directory.", + value_hint = ValueHint::DirPath )] root: Option, - #[clap(help = "optional solidity template to start from", long, short)] + #[clap(help = "The template to start from.", long, short)] template: Option, - #[clap( - help = "initialize without creating a git repository", - conflicts_with = "template", - long - )] + #[clap(help = "Do not create a git repository.", conflicts_with = "template", long)] no_git: bool, - #[clap(help = "do not create initial commit", conflicts_with = "template", long)] + #[clap(help = "Do not create an initial commit.", conflicts_with = "template", long)] no_commit: bool, - #[clap(help = "do not print messages", short, long)] + #[clap(help = "Do not print any messages.", short, long)] quiet: bool, #[clap( - help = "run without installing libs from the network", + help = "Do not install dependencies from the network.", conflicts_with = "template", long, alias = "no-deps" )] offline: bool, - #[clap(help = "force init if project dir is not empty", conflicts_with = "template", long)] + #[clap( + help = "Create the project even if the specified root directory is not empty.", + conflicts_with = "template", + long + )] force: bool, #[clap( - help = "initialize .vscode/settings.json file with solidity settings and generate a remappings.txt file.", + help = "Create a .vscode/settings.json file with Solidity settings, and generate a remappings.txt file.", conflicts_with = "template", long )] diff --git a/cli/src/cmd/forge/inspect.rs b/cli/src/cmd/forge/inspect.rs index 80681d549e25a..094ff8827ce0f 100644 --- a/cli/src/cmd/forge/inspect.rs +++ b/cli/src/cmd/forge/inspect.rs @@ -1,8 +1,6 @@ -use std::{fmt, str::FromStr}; - use crate::{ cmd::{ - forge::build::{self, BuildArgs}, + forge::build::{self, CoreBuildArgs}, Cmd, }, compile, @@ -13,6 +11,7 @@ use ethers::prelude::artifacts::output_selection::{ ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection, }; use serde_json::{to_value, Value}; +use std::{fmt, str::FromStr}; /// Contract level output selection #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -83,33 +82,33 @@ impl FromStr for ContractArtifactFields { "metadata" | "meta" => Ok(ContractArtifactFields::Metadata), "userdoc" | "userDoc" | "user-doc" => Ok(ContractArtifactFields::UserDoc), "ewasm" | "e-wasm" => Ok(ContractArtifactFields::Ewasm), - _ => Err(format!("Unknown mode: {}", s)), + _ => Err(format!("Unknown field: {}", s)), } } } #[derive(Debug, Clone, Parser)] pub struct InspectArgs { - #[clap(help = "the contract to inspect")] + #[clap(help = "The name of the contract to inspect.")] pub contract: String, - #[clap(help = "the contract artifact field to inspect")] - pub mode: ContractArtifactFields, + #[clap(help = "The contract artifact field to inspect.")] + pub field: ContractArtifactFields, /// All build arguments are supported #[clap(flatten)] - build: build::BuildArgs, + build: build::CoreBuildArgs, } impl Cmd for InspectArgs { type Output = (); fn run(self) -> eyre::Result { - let InspectArgs { contract, mode, build } = self; + let InspectArgs { contract, field, build } = self; - // Map mode to ContractOutputSelection + // Map field to ContractOutputSelection let mut cos = build.compiler.extra_output.unwrap_or_default(); - if !cos.iter().any(|&i| i.to_string() == mode.to_string()) { - match mode { + if !cos.iter().any(|&i| i.to_string() == field.to_string()) { + match field { ContractArtifactFields::Abi => cos.push(ContractOutputSelection::Abi), ContractArtifactFields::Bytecode => { /* Auto Generated */ } ContractArtifactFields::DeployedBytecode => { /* Auto Generated */ } @@ -139,14 +138,14 @@ impl Cmd for InspectArgs { } // Run Optimized? - let optimized = if let ContractArtifactFields::AssemblyOptimized = mode { + let optimized = if let ContractArtifactFields::AssemblyOptimized = field { true } else { build.compiler.optimize }; // Build modified Args - let modified_build_args = BuildArgs { + let modified_build_args = CoreBuildArgs { compiler: CompilerArgs { extra_output: Some(cos), optimize: optimized, @@ -168,7 +167,7 @@ impl Cmd for InspectArgs { })?; // Match on ContractArtifactFields and Pretty Print - match mode { + match field { ContractArtifactFields::Abi => { println!("{}", serde_json::to_string_pretty(&to_value(&artifact.abi)?)?); } diff --git a/cli/src/cmd/forge/install.rs b/cli/src/cmd/forge/install.rs index 53806b0c68e49..082a3b7d7dfa4 100644 --- a/cli/src/cmd/forge/install.rs +++ b/cli/src/cmd/forge/install.rs @@ -14,14 +14,22 @@ use std::{ /// Command to install dependencies #[derive(Debug, Clone, Parser)] pub struct InstallArgs { - #[clap( - help = "Installs one or more dependencies as git submodules (will install existing dependencies if no arguments are provided)" - )] + /// The dependencies to install. + /// + /// A dependency can be a raw URL, or the path to a GitHub repository. + /// + /// Additionally, a ref can be provided by adding @ to the dependency path. + /// + /// A ref can be: + /// - A branch: master + /// - A tag: v1.2.3 + /// - A commit: 8e8128 dependencies: Vec, #[clap(flatten)] opts: DependencyInstallOpts, #[clap( - help = "the project's root path. By default, this is the root directory of the current Git repository or the current working directory if it is not part of a Git repository", + help = "The project's root path.", + long_help = "The project's root path. By default, this is the root directory of the current Git repository, or the current working directory.", long, value_hint = ValueHint::DirPath )] @@ -40,11 +48,11 @@ impl Cmd for InstallArgs { #[derive(Debug, Clone, Copy, Default, Parser)] pub struct DependencyInstallOpts { - #[clap(help = "install without creating a submodule repository", long)] + #[clap(help = "Install without adding the dependency as a submodule.", long)] pub no_git: bool, - #[clap(help = "do not create a commit", long)] + #[clap(help = "Do not create a commit.", long)] pub no_commit: bool, - #[clap(help = "do not print messages", short, long)] + #[clap(help = "Do not print any messages.", short, long)] pub quiet: bool, } diff --git a/cli/src/cmd/forge/remappings.rs b/cli/src/cmd/forge/remappings.rs index 3b162b00828b2..e501b551b9fcf 100644 --- a/cli/src/cmd/forge/remappings.rs +++ b/cli/src/cmd/forge/remappings.rs @@ -9,17 +9,17 @@ use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Parser)] pub struct RemappingArgs { #[clap( - help = "the project's root path, default being the current working directory", + help = "The project's root path. Defaults to the current working directory.", long, value_hint = ValueHint::DirPath )] root: Option, #[clap( - help = "the paths where your libraries are installed", + help = "The path to the library folder.", long, value_hint = ValueHint::DirPath )] - lib_paths: Vec, + lib_path: Vec, } impl Cmd for RemappingArgs { @@ -29,13 +29,13 @@ impl Cmd for RemappingArgs { let root = self.root.unwrap_or_else(|| std::env::current_dir().unwrap()); let root = dunce::canonicalize(root)?; - let lib_paths = if self.lib_paths.is_empty() { + let lib_path = if self.lib_path.is_empty() { ProjectPathsConfig::find_libs(&root) } else { - self.lib_paths + self.lib_path }; let remappings: Vec<_> = - lib_paths.iter().flat_map(|lib| relative_remappings(lib, &root)).collect(); + lib_path.iter().flat_map(|lib| relative_remappings(lib, &root)).collect(); remappings.iter().for_each(|x| println!("{}", x)); Ok(()) } diff --git a/cli/src/cmd/forge/run.rs b/cli/src/cmd/forge/run.rs index d00b9e7b841d9..a92b4dc294d96 100644 --- a/cli/src/cmd/forge/run.rs +++ b/cli/src/cmd/forge/run.rs @@ -1,5 +1,5 @@ use crate::{ - cmd::{forge::build::BuildArgs, Cmd}, + cmd::{forge::build::CoreBuildArgs, Cmd}, compile, opts::evm::EvmArgs, utils, @@ -46,11 +46,11 @@ pub struct RunArgs { pub args: Vec, /// The name of the contract you want to run. - #[clap(long, short)] + #[clap(long, short, value_name = "CONTRACT_NAME")] pub target_contract: Option, /// The signature of the function you want to call in the contract, or raw calldata. - #[clap(long, short, default_value = "run()")] + #[clap(long, short, default_value = "run()", value_name = "SIGNATURE")] pub sig: String, /// Open the script in the debugger. @@ -58,7 +58,7 @@ pub struct RunArgs { pub debug: bool, #[clap(flatten, next_help_heading = "BUILD OPTIONS")] - pub opts: BuildArgs, + pub opts: CoreBuildArgs, #[clap(flatten, next_help_heading = "EVM OPTIONS")] pub evm_opts: EvmArgs, diff --git a/cli/src/cmd/forge/snapshot.rs b/cli/src/cmd/forge/snapshot.rs index 92fedf651a18f..34cd476b96858 100644 --- a/cli/src/cmd/forge/snapshot.rs +++ b/cli/src/cmd/forge/snapshot.rs @@ -1,8 +1,7 @@ //! Snapshot command - use crate::cmd::{ forge::{ - build::BuildArgs, + build::CoreBuildArgs, test, test::{custom_run, Test, TestOutcome}, }, @@ -35,7 +34,7 @@ pub static RE_BASIC_SNAPSHOT_ENTRY: Lazy = Lazy::new(|| { pub struct SnapshotArgs { /// All test arguments are supported #[clap(flatten)] - test: test::TestArgs, + pub(crate) test: test::TestArgs, /// Additional configs for test results #[clap(flatten)] @@ -78,17 +77,17 @@ pub struct SnapshotArgs { impl SnapshotArgs { /// Returns whether `SnapshotArgs` was configured with `--watch` pub fn is_watch(&self) -> bool { - self.test.build_args().is_watch() + self.test.is_watch() } /// Returns the [`watchexec::InitConfig`] and [`watchexec::RuntimeConfig`] necessary to /// bootstrap a new [`watchexe::Watchexec`] loop. pub(crate) fn watchexec_config(&self) -> eyre::Result<(InitConfig, RuntimeConfig)> { - self.test.build_args().watchexec_config() + self.test.watchexec_config() } - /// Returns the nested [`BuildArgs`] - pub fn build_args(&self) -> &BuildArgs { + /// Returns the nested [`CoreBuildArgs`] + pub fn build_args(&self) -> &CoreBuildArgs { self.test.build_args() } } diff --git a/cli/src/cmd/forge/test.rs b/cli/src/cmd/forge/test.rs index bac0dd6b81e95..55c0460d96c1a 100644 --- a/cli/src/cmd/forge/test.rs +++ b/cli/src/cmd/forge/test.rs @@ -1,7 +1,7 @@ //! Test command use crate::{ cmd::{ - forge::{build::BuildArgs, run::RunArgs}, + forge::{build::CoreBuildArgs, run::RunArgs, watch::WatchArgs}, Cmd, }, compile::ProjectCompiler, @@ -31,6 +31,7 @@ use std::{ thread, time::Duration, }; +use watchexec::config::{InitConfig, RuntimeConfig}; #[derive(Debug, Clone, Parser)] pub struct Filter { @@ -161,24 +162,27 @@ pub struct TestArgs { #[clap(long, env = "FORGE_GAS_REPORT")] gas_report: bool, - /// Force the process to exit with code 0, even if the tests fail. + /// Exit with code 0 even if a test fails. #[clap(long, env = "FORGE_ALLOW_FAILURE")] allow_failure: bool, /// Output test results in JSON format. - #[clap(long, short)] + #[clap(long, short, help_heading = "DISPLAY OPTIONS")] json: bool, #[clap(flatten, next_help_heading = "EVM OPTIONS")] evm_opts: EvmArgs, #[clap(flatten, next_help_heading = "BUILD OPTIONS")] - opts: BuildArgs, + opts: CoreBuildArgs, + + #[clap(flatten, next_help_heading = "WATCH OPTIONS")] + pub watch: WatchArgs, } impl TestArgs { - /// Returns the flattened [`BuildArgs`] - pub fn build_args(&self) -> &BuildArgs { + /// Returns the flattened [`CoreBuildArgs`] + pub fn build_args(&self) -> &CoreBuildArgs { &self.opts } @@ -195,6 +199,17 @@ impl TestArgs { let config = Config::from_provider(figment).sanitized(); Ok((config, evm_opts)) } + + /// Returns whether `BuildArgs` was configured with `--watch` + pub fn is_watch(&self) -> bool { + self.watch.watch.is_some() + } + + /// Returns the [`watchexec::InitConfig`] and [`watchexec::RuntimeConfig`] necessary to + /// bootstrap a new [`watchexe::Watchexec`] loop. + pub(crate) fn watchexec_config(&self) -> eyre::Result<(InitConfig, RuntimeConfig)> { + self.watch.watchexec_config(|| Config::from(self).src) + } } impl Cmd for TestArgs { diff --git a/cli/src/cmd/forge/tree.rs b/cli/src/cmd/forge/tree.rs index 2e7b4b540c308..1608f831ef57b 100644 --- a/cli/src/cmd/forge/tree.rs +++ b/cli/src/cmd/forge/tree.rs @@ -1,6 +1,6 @@ //! tree command -use crate::cmd::{forge::build::BuildArgs, Cmd}; +use crate::cmd::{forge::build::ProjectPathsArgs, Cmd}; use clap::Parser; use ethers::solc::Graph; use foundry_config::Config; @@ -11,13 +11,12 @@ use ethers::solc::resolver::{Charset, TreeOptions}; /// Command to display the project's dependency tree #[derive(Debug, Clone, Parser)] pub struct TreeArgs { - // TODO extract path related args from BuildArgs - #[clap(flatten)] - opts: BuildArgs, #[clap(help = "Do not de-duplicate (repeats all shared dependencies)", long)] no_dedupe: bool, #[clap(help = "Character set to use in output: utf8, ascii", default_value = "utf8", long)] charset: Charset, + #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] + opts: ProjectPathsArgs, } impl Cmd for TreeArgs { diff --git a/cli/src/cmd/forge/verify.rs b/cli/src/cmd/forge/verify.rs index b584ecbaf3757..4b01217d82ca7 100644 --- a/cli/src/cmd/forge/verify.rs +++ b/cli/src/cmd/forge/verify.rs @@ -1,9 +1,7 @@ //! Verify contract source on etherscan -use crate::{ - cmd::forge::{build::BuildArgs, flatten::CoreFlattenArgs}, - opts::forge::ContractInfo, -}; +use super::build::{CoreBuildArgs, ProjectPathsArgs}; +use crate::opts::forge::ContractInfo; use clap::Parser; use ethers::{ abi::Address, @@ -25,49 +23,49 @@ use tracing::{trace, warn}; /// Verification arguments #[derive(Debug, Clone, Parser)] pub struct VerifyArgs { - #[clap(help = "the target contract address")] + #[clap(help = "The address of the contract to verify.")] address: Address, - #[clap(help = "the contract source info `:`")] + #[clap(help = "The contract identifier in the form `:`.")] contract: ContractInfo, #[clap(long, help = "the encoded constructor arguments")] constructor_args: Option, - #[clap(long, help = "the compiler version used during build")] + #[clap(long, help = "The compiler version used to build the smart contract.")] compiler_version: String, - #[clap(alias = "optimizer-runs", long, help = "the number of optimization runs used")] + #[clap( + alias = "optimizer-runs", + long, + help = "The number of optimization runs used to build the smart contract." + )] num_of_optimizations: Option, #[clap( long, alias = "chain-id", env = "CHAIN", - help = "the name or id of chain of the network you are verifying for", + help = "The chain ID the contract is deployed to.", default_value = "mainnet" )] chain: Chain, - #[clap(help = "your etherscan api key", env = "ETHERSCAN_API_KEY")] + #[clap(help = "Your Etherscan API key.", env = "ETHERSCAN_API_KEY")] etherscan_key: String, - #[clap( - help = "flatten the source code. Make sure to use bytecodehash='ipfs'", - long = "flatten" - )] + #[clap(help = "Flatten the source code before verifying.", long = "flatten")] flatten: bool, - #[clap(flatten)] - opts: CoreFlattenArgs, - #[clap( short, long, - help = r#"usually the command will try to compile the flattened code locally first to ensure it's valid. -This flag we skip that process and send the content directly to the endpoint."# + help = "Do not compile the flattened smart contract before verifying (if --flatten is passed)." )] force: bool, + + #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] + project_paths: ProjectPathsArgs, } impl VerifyArgs { @@ -126,35 +124,16 @@ impl VerifyArgs { /// If `--flatten` is set to `true` then this will send with [`CodeFormat::SingleFile`] /// otherwise this will use the [`CodeFormat::StandardJsonInput`] fn create_verify_request(&self) -> eyre::Result { - let CoreFlattenArgs { - root, - contracts, - remappings, - remappings_env, - cache_path, - lib_paths, - hardhat, - } = self.opts.clone(); - - let build_args = BuildArgs { - root, - contracts, - remappings, - remappings_env, - cache_path, - lib_paths, - out_path: None, + let build_args = CoreBuildArgs { + project_paths: self.project_paths.clone(), + out_path: Default::default(), compiler: Default::default(), - names: false, - sizes: false, ignored_error_codes: vec![], no_auto_detect: false, use_solc: None, offline: false, force: false, - hardhat, libraries: vec![], - watch: Default::default(), via_ir: false, config_path: None, }; @@ -245,7 +224,7 @@ impl VerifyArgs { eprintln!( r#"Failed to compile the flattened code locally. This could be a bug, please inspect the outout of `forge flatten {}` and report an issue. -To skip this solc dry, have a look at the `--force` flag of this command. +To skip this solc dry, pass `--force`. "#, self.contract.path.as_ref().expect("Path is some;") ); @@ -259,19 +238,19 @@ To skip this solc dry, have a look at the `--force` flag of this command. /// Check verification status arguments #[derive(Debug, Clone, Parser)] pub struct VerifyCheckArgs { - #[clap(help = "the verification guid")] + #[clap(help = "The verification GUID.")] guid: String, #[clap( long, alias = "chain-id", env = "CHAIN", - help = "the name or id of chain of the network you are verifying for", + help = "The chain ID the contract is deployed to.", default_value = "mainnet" )] chain: Chain, - #[clap(help = "your etherscan api key", env = "ETHERSCAN_API_KEY")] + #[clap(help = "Your Etherscan API key.", env = "ETHERSCAN_API_KEY")] etherscan_key: String, } diff --git a/cli/src/cmd/forge/watch.rs b/cli/src/cmd/forge/watch.rs index 16e3a03d83aa7..0f9224173beab 100644 --- a/cli/src/cmd/forge/watch.rs +++ b/cli/src/cmd/forge/watch.rs @@ -22,9 +22,9 @@ use watchexec::{ #[derive(Debug, Clone, Parser, Default)] pub struct WatchArgs { - /// File updates debounce delay + /// File update debounce delay /// - /// During this time, incoming change events are accumulated and + /// During the delay, incoming change events are accumulated and /// only once the delay has passed, is an action taken. Note that /// this does not mean a command will be started: if --no-restart is /// given and a command is already running, the outcome of the @@ -32,28 +32,24 @@ pub struct WatchArgs { /// /// Defaults to 50ms. Parses as decimal seconds by default, but /// using an integer with the `ms` suffix may be more convenient. + /// /// When using --poll mode, you'll want a larger duration, or risk /// overloading disk I/O. - #[clap( - short = 'd', - long = "delay", - forbid_empty_values = true, - help = "File updates debounce delay. Only take action (detect changes) after this time period has passed." - )] + #[clap(short = 'd', long = "delay", forbid_empty_values = true)] pub delay: Option, - #[clap(long = "no-restart", help = "Don’t restart command while it’s still running.")] + #[clap(long = "no-restart", help = "Do not restart the command while it's still running.")] pub no_restart: bool, - #[clap( - long = "run-all", - help = "By default, only the tests of the last modified test file are executed. This explicitly runs all tests when a change is made." - )] + /// Explicitly re-run all tests when a change is made. + /// + /// By default, only the tests of the last modified test file are executed. + #[clap(long = "run-all")] pub run_all: bool, /// Watch specific file(s) or folder(s) /// - /// By default, the project's source dir is watched + /// By default, the project's source directory is watched. #[clap( short = 'w', long = "watch", @@ -112,15 +108,13 @@ pub async fn watch_build(args: BuildArgs) -> eyre::Result<()> { /// snapshot` pub async fn watch_snapshot(args: SnapshotArgs) -> eyre::Result<()> { let (init, mut runtime) = args.watchexec_config()?; - let cmd = cmd_args( - args.build_args().watch.watch.as_ref().map(|paths| paths.len()).unwrap_or_default(), - ); + let cmd = cmd_args(args.test.watch.watch.as_ref().map(|paths| paths.len()).unwrap_or_default()); trace!("watch snapshot cmd={:?}", cmd); runtime.command(cmd.clone()); let wx = Watchexec::new(init, runtime.clone())?; - on_action(args.build_args().watch.clone(), runtime, Arc::clone(&wx), cmd, (), |_| {}); + on_action(args.test.watch.clone(), runtime, Arc::clone(&wx), cmd, (), |_| {}); // start executing the command immediately wx.send_event(Event::default()).await?; @@ -132,10 +126,8 @@ pub async fn watch_snapshot(args: SnapshotArgs) -> eyre::Result<()> { /// Executes a [`Watchexec`] that listens for changes in the project's src dir and reruns `forge /// test` pub async fn watch_test(args: TestArgs) -> eyre::Result<()> { - let (init, mut runtime) = args.build_args().watchexec_config()?; - let cmd = cmd_args( - args.build_args().watch.watch.as_ref().map(|paths| paths.len()).unwrap_or_default(), - ); + let (init, mut runtime) = args.watchexec_config()?; + let cmd = cmd_args(args.watch.watch.as_ref().map(|paths| paths.len()).unwrap_or_default()); trace!("watch test cmd={:?}", cmd); runtime.command(cmd.clone()); let wx = Watchexec::new(init, runtime.clone())?; @@ -145,7 +137,7 @@ pub async fn watch_test(args: TestArgs) -> eyre::Result<()> { args.filter().test_pattern.is_some() || args.filter().path_pattern.is_some() || args.filter().contract_pattern.is_some() || - args.build_args().watch.run_all; + args.watch.run_all; let config: Config = args.build_args().into(); let state = WatchTestState { @@ -153,7 +145,7 @@ pub async fn watch_test(args: TestArgs) -> eyre::Result<()> { no_reconfigure, last_test_files: Default::default(), }; - on_action(args.build_args().watch.clone(), runtime, Arc::clone(&wx), cmd, state, on_test); + on_action(args.watch.clone(), runtime, Arc::clone(&wx), cmd, state, on_test); // start executing the command immediately wx.send_event(Event::default()).await?; diff --git a/cli/src/forge.rs b/cli/src/forge.rs index 8c462c8aee818..0ef2df7301278 100644 --- a/cli/src/forge.rs +++ b/cli/src/forge.rs @@ -18,7 +18,7 @@ fn main() -> eyre::Result<()> { let opts = Opts::parse(); match opts.sub { Subcommands::Test(cmd) => { - if cmd.build_args().is_watch() { + if cmd.is_watch() { utils::block_on(watch::watch_test(cmd))?; } else { let outcome = cmd.run()?; diff --git a/cli/src/opts/cast.rs b/cli/src/opts/cast.rs index 42a0320dd2a14..01a00af2fe956 100644 --- a/cli/src/opts/cast.rs +++ b/cli/src/opts/cast.rs @@ -1,99 +1,104 @@ -use std::{path::PathBuf, str::FromStr}; - -use clap::{Parser, Subcommand}; -use ethers::{ - abi::token::{LenientTokenizer, Tokenizer}, - types::{Address, BlockId, BlockNumber, NameOrAddress, H256, U256}, -}; - use super::{ClapChain, EthereumOpts, Wallet}; use crate::{ cmd::cast::{call::CallArgs, find_block::FindBlockArgs}, - utils::parse_u256, + utils::{parse_ether_value, parse_u256}, }; +use clap::{Parser, Subcommand, ValueHint}; +use ethers::types::{Address, BlockId, BlockNumber, NameOrAddress, H256, U256}; +use std::{path::PathBuf, str::FromStr}; #[derive(Debug, Subcommand)] -#[clap(about = "Perform Ethereum RPC calls from the comfort of your command line.")] +#[clap( + about = "Perform Ethereum RPC calls from the comfort of your command line.", + after_help = "Find more information in the book: http://book.getfoundry.sh/reference/cast/cast.html" +)] pub enum Subcommands { #[clap(name = "--max-int")] - #[clap(about = "Maximum i256 value")] + #[clap(about = "Get the maximum i256 value.")] MaxInt, #[clap(name = "--min-int")] - #[clap(about = "Minimum i256 value")] + #[clap(about = "Get the minimum i256 value.")] MinInt, #[clap(name = "--max-uint")] - #[clap(about = "Maximum u256 value")] + #[clap(about = "Get the maximum u256 value.")] MaxUint, #[clap(aliases = &["--from-ascii"])] #[clap(name = "--from-utf8")] - #[clap(about = "Convert text data into hexdata")] + #[clap(about = "Convert UTF8 text to hex.")] FromUtf8 { text: Option }, #[clap(name = "--to-hex")] - #[clap(about = "Convert a decimal number into hex")] + #[clap(about = "Convert an integer to hex.")] ToHex { decimal: Option }, #[clap(name = "--concat-hex")] - #[clap(about = "Concatencate hex strings")] + #[clap(about = "Concatencate hex strings.")] ConcatHex { data: Vec }, #[clap(name = "--from-bin")] - #[clap(about = "Convert binary data into hex data")] + #[clap(about = "Convert binary data into hex data.")] FromBin, #[clap(name = "--to-hexdata")] - #[clap(about = r#"[||<@tag>] - Output lowercase, 0x-prefixed hex, converting from the - input, which can be: - - mixed case hex with or without 0x prefix - - 0x prefixed hex, concatenated with a ':' - - absolute path to file - - @tag, where $TAG is defined in environment variables - "#)] + #[clap( + about = "Normalize the input to lowercase, 0x-prefixed hex. See --help for more info.", + long_about = r#"Normalize the input to lowercase, 0x-prefixed hex. + +The input can be: +- mixed case hex with or without 0x prefix +- 0x prefixed hex, concatenated with a ':' +- an absolute path to file +- @tag, where the tag is defined in an environment variable"# + )] ToHexdata { input: Option }, #[clap(aliases = &["--to-checksum"])] // Compatibility with dapptools' cast #[clap(name = "--to-checksum-address")] - #[clap(about = "Convert an address to a checksummed format (EIP-55)")] + #[clap(about = "Convert an address to a checksummed format (EIP-55).")] ToCheckSumAddress { address: Option
}, #[clap(name = "--to-ascii")] - #[clap(about = "Convert hex data to text data")] + #[clap(about = "Convert hex data to an ASCII string.")] ToAscii { hexdata: Option }, #[clap(name = "--from-fix")] - #[clap(about = "Convert fixed point into specified number of decimals")] + #[clap(about = "Convert a fixed point number into an integer.")] FromFix { decimals: Option, #[clap(allow_hyphen_values = true)] // negative values not yet supported internally value: Option, }, #[clap(name = "--to-bytes32")] - #[clap(about = "Right-pads a hex bytes string to 32 bytes")] + #[clap(about = "Right-pads hex data to 32 bytes.")] ToBytes32 { bytes: Option }, #[clap(name = "--to-dec")] - #[clap(about = "Convert hex value into decimal number")] + #[clap(about = "Convert hex value into a decimal number.")] ToDec { hexvalue: Option }, #[clap(name = "--to-fix")] - #[clap(about = "Convert integers into fixed point with specified decimals")] + #[clap(about = "Convert an integer into a fixed point number.")] ToFix { decimals: Option, #[clap(allow_hyphen_values = true)] // negative values not yet supported internally value: Option, }, #[clap(name = "--to-uint256")] - #[clap(about = "Convert a number into uint256 hex string with 0x prefix")] + #[clap(about = "Convert a number to a hex-encoded uint256.")] ToUint256 { value: Option }, #[clap(name = "--to-int256")] - #[clap(about = "Convert a number into int256 hex string with 0x prefix")] + #[clap(about = "Convert a number to a hex-encoded int256.")] ToInt256 { value: Option }, #[clap(name = "--to-unit")] #[clap( - about = r#"Convert an ETH amount into a specified unit: ether, gwei or wei (default: wei). - Usage: - - 1ether wei | converts 1 ether to wei - - "1 ether" wei | converts 1 ether to wei - - 1ether | converts 1 ether to wei - - 1 gwei | converts 1 wei to gwei - - 1gwei ether | converts 1 gwei to ether - "# + about = "Convert an ETH amount into another unit (ether, gwei or wei).", + long_about = r#"Convert an ETH amount into another unit (ether, gwei or wei).\ + +Examples: +- 1ether wei +- "1 ether" wei +- 1ether +- 1 gwei +- 1gwei ether"# )] - ToUnit { value: Option, unit: Option }, + ToUnit { + value: Option, + #[clap(help = "The unit to convert to (ether, gwei, wei).", default_value = "wei")] + unit: String, + }, #[clap(name = "--to-wei")] - #[clap(about = "Convert an ETH amount into wei. Consider using --to-unit.")] + #[clap(about = "Convert an ETH amount to wei. Consider using --to-unit.")] ToWei { #[clap(allow_hyphen_values = true)] // negative values not yet supported internally value: Option, @@ -107,129 +112,157 @@ pub enum Subcommands { unit: Option, }, #[clap(name = "access-list")] - #[clap(about = "Create an access list for a transaction")] + #[clap(about = "Create an access list for a transaction.")] AccessList { - #[clap(help = "the address you want to query", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The destination of the transaction.", parse(try_from_str = parse_name_or_address))] address: NameOrAddress, + #[clap(help = "The signature of the function to call.")] sig: String, + #[clap(help = "The arguments of the function to call.")] args: Vec, - #[clap(long, short, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: Option, #[clap(flatten)] + // TODO: We only need RPC URL + etherscan stuff from this struct eth: EthereumOpts, - #[clap(long = "json", short = 'j')] + #[clap(long = "json", short = 'j', help_heading = "DISPLAY OPTIONS")] to_json: bool, }, #[clap(name = "block")] - #[clap( - about = "Prints information about . If is given, print only the value of that field" - )] + #[clap(about = "Get information about a block.")] Block { - #[clap(help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: BlockId, #[clap(long, env = "CAST_FULL_BLOCK")] full: bool, + #[clap(long, short, help = "If specified, only get the given field of the block.")] field: Option, - #[clap(long = "json", short = 'j')] + #[clap(long = "json", short = 'j', help_heading = "DISPLAY OPTIONS")] to_json: bool, #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "block-number")] - #[clap(about = "Prints latest block number")] + #[clap(about = "Get the latest block number.")] BlockNumber { #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "call")] - #[clap(about = "Perform a local call to without publishing a transaction.")] + #[clap(about = "Perform a call on an account without publishing a transaction.")] Call(CallArgs), - #[clap(about = "Pack a signature and an argument list into hexadecimal calldata.")] + #[clap(about = "ABI-encode a function with arguments.")] Calldata { #[clap( - help = r#"When called with of the form (...), then perform ABI encoding to produce the hexadecimal calldata. - If the value given—containing at least one slash character—then treat it as a file name to read, and proceed as if the contents were passed as hexadecimal data. - Given data, ensure it is hexadecimal calldata starting with 0x and normalize it to lowercase. - "# + help = "The function signature.", + long_help = "The function signature in the form ()" )] sig: String, - #[clap(allow_hyphen_values = true)] // negative values not yet supported internally + #[clap(allow_hyphen_values = true)] args: Vec, }, #[clap(name = "chain")] - #[clap(about = "Prints symbolic name of current blockchain by checking genesis hash")] + #[clap(about = "Get the symbolic name of the current chain.")] Chain { #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "chain-id")] - #[clap(about = "Returns ethereum chain id")] + #[clap(about = "Get the Ethereum chain ID.")] ChainId { #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "client")] - #[clap(about = "Returns the current client version")] + #[clap(about = "Get the current client version.")] Client { #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "compute-address")] - #[clap(about = "Returns the computed address from a given address and nonce pair")] + #[clap(about = "Compute the contract address from a given nonce and deployer address.")] ComputeAddress { #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, - #[clap(help = "the address to create from")] + #[clap(help = "The deployer address.")] address: String, - #[clap(long, help = "address nonce", parse(try_from_str = parse_u256))] + #[clap(long, help = "The nonce of the deployer address.", parse(try_from_str = parse_u256))] nonce: Option, }, #[clap(name = "namehash")] - #[clap(about = "Returns ENS namehash of provided name")] + #[clap(about = "Calculate the ENS namehash of a name.")] Namehash { name: String }, #[clap(name = "tx")] - #[clap(about = "Show information about the transaction ")] + #[clap(about = "Get information about a transaction.")] Tx { hash: String, field: Option, - #[clap(long = "json", short = 'j')] + #[clap(long = "json", short = 'j', help_heading = "DISPLAY OPTIONS")] to_json: bool, #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "receipt")] - #[clap(about = "Print information about the transaction receipt for ")] + #[clap(about = "Get the transaction receipt for a transaction.")] Receipt { + #[clap(value_name = "TX_HASH")] hash: String, field: Option, #[clap( short, long, - help = "the number of confirmations until the receipt is fetched", + help = "The number of confirmations until the receipt is fetched", default_value = "1" )] confirmations: usize, #[clap(long, env = "CAST_ASYNC")] cast_async: bool, - #[clap(long = "json", short = 'j')] + #[clap(long = "json", short = 'j', help_heading = "DISPLAY OPTIONS")] to_json: bool, #[clap(long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "send")] - #[clap(about = "Publish a transaction signed by to call with ")] + #[clap(about = "Sign and publish a transaction.")] SendTx { - #[clap(help = "the address you want to transact with", parse(try_from_str = parse_name_or_address))] + #[clap( + help = "The destination of the transaction.", + parse(try_from_str = parse_name_or_address) + )] to: NameOrAddress, - #[clap(help = "the function signature or name you want to call")] + #[clap(help = "The signature of the function to call.")] sig: Option, - #[clap(help = "the list of arguments you want to call the function with")] + #[clap(help = "The arguments of the function to call.")] args: Vec, - #[clap(long, help = "gas quantity for the transaction", parse(try_from_str = parse_u256))] + #[clap(long, help = "Gas limit for the transaction.", parse(try_from_str = parse_u256))] gas: Option, - #[clap(long = "gas-price", help = "gas price for the transaction", env = "ETH_GAS_PRICE", parse(try_from_str = parse_ether_value))] + #[clap( + long = "gas-price", + help = "Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.", + env = "ETH_GAS_PRICE", + parse(try_from_str = parse_ether_value) + )] gas_price: Option, - #[clap(long, help = "ether value (in wei or string with unit type e.g. 1ether, 10gwei, 0.01ether) for the transaction", parse(try_from_str = parse_ether_value))] + #[clap( + long, + help = "Ether to send in the transaction.", + long_help = r#"Ether to send in the transaction, either specified in wei, or as a string with a unit type. + +Examples: 1ether, 10gwei, 0.01ether"#, + parse(try_from_str = parse_ether_value) + )] value: Option, #[clap(long, help = "nonce for the transaction", parse(try_from_str = parse_u256))] nonce: Option, @@ -239,274 +272,338 @@ pub enum Subcommands { eth: EthereumOpts, #[clap( long, - help = "use legacy transactions instead of EIP1559 ones. this is auto-enabled for common networks without EIP1559" + help = "Send a legacy transaction instead of an EIP1559 transaction.", + long_help = r#"Send a legacy transaction instead of an EIP1559 transaction. + +This is automatically enabled for common networks without EIP1559."# )] legacy: bool, #[clap( short, long, - help = "the number of confirmations until the receipt is fetched", + help = "The number of confirmations until the receipt is fetched.", default_value = "1" )] confirmations: usize, - #[clap(long = "json", short = 'j')] + #[clap(long = "json", short = 'j', help_heading = "DISPLAY OPTIONS")] to_json: bool, - #[clap(long = "resend", help = "reuse account latest nonce", conflicts_with = "nonce")] + #[clap( + long = "resend", + help = "Reuse the latest nonce for the sender account.", + conflicts_with = "nonce" + )] resend: bool, }, #[clap(name = "publish")] - #[clap(about = "Publish a raw transaction to the network")] + #[clap(about = "Publish a raw transaction to the network.")] PublishTx { - #[clap(help = "the raw transaction you want to publish")] + #[clap(help = "The raw transaction", value_name = "RAW_TX")] raw_tx: String, #[clap(long, env = "CAST_ASYNC")] cast_async: bool, + // FIXME: We only need the RPC URL and `--flashbots` options from this. #[clap(flatten)] eth: EthereumOpts, }, #[clap(name = "estimate")] - #[clap(about = "Estimate the gas cost of a transaction from to with ")] + #[clap(about = "Estimate the gas cost of a transaction.")] Estimate { - #[clap(help = "the address you want to transact with", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The destination of the transaction.", parse(try_from_str = parse_name_or_address))] to: NameOrAddress, - #[clap(help = "the function signature or name you want to call")] + #[clap(help = "The signature of the function to call.")] sig: String, - #[clap(help = "the list of arguments you want to call the function with")] + #[clap(help = "The arguments of the function to call.")] args: Vec, - #[clap(long, help = "value for tx estimate (in wei)")] + #[clap( + long, + help = "Ether to send in the transaction.", + long_help = r#"Ether to send in the transaction, either specified in wei, or as a string with a unit type. + +Examples: 1ether, 10gwei, 0.01ether"#, + parse(try_from_str = parse_ether_value) + )] value: Option, #[clap(flatten)] + // TODO: We only need RPC URL and Etherscan API key here. eth: EthereumOpts, }, #[clap(name = "--calldata-decode")] - #[clap(about = "Decode ABI-encoded hex input data. Use `--abi-decode` to decode output data")] + #[clap(about = "Decode ABI-encoded input data.")] CalldataDecode { - #[clap( - help = "the function signature you want to decode, in the format `()()`" - )] + #[clap(help = "The function signature in the format `()()`.")] sig: String, - #[clap(help = "the encoded calldata, in hex format")] + #[clap(help = "The ABI-encoded calldata.")] calldata: String, }, #[clap(name = "--abi-decode")] #[clap( - about = "Decode ABI-encoded hex output data. Pass --input to decode as input, or use `--calldata-decode`" + about = "Decode ABI-encoded input or output data", + long_about = r#"Decode ABI-encoded input or output data. + +Defaults to decoding output data. To decode input data pass --input or use cast --calldata-decode."# )] AbiDecode { - #[clap( - help = "the function signature you want to decode, in the format `()()`" - )] + #[clap(help = "The function signature in the format `()()`.")] sig: String, - #[clap(help = "the encoded calldata, in hex format")] + #[clap(help = "The ABI-encoded calldata.")] calldata: String, - #[clap(long, short, help = "the encoded output, in hex format")] + #[clap(long, short, help = "Decode input data.")] input: bool, }, #[clap(name = "abi-encode")] - #[clap( - about = "ABI encodes the given arguments with the function signature, excluding the selector" - )] + #[clap(about = "ABI encode the given function argument, excluding the selector.")] AbiEncode { - #[clap(help = "the function signature")] + #[clap(help = "The function signature.")] sig: String, - #[clap(help = "the list of function arguments")] + #[clap(help = "The arguments of the function.")] #[clap(allow_hyphen_values = true)] args: Vec, }, #[clap(name = "index")] - #[clap( - about = "Get storage slot of value from mapping type, mapping slot number and input value" - )] + #[clap(about = "Compute the storage slot for an entry in a mapping.")] Index { - #[clap(help = "mapping key type")] - from_type: String, - #[clap(help = "mapping value type")] - to_type: String, - #[clap(help = "the value")] - from_value: String, - #[clap(help = "storage slot of the mapping")] + #[clap(help = "The mapping key type.")] + key_type: String, + #[clap(help = "The mapping value type.")] + value_type: String, + #[clap(help = "The mapping key.")] + key: String, + #[clap(help = "The storage slot of the mapping.")] slot_number: String, }, #[clap(name = "4byte")] - #[clap(about = "Fetches function signatures given the selector from 4byte.directory")] + #[clap(about = "Get the function signatures for the given selector from 4byte.directory.")] FourByte { - #[clap(help = "the function selector")] + #[clap(help = "The function selector.")] selector: String, }, #[clap(name = "4byte-decode")] - #[clap(about = "Decodes transaction calldata by fetching the signature using 4byte.directory")] + #[clap(about = "Decode ABI-encoded calldata using 4byte.directory.")] FourByteDecode { - #[clap(help = "the ABI-encoded calldata")] + #[clap(help = "The ABI-encoded calldata.")] calldata: String, - #[clap(long, help = "the 4byte selector id to use, can also be earliest/latest")] + #[clap( + long, + help = "The index of the resolved signature to use.", + long_help = r#"The index of the resolved signature to use. + +4byte.directory can have multiple possible signatures for a given selector. + +The index can also be earliest or latest."# + )] id: Option, }, #[clap(name = "4byte-event")] - #[clap( - about = "Takes a 32 byte topic and prints the response from querying 4byte.directory for that topic" - )] + #[clap(about = "Get the event signature for a given topic 0 from 4byte.directory.")] FourByteEvent { - #[clap(help = "the 32 byte topic")] + #[clap(help = "Topic 0", value_name = "TOPIC_0")] topic: String, }, #[clap(name = "pretty-calldata")] - #[clap(about = "Pretty prints calldata, if available gets signature from 4byte.directory")] + #[clap( + about = "Pretty print calldata.", + long_about = r#"Pretty print calldata. + +Tries to decode the calldata using 4byte.directory unless --offline is passed."# + )] PrettyCalldata { - #[clap(help = "Hex encoded calldata")] + #[clap(help = "The calldata.")] calldata: String, #[clap(long, short, help = "Skip the 4byte directory lookup.")] offline: bool, }, - #[clap(name = "age")] - #[clap(about = "Prints the timestamp of a block")] + #[clap(about = "Get the timestamp of a block.")] Age { - #[clap(global = true, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: Option, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "balance")] - #[clap(about = "Print the balance of in wei")] + #[clap(about = "Get the balance of an account in wei.")] Balance { - #[clap(long, short, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: Option, - #[clap(help = "the account you want to query", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The account you want to query", parse(try_from_str = parse_name_or_address))] who: NameOrAddress, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "basefee")] - #[clap(about = "Print the basefee of a block")] + #[clap(about = "Get the basefee of a block.")] BaseFee { - #[clap(global = true, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: Option, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "code")] - #[clap(about = "Prints the bytecode at
")] + #[clap(about = "Get the bytecode of a contract.")] Code { - #[clap(long, short, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: Option, - #[clap(help = "the address you want to query", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The contract address.", parse(try_from_str = parse_name_or_address))] who: NameOrAddress, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "gas-price")] - #[clap(about = "Prints current gas price of target chain")] + #[clap(about = "Get the current gas price.")] GasPrice { #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "keccak")] - #[clap(about = "Keccak-256 hashes arbitrary data")] + #[clap(about = "Hash arbitrary data using keccak-256.")] Keccak { data: String }, #[clap(name = "resolve-name")] - #[clap(about = "Returns the address the provided ENS name resolves to")] + #[clap(about = "Perform an ENS lookup.")] ResolveName { - #[clap(help = "the account you want to resolve")] + #[clap(help = "The name to lookup.")] who: Option, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, - #[clap(long, short, help = "do a forward resolution to ensure the ENS name is correct")] + #[clap(long, short, help = "Perform a reverse lookup to verify that the name is correct.")] verify: bool, }, #[clap(name = "lookup-address")] - #[clap(about = "Returns the name the provided address resolves to")] + #[clap(about = "Perform an ENS reverse lookup.")] LookupAddress { - #[clap(help = "the account you want to resolve")] + #[clap(help = "The account to perform the lookup for.")] who: Option
, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, - #[clap(long, short, help = "do a forward resolution to ensure the address is correct")] + #[clap( + long, + short, + help = "Perform a normal lookup to verify that the address is correct." + )] verify: bool, }, - #[clap(name = "storage", about = "Show the raw value of a contract's storage slot")] + #[clap(name = "storage", about = "Get the raw value of a contract's storage slot.")] Storage { - #[clap(help = "the contract address", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The contract address.", parse(try_from_str = parse_name_or_address))] address: NameOrAddress, - #[clap(help = "the storage slot number (hex or number)", parse(try_from_str = parse_slot))] + #[clap(help = "The storage slot number (hex or decimal)", parse(try_from_str = parse_slot))] slot: H256, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, #[clap( long, - short, - help = "the block you want to query, can also be earliest/latest/pending", + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", parse(try_from_str = parse_block_id) )] block: Option, }, - #[clap(name = "proof", about = "Generate a storage proof for a given slot")] + #[clap(name = "proof", about = "Generate a storage proof for a given storage slot.")] Proof { - #[clap(help = "the contract address", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The contract address.", parse(try_from_str = parse_name_or_address))] address: NameOrAddress, - #[clap(help = "the storage slot numbers (hex or number)", parse(try_from_str = parse_slot))] + #[clap(help = "The storage slot numbers (hex or decimal).", parse(try_from_str = parse_slot))] slots: Vec, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, #[clap( long, - short, - help = "the block you want to query, can also be earliest/latest/pending", + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", parse(try_from_str = parse_block_id) )] block: Option, }, #[clap(name = "nonce")] - #[clap(about = "Prints the number of transactions sent from
")] + #[clap(about = "Get the nonce for an account.")] Nonce { - #[clap(long, short = 'B', help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + parse(try_from_str = parse_block_id) + )] block: Option, - #[clap(help = "the address you want to query", parse(try_from_str = parse_name_or_address))] + #[clap(help = "The address you want to get the nonce for.", parse(try_from_str = parse_name_or_address))] who: NameOrAddress, #[clap(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, #[clap(name = "etherscan-source")] - #[clap(about = "Prints the source code of a contract from Etherscan")] + #[clap(about = "Get the source code of a contract from Etherscan.")] EtherscanSource { #[clap(flatten)] chain: ClapChain, - #[clap(help = "the contract address")] + #[clap(help = "The contract's address.")] address: String, - #[clap(short, help = "output directory to expand source tree")] + #[clap(short, help = "The output directory to expand source tree into.", value_hint = ValueHint::DirPath)] directory: Option, #[clap(long, env = "ETHERSCAN_API_KEY")] etherscan_api_key: String, }, - #[clap(name = "wallet", about = "Set of wallet management utilities")] + #[clap(name = "wallet", about = "Wallet management utilities.")] Wallet { #[clap(subcommand)] command: WalletSubcommands, }, #[clap( name = "interface", - about = "Generate contract's interface from ABI. Currently it doesn't support ABI encoder V2" + about = "Generate a Solidity interface from a given ABI.", + long_about = "Generate a Solidity interface from a given ABI. Currently does not support ABI encoder v2." )] Interface { - #[clap(help = "The contract address or path to ABI file")] + #[clap( + help = "The contract address, or the path to an ABI file.", + long_help = r#"The contract address, or the path to an ABI file. + +If an address is specified, then the ABI is fetched from Etherscan."# + )] path_or_address: String, - #[clap(long, short, default_value = "^0.8.10", help = "pragma version")] + #[clap(long, short, default_value = "^0.8.10", help = "Solidity pragma version.")] pragma: String, - #[clap(short, help = "Path to output file. Defaults to stdout")] + #[clap( + short, + help = "The path to the output file.", + long_help = "The path to the output file. If not specified, the interface will be output to stdout." + )] output_location: Option, - #[clap(short, env = "ETHERSCAN_API_KEY", help = "etherscan API key")] + #[clap(long, short, env = "ETHERSCAN_API_KEY", help = "etherscan API key")] etherscan_api_key: Option, #[clap(flatten)] chain: ClapChain, }, - #[clap(name = "sig", about = "Print a function's 4-byte selector")] + #[clap(name = "sig", about = "Get the selector for a function.")] Sig { - #[clap(help = "The human-readable function signature, e.g. 'transfer(address,uint256)'")] + #[clap(help = "The function signature, e.g. transfer(address,uint256).")] sig: String, }, - #[clap( - name = "find-block", - about = "Prints the block number closest to the provided timestamp" - )] + #[clap(name = "find-block", about = "Get the block number closest to the provided timestamp.")] FindBlock(FindBlockArgs), #[clap(about = "Generate shell completions script")] Completions { @@ -517,57 +614,61 @@ pub enum Subcommands { #[derive(Debug, Parser)] pub enum WalletSubcommands { - #[clap(name = "new", about = "Create and output a new random keypair")] + #[clap(name = "new", about = "Create a new random keypair.")] New { - #[clap(help = "If provided, then keypair will be written to encrypted json keystore")] + #[clap(help = "If provided, then keypair will be written to an encrypted JSON keystore.")] path: Option, #[clap( long, short, - help = "Triggers a hidden password prompt for the json keystore", + help = "Triggers a hidden password prompt for the JSON keystore.", conflicts_with = "unsafe-password", requires = "path" )] password: bool, #[clap( long, - help = "Password for json keystore in cleartext. This is UNSAFE to use and we recommend using the --password parameter", + help = "Password for the JSON keystore in cleartext. This is UNSAFE to use and we recommend using the --password.", requires = "path", env = "CAST_PASSWORD" )] unsafe_password: Option, }, - #[clap(name = "vanity", about = "Generate a vanity address")] + #[clap(name = "vanity", about = "Generate a vanity address.")] Vanity { - #[clap(long, help = "Prefix for vanity address", required_unless_present = "ends-with")] + #[clap( + long, + help = "Prefix for the vanity address.", + required_unless_present = "ends-with" + )] starts_with: Option, - #[clap(long, help = "Suffix for vanity address")] + #[clap(long, help = "Suffix for the vanity address.")] ends_with: Option, #[clap( long, - help = "Generate a vanity contract address created by the generated account with specified nonce" + help = "Generate a vanity contract address created by the generated keypair with the specified nonce." )] nonce: Option, /* 2^64-1 is max possible nonce per https://eips.ethereum.org/EIPS/eip-2681 */ }, - #[clap(name = "address", about = "Convert a private key to an address")] + #[clap(name = "address", about = "Convert a private key to an address.")] Address { #[clap(flatten)] wallet: Wallet, }, - #[clap(name = "sign", about = "Sign the message with provided private key")] + #[clap(name = "sign", about = "Sign a message.")] Sign { #[clap(help = "message to sign")] message: String, #[clap(flatten)] wallet: Wallet, }, - #[clap(name = "verify", about = "Verify the signature on the message")] + #[clap(name = "verify", about = "Verify the signature of a message.")] Verify { - #[clap(help = "original message")] + #[clap(help = "The original message.")] message: String, - #[clap(help = "signature to verify")] + #[clap(help = "The signature to verify.")] signature: String, - #[clap(long, short, help = "pubkey of message signer")] + #[clap(long, short, help = "The address of the message signer.")] address: String, }, } @@ -599,14 +700,6 @@ fn parse_slot(s: &str) -> eyre::Result { }) } -fn parse_ether_value(value: &str) -> eyre::Result { - Ok(if value.starts_with("0x") { - U256::from_str(value)? - } else { - U256::from(LenientTokenizer::tokenize_uint(value)?) - }) -} - #[derive(Debug, Parser)] #[clap(name = "cast", version = crate::utils::VERSION_MESSAGE)] pub struct Opts { diff --git a/cli/src/opts/evm.rs b/cli/src/opts/evm.rs index 419a0a344d9d1..e8545dcfa53b1 100644 --- a/cli/src/opts/evm.rs +++ b/cli/src/opts/evm.rs @@ -49,15 +49,14 @@ pub struct EvmArgs { #[serde(skip_serializing_if = "Option::is_none")] pub fork_block_number: Option, - /// Disables storage caching entirely. This overrides any settings made in - /// [foundry_config::caching::StorageCachingConfig] + /// Explicitly disables the use of RPC caching. + /// + /// All storage slots are read entirely from the endpoint. + /// + /// This flag overrides the project's configuration file. /// /// See --fork-url. - #[clap( - long, - requires = "fork-url", - help = "Explicitly disables the use of storage. All storage slots are read entirely from the endpoint." - )] + #[clap(long, requires = "fork-url")] #[serde(skip)] pub no_storage_caching: bool, @@ -72,7 +71,7 @@ pub struct EvmArgs { pub sender: Option
, /// Enable the FFI cheatcode. - #[clap(help = "enables the FFI cheatcode", long)] + #[clap(help = "Enables the FFI cheatcode.", long)] #[serde(skip)] pub ffi: bool, @@ -81,11 +80,11 @@ pub struct EvmArgs { /// Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv). /// /// Verbosity levels: - /// 2: Print logs for all tests - /// 3: Print execution traces for failing tests - /// 4: Print execution traces for all tests, and setup traces for failing tests - /// 5: Print execution and setup traces for all tests - #[clap(long, short, parse(from_occurrences))] + /// - 2: Print logs for all tests + /// - 3: Print execution traces for failing tests + /// - 4: Print execution traces for all tests, and setup traces for failing tests + /// - 5: Print execution and setup traces for all tests + #[clap(long, short, parse(from_occurrences), verbatim_doc_comment)] #[serde(skip)] pub verbosity: u8, diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index 50849e8083006..ae4637273bb63 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -33,62 +33,71 @@ pub struct Opts { } #[derive(Debug, Subcommand)] -#[clap(about = "Build, test, fuzz, formally verify, debug & deploy solidity contracts.")] +#[clap( + about = "Build, test, fuzz, debug and deploy Solidity contracts.", + after_help = "Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html" +)] #[allow(clippy::large_enum_variant)] pub enum Subcommands { - #[clap(about = "Test your smart contracts")] + #[clap(about = "Run the project's tests.")] #[clap(alias = "t")] Test(test::TestArgs), - #[clap(about = "Generate rust bindings for your smart contracts")] + #[clap(about = "Generate Rust bindings for smart contracts.")] Bind(BindArgs), - #[clap(about = "Build your smart contracts")] + #[clap(about = "Build the project's smart contracts.")] #[clap(alias = "b")] Build(BuildArgs), - #[clap(about = "Run a single smart contract as a script")] + #[clap(about = "Run a single smart contract as a script.")] #[clap(alias = "r")] Run(RunArgs), - #[clap(alias = "u", about = "Fetches all upstream lib changes")] + #[clap( + alias = "u", + about = "Update one or multiple dependencies.", + long_about = "Update one or multiple dependencies. If no arguments are provided, then all dependencies are updated." + )] Update { #[clap( - help = "The submodule name of the library you want to update (will update all if none is provided)", + help = "The path to the dependency you want to update.", value_hint = ValueHint::DirPath )] lib: Option, }, - #[clap( - alias = "i", - about = "Installs one or more dependencies as git submodules (will install existing dependencies if no arguments are provided)" - )] + /// Install one or multiple dependencies. + /// + /// If no arguments are provided, then existing dependencies will be installed. + #[clap(alias = "i")] Install(InstallArgs), - #[clap(alias = "rm", about = "Removes one or more dependencies from git submodules")] + #[clap(alias = "rm", about = "Remove one or multiple dependencies.")] Remove { - #[clap(help = "The submodule name of the library you want to remove")] + #[clap(help = "The path to the dependency you want to remove.")] dependencies: Vec, }, - #[clap(about = "Prints the automatically inferred remappings for this repository")] + #[clap(about = "Get the automatically inferred remappings for the project.")] Remappings(RemappingArgs), #[clap( - about = "Verify your smart contracts source code on Etherscan. Requires `ETHERSCAN_API_KEY` to be set." + about = "Verify smart contracts on Etherscan.", + long_about = "Verify smart contracts on Etherscan." )] VerifyContract(VerifyArgs), #[clap( - about = "Check verification status on Etherscan. Requires `ETHERSCAN_API_KEY` to be set." + about = "Check verification status on Etherscan.", + long_about = "Check verification status on Etherscan." )] VerifyCheck(VerifyCheckArgs), - #[clap(alias = "c", about = "Deploy a compiled contract")] + #[clap(alias = "c", about = "Deploy a smart contract.")] Create(CreateArgs), - #[clap(alias = "i", about = "Initializes a new forge sample project")] + #[clap(alias = "i", about = "Create a new Forge project.")] Init(InitArgs), #[clap(about = "Generate shell completions script")] @@ -97,29 +106,29 @@ pub enum Subcommands { shell: clap_complete::Shell, }, - #[clap(about = "Removes the build artifacts and cache directories")] + #[clap(about = "Remove the build artifacts and cache directories.")] Clean { #[clap( - help = "The project's root path, default being the current working directory", + help = "The project's root path. Defaults to the current working directory.", long, value_hint = ValueHint::DirPath )] root: Option, }, - #[clap(about = "Creates a snapshot of each test's gas usage")] + #[clap(about = "Create a snapshot of each test's gas usage.")] Snapshot(snapshot::SnapshotArgs), - #[clap(about = "Shows the currently set config values")] + #[clap(about = "Display the current config.")] Config(config::ConfigArgs), - #[clap(about = "Concats a file with all of its imports")] + #[clap(about = "Flatten a source file and all of its imports into one file.")] Flatten(flatten::FlattenArgs), // #[clap(about = "formats Solidity source files")] // Fmt(FmtArgs), - #[clap(about = "Outputs a contract in a specified format (ir, assembly, ...)")] + #[clap(about = "Get specialized information about a smart contract")] Inspect(inspect::InspectArgs), - #[clap(about = "Display a tree visualization of the project's dependency graph")] + #[clap(about = "Display a tree visualization of the project's dependency graph.")] Tree(tree::TreeArgs), } @@ -129,30 +138,31 @@ pub enum Subcommands { // See also [`BuildArgs`] #[derive(Default, Debug, Clone, Parser, Serialize)] pub struct CompilerArgs { - #[clap(help = "Choose the evm version", long)] + #[clap(help = "The target EVM version.", long)] #[serde(skip_serializing_if = "Option::is_none")] pub evm_version: Option, - #[clap(help = "Activate the solidity optimizer", long)] - // skipped because, optimize is opt-in + #[clap(help = "Activate the Solidity optimizer.", long)] #[serde(skip)] pub optimize: bool, - #[clap(help = "Optimizer parameter runs", long)] + #[clap(help = "The number of optimizer runs.", long)] #[serde(skip_serializing_if = "Option::is_none")] pub optimize_runs: Option, - #[clap( - help = "Extra output types to include in the contract's json artifact [evm.assembly, ewasm, ir, irOptimized, metadata] eg: `--extra-output evm.assembly`", - long - )] + /// Extra output to include in the contract's artifact. + /// + /// Example keys: evm.assembly, ewasm, ir, irOptimized, metadata + /// + /// For a full description, see https://docs.soliditylang.org/en/v0.8.13/using-the-compiler.html#input-description + #[clap(long)] #[serde(skip_serializing_if = "Option::is_none")] pub extra_output: Option>, - #[clap( - help = "Extra output types to write to a separate file [metadata, ir, irOptimized, ewasm] eg: `--extra-output-files metadata`", - long - )] + /// Extra output to write to separate files. + /// + /// Valid values: metadata, ir, irOptimized, ewasm, evm.assembly + #[clap(long)] #[serde(skip_serializing_if = "Option::is_none")] pub extra_output_files: Option>, } diff --git a/cli/src/opts/mod.rs b/cli/src/opts/mod.rs index d8de4960e07a3..d66f54fc7eda5 100644 --- a/cli/src/opts/mod.rs +++ b/cli/src/opts/mod.rs @@ -29,6 +29,7 @@ use serde::Serialize; const FLASHBOTS_URL: &str = "https://rpc.flashbots.net"; // Helper for exposing enum values for `Chain` +// TODO: Is this a duplicate of config/src/chain.rs? #[derive(Debug, Clone, Parser)] pub struct ClapChain { #[clap( @@ -60,16 +61,9 @@ pub struct ClapChain { #[derive(Parser, Debug, Clone, Serialize)] pub struct EthereumOpts { - #[clap(env = "ETH_RPC_URL", long = "rpc-url", help = "The tracing / archival node's URL")] + #[clap(env = "ETH_RPC_URL", long = "rpc-url", help = "The RPC endpoint.")] pub rpc_url: Option, - #[clap(env = "ETH_FROM", short, long = "from", help = "The sender account")] - pub from: Option
, - - #[clap(flatten)] - #[serde(skip)] - pub wallet: Wallet, - #[clap(long, help = "Use the flashbots RPC URL (https://rpc.flashbots.net)")] pub flashbots: bool, @@ -79,6 +73,10 @@ pub struct EthereumOpts { #[clap(long, env = "CHAIN", default_value = "mainnet")] #[serde(skip)] pub chain: Chain, + + #[clap(flatten, next_help_heading = "WALLET OPTIONS")] + #[serde(skip)] + pub wallet: Wallet, } impl EthereumOpts { @@ -92,7 +90,7 @@ impl EthereumOpts { WalletType::Trezor(signer) => signer.address(), } } else { - self.from.unwrap_or_else(Address::zero) + self.wallet.from.unwrap_or_else(Address::zero) } } @@ -189,36 +187,83 @@ The wallet options can either be: "# )] pub struct Wallet { - #[clap(long, short, help = "Interactive prompt to insert your private key")] + #[clap( + long, + short, + help_heading = "WALLET OPTIONS - RAW", + help = "Open an interactive prompt to enter your private key." + )] pub interactive: bool, - #[clap(long = "private-key", help = "Your private key string")] + #[clap( + long = "private-key", + help_heading = "WALLET OPTIONS - RAW", + help = "Use the provided private key." + )] pub private_key: Option, - #[clap(env = "ETH_KEYSTORE", long = "keystore", help = "Path to your keystore folder / file")] + #[clap( + long = "mnemonic-path", + help_heading = "WALLET OPTIONS - RAW", + help = "Use the mnemonic file at the specified path." + )] + pub mnemonic_path: Option, + + #[clap( + long = "mnemonic-index", + help_heading = "WALLET OPTIONS - RAW", + help = "Use the private key from the given mnemonic index. Used with --mnemonic-path.", + default_value = "0" + )] + pub mnemonic_index: u32, + + #[clap( + env = "ETH_KEYSTORE", + long = "keystore", + help_heading = "WALLET OPTIONS - KEYSTORE", + help = "Use the keystore in the given folder or file." + )] pub keystore_path: Option, - #[clap(long = "password", help = "Your keystore password", requires = "keystore-path")] + #[clap( + long = "password", + help_heading = "WALLET OPTIONS - KEYSTORE", + help = "The keystore password. Used with --keystore.", + requires = "keystore-path" + )] pub keystore_password: Option, - #[clap(long = "mnemonic-path", help = "Path to your mnemonic file")] - pub mnemonic_path: Option, - - #[clap(short, long = "ledger", help = "Use your Ledger hardware wallet")] + #[clap( + short, + long = "ledger", + help_heading = "WALLET OPTIONS - HARDWARE WALLET", + help = "Use a Ledger hardware wallet." + )] pub ledger: bool, - #[clap(short, long = "trezor", help = "Use your Trezor hardware wallet")] + #[clap( + short, + long = "trezor", + help_heading = "WALLET OPTIONS - HARDWARE WALLET", + help = "Use a Trezor hardware wallet." + )] pub trezor: bool, - #[clap(long = "hd-path", help = "Derivation path for your hardware wallet (trezor or ledger)")] + #[clap( + long = "hd-path", + help_heading = "WALLET OPTIONS - HARDWARE WALLET", + help = "The derivation path to use with hardware wallets." + )] pub hd_path: Option, #[clap( - long = "mnemonic-index", - help = "your index in the standard hd path", - default_value = "0" + env = "ETH_FROM", + short, + long = "from", + help_heading = "WALLET OPTIONS - REMOTE", + help = "The sender account." )] - pub mnemonic_index: u32, + pub from: Option
, } impl Wallet { @@ -279,6 +324,7 @@ mod tests { #[test] fn illformed_private_key_generates_user_friendly_error() { let wallet = Wallet { + from: None, interactive: false, private_key: Some("123".to_string()), keystore_path: None, diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 818d7c134e4fc..0c55bbd5f8c54 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -1,13 +1,16 @@ +use ethers::{ + abi::token::{LenientTokenizer, Tokenizer}, + solc::EvmVersion, + types::U256, +}; +use forge::executor::{opts::EvmOpts, Fork, SpecId}; +use foundry_config::{caching::StorageCachingConfig, Config}; use std::{ future::Future, path::{Path, PathBuf}, str::FromStr, time::Duration, }; - -use ethers::{solc::EvmVersion, types::U256}; -use forge::executor::{opts::EvmOpts, Fork, SpecId}; -use foundry_config::{caching::StorageCachingConfig, Config}; use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; @@ -126,6 +129,20 @@ pub fn parse_u256(s: &str) -> eyre::Result { Ok(if s.starts_with("0x") { U256::from_str(s)? } else { U256::from_dec_str(s)? }) } +/// Parses an ether value from a string. +/// +/// The amount can be tagged with a unit, e.g. "1ether". +/// +/// If the string represents an untagged amount (e.g. "100") then +/// it is interpreted as wei. +pub fn parse_ether_value(value: &str) -> eyre::Result { + Ok(if value.starts_with("0x") { + U256::from_str(value)? + } else { + U256::from(LenientTokenizer::tokenize_uint(value)?) + }) +} + /// Parses a `Duration` from a &str pub fn parse_delay(delay: &str) -> eyre::Result { let delay = if delay.ends_with("ms") {