From 4c4f5f9d1e8da7d3c788bff9f5c727eda9bbd15b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Feb 2024 13:44:59 -0800 Subject: [PATCH 1/3] feat: `cast mktx` --- crates/cast/bin/cmd/mktx.rs | 145 ++++++++++++++++++++++++++++++++++ crates/cast/bin/cmd/mod.rs | 1 + crates/cast/bin/main.rs | 1 + crates/cast/bin/opts.rs | 7 +- crates/cast/tests/cli/main.rs | 83 +++++++++++++++++++ 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 crates/cast/bin/cmd/mktx.rs diff --git a/crates/cast/bin/cmd/mktx.rs b/crates/cast/bin/cmd/mktx.rs new file mode 100644 index 000000000000..d40a1b7c4222 --- /dev/null +++ b/crates/cast/bin/cmd/mktx.rs @@ -0,0 +1,145 @@ +use cast::TxBuilder; +use clap::Parser; +use ethers_core::types::NameOrAddress; +use ethers_middleware::MiddlewareBuilder; +use ethers_providers::Middleware; +use ethers_signers::Signer; +use eyre::Result; +use foundry_cli::{ + opts::{EthereumOpts, TransactionOpts}, + utils, +}; +use foundry_common::types::ToAlloy; +use foundry_config::Config; +use std::str::FromStr; + +/// CLI arguments for `cast mktx`. +#[derive(Debug, Parser)] +pub struct MakeTxArgs { + /// The destination of the transaction. + /// + /// If not provided, you must use `cast mktx --create`. + #[clap(value_parser = NameOrAddress::from_str)] + to: Option, + + /// The signature of the function to call. + sig: Option, + + /// The arguments of the function to call. + args: Vec, + + /// Reuse the latest nonce for the sender account. + #[clap(long, conflicts_with = "nonce")] + resend: bool, + + #[clap(subcommand)] + command: Option, + + #[clap(flatten)] + tx: TransactionOpts, + + #[clap(flatten)] + eth: EthereumOpts, +} + +#[derive(Debug, Parser)] +pub enum MakeTxSubcommands { + /// Use to deploy raw contract bytecode. + #[clap(name = "--create")] + Create { + /// The initialization bytecode of the contract to deploy. + code: String, + + /// The signature of the constructor. + sig: Option, + + /// The constructor arguments. + args: Vec, + }, +} + +impl MakeTxArgs { + pub async fn run(self) -> Result<()> { + let MakeTxArgs { to, mut sig, mut args, resend, command, mut tx, eth } = self; + + let code = if let Some(MakeTxSubcommands::Create { + code, + sig: constructor_sig, + args: constructor_args, + }) = command + { + sig = constructor_sig; + args = constructor_args; + Some(code) + } else { + None + }; + + // ensure mandatory fields are provided + if code.is_none() && to.is_none() { + eyre::bail!("Must specify a recipient address or contract code to deploy"); + } + + let config = Config::from(ð); + let provider = utils::get_provider(&config)?; + let chain = utils::get_chain(config.chain, &provider).await?; + let api_key = config.get_etherscan_api_key(Some(chain)); + + // Retrieve the signer, and bail if it can't be constructed. + let signer = eth.wallet.signer(chain.id()).await?; + let from = signer.address(); + + // prevent misconfigured hwlib from sending a transaction that defies + // user-specified --from + if let Some(specified_from) = eth.wallet.from { + if specified_from != from.to_alloy() { + eyre::bail!( + "\ +The specified sender via CLI/env vars does not match the sender configured via +the hardware wallet's HD Path. +Please use the `--hd-path ` parameter to specify the BIP32 Path which +corresponds to the sender, or let foundry automatically detect it by not specifying any sender address." + ) + } + } + + if resend { + tx.nonce = Some(provider.get_transaction_count(from, None).await?.to_alloy()); + } + + let provider = provider.with_signer(signer); + + let params = sig.as_deref().map(|sig| (sig, args)); + let mut builder = TxBuilder::new(&provider, from, to, chain, tx.legacy).await?; + builder + .etherscan_api_key(api_key) + .gas(tx.gas_limit) + .gas_price(tx.gas_price) + .priority_gas_price(tx.priority_gas_price) + .value(tx.value) + .nonce(tx.nonce); + + if let Some(code) = code { + let mut data = hex::decode(code)?; + + if let Some((sig, args)) = params { + let (mut sigdata, _) = builder.create_args(sig, args).await?; + data.append(&mut sigdata); + } + + builder.set_data(data); + } else { + builder.args(params).await?; + } + let (mut tx, _) = builder.build(); + + // Fill nonce, gas limit, gas price, and max priority fee per gas if needed + provider.fill_transaction(&mut tx, None).await?; + + let signature = provider.sign_transaction(&tx, from).await?; + let signed_tx = tx.rlp_signed(&signature); + println!("{signed_tx}"); + + Ok(()) + } +} diff --git a/crates/cast/bin/cmd/mod.rs b/crates/cast/bin/cmd/mod.rs index bd1c8ddf6a07..6c904417407c 100644 --- a/crates/cast/bin/cmd/mod.rs +++ b/crates/cast/bin/cmd/mod.rs @@ -13,6 +13,7 @@ pub mod estimate; pub mod find_block; pub mod interface; pub mod logs; +pub mod mktx; pub mod rpc; pub mod run; pub mod send; diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index c4ee0e56388e..754ca2e1a427 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -362,6 +362,7 @@ async fn main() -> Result<()> { // Calls & transactions CastSubcommand::Call(cmd) => cmd.run().await?, CastSubcommand::Estimate(cmd) => cmd.run().await?, + CastSubcommand::MakeTx(cmd) => cmd.run().await?, CastSubcommand::PublishTx { raw_tx, cast_async, rpc } => { let config = Config::from(&rpc); let provider = utils::get_provider(&config)?; diff --git a/crates/cast/bin/opts.rs b/crates/cast/bin/opts.rs index 2f09c2534830..9da5be08ea9f 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -1,7 +1,8 @@ use crate::cmd::{ access_list::AccessListArgs, bind::BindArgs, call::CallArgs, create2::Create2Args, estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs, - rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, wallet::WalletSubcommands, + mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, + wallet::WalletSubcommands, }; use alloy_primitives::{Address, B256, U256}; use clap::{Parser, Subcommand, ValueHint}; @@ -381,6 +382,10 @@ pub enum CastSubcommand { bytecode: String, }, + /// Build and sign a transaction. + #[clap(name = "mktx", visible_alias = "m")] + MakeTx(MakeTxArgs), + /// Calculate the ENS namehash of a name. #[clap(visible_aliases = &["na", "nh"])] Namehash { name: Option }, diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index e4009aa4af9f..645ae16f70c3 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -493,6 +493,89 @@ casttest!(logs_sig_2, |_prj, cmd| { ); }); +casttest!(mktx, |_prj, cmd| { + cmd.args([ + "mktx", + "--private-key", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "--chain", + "1", + "--nonce", + "0", + "--value", + "100", + "--gas-limit", + "21000", + "--gas-price", + "10000000000", + "--priority-gas-price", + "1000000000", + "0x0000000000000000000000000000000000000001", + ]); + let output = cmd.stdout_lossy(); + assert_eq!( + output.trim(), + "0x02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000016480c001a070d55e79ed3ac9fc8f51e78eb91fd054720d943d66633f2eb1bc960f0126b0eca052eda05a792680de3181e49bab4093541f75b49d1ecbe443077b3660c836016a" + ); +}); + +// ensure recipient or code is required +casttest!(mktx_requires_to, |_prj, cmd| { + cmd.args([ + "mktx", + "--private-key", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ]); + let output = cmd.stderr_lossy(); + assert_eq!( + output.trim(), + "Error: \nMust specify a recipient address or contract code to deploy" + ); +}); + +casttest!(mktx_signer_from_mismatch, |_prj, cmd| { + cmd.args([ + "mktx", + "--private-key", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "--from", + "0x0000000000000000000000000000000000000001", + "--chain", + "1", + "0x0000000000000000000000000000000000000001", + ]); + let output = cmd.stderr_lossy(); + assert!( + output.contains("The specified sender via CLI/env vars does not match the sender configured via\nthe hardware wallet's HD Path.") + ); +}); + +casttest!(mktx_signer_from_match, |_prj, cmd| { + cmd.args([ + "mktx", + "--private-key", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "--from", + "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf", + "--chain", + "1", + "--nonce", + "0", + "--gas-limit", + "21000", + "--gas-price", + "10000000000", + "--priority-gas-price", + "1000000000", + "0x0000000000000000000000000000000000000001", + ]); + let output = cmd.stdout_lossy(); + assert_eq!( + output.trim(), + "0x02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000018080c001a0cce9a61187b5d18a89ecd27ec675e3b3f10d37f165627ef89a15a7fe76395ce8a07537f5bffb358ffbef22cda84b1c92f7211723f9e09ae037e81686805d3e5505" + ); +}); + // tests that the raw encoded transaction is returned casttest!(tx_raw, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); From 70792cd59adc6c0fe79e18d54dc661fd0dba2040 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Feb 2024 15:17:36 -0800 Subject: [PATCH 2/3] refactor: similar code in `cast send` and `cast mktx` --- crates/cast/bin/cmd/mktx.rs | 46 +++-------------------- crates/cast/bin/cmd/send.rs | 62 +++++++------------------------ crates/cast/bin/main.rs | 1 + crates/cast/bin/tx.rs | 73 +++++++++++++++++++++++++++++++++++++ crates/cast/src/lib.rs | 4 +- 5 files changed, 95 insertions(+), 91 deletions(-) create mode 100644 crates/cast/bin/tx.rs diff --git a/crates/cast/bin/cmd/mktx.rs b/crates/cast/bin/cmd/mktx.rs index d40a1b7c4222..d6e5c901ab21 100644 --- a/crates/cast/bin/cmd/mktx.rs +++ b/crates/cast/bin/cmd/mktx.rs @@ -1,4 +1,4 @@ -use cast::TxBuilder; +use crate::tx; use clap::Parser; use ethers_core::types::NameOrAddress; use ethers_middleware::MiddlewareBuilder; @@ -75,10 +75,7 @@ impl MakeTxArgs { None }; - // ensure mandatory fields are provided - if code.is_none() && to.is_none() { - eyre::bail!("Must specify a recipient address or contract code to deploy"); - } + tx::validate_to_address(&code, &to)?; let config = Config::from(ð); let provider = utils::get_provider(&config)?; @@ -89,19 +86,7 @@ impl MakeTxArgs { let signer = eth.wallet.signer(chain.id()).await?; let from = signer.address(); - // prevent misconfigured hwlib from sending a transaction that defies - // user-specified --from - if let Some(specified_from) = eth.wallet.from { - if specified_from != from.to_alloy() { - eyre::bail!( - "\ -The specified sender via CLI/env vars does not match the sender configured via -the hardware wallet's HD Path. -Please use the `--hd-path ` parameter to specify the BIP32 Path which -corresponds to the sender, or let foundry automatically detect it by not specifying any sender address." - ) - } - } + tx::validate_from_address(eth.wallet.from, from.to_alloy())?; if resend { tx.nonce = Some(provider.get_transaction_count(from, None).await?.to_alloy()); @@ -109,29 +94,8 @@ corresponds to the sender, or let foundry automatically detect it by not specify let provider = provider.with_signer(signer); - let params = sig.as_deref().map(|sig| (sig, args)); - let mut builder = TxBuilder::new(&provider, from, to, chain, tx.legacy).await?; - builder - .etherscan_api_key(api_key) - .gas(tx.gas_limit) - .gas_price(tx.gas_price) - .priority_gas_price(tx.priority_gas_price) - .value(tx.value) - .nonce(tx.nonce); - - if let Some(code) = code { - let mut data = hex::decode(code)?; - - if let Some((sig, args)) = params { - let (mut sigdata, _) = builder.create_args(sig, args).await?; - data.append(&mut sigdata); - } - - builder.set_data(data); - } else { - builder.args(params).await?; - } - let (mut tx, _) = builder.build(); + let (mut tx, _) = + tx::build_tx(&provider, from, to, code, sig, args, tx, chain, api_key).await?; // Fill nonce, gas limit, gas price, and max priority fee per gas if needed provider.fill_transaction(&mut tx, None).await?; diff --git a/crates/cast/bin/cmd/send.rs b/crates/cast/bin/cmd/send.rs index b79bf1d7cd9b..73581a6b0276 100644 --- a/crates/cast/bin/cmd/send.rs +++ b/crates/cast/bin/cmd/send.rs @@ -1,4 +1,5 @@ -use cast::{Cast, TxBuilder}; +use crate::tx; +use cast::Cast; use clap::Parser; use ethers_core::types::NameOrAddress; use ethers_middleware::MiddlewareBuilder; @@ -82,7 +83,7 @@ impl SendTxArgs { let SendTxArgs { eth, to, - sig, + mut sig, cast_async, mut args, mut tx, @@ -93,24 +94,20 @@ impl SendTxArgs { unlocked, } = self; - let mut sig = sig.unwrap_or_default(); let code = if let Some(SendTxSubcommands::Create { code, sig: constructor_sig, args: constructor_args, }) = command { - sig = constructor_sig.unwrap_or_default(); + sig = constructor_sig; args = constructor_args; Some(code) } else { None }; - // ensure mandatory fields are provided - if code.is_none() && to.is_none() { - eyre::bail!("Must specify a recipient address or contract code to deploy"); - } + tx::validate_to_address(&code, &to)?; let config = Config::from(ð); let provider = utils::get_provider(&config)?; @@ -155,7 +152,8 @@ impl SendTxArgs { config.sender.to_ethers(), to, code, - (sig, args), + sig, + args, tx, chain, api_key, @@ -173,19 +171,7 @@ impl SendTxArgs { let signer = eth.wallet.signer(chain.id()).await?; let from = signer.address(); - // prevent misconfigured hwlib from sending a transaction that defies - // user-specified --from - if let Some(specified_from) = eth.wallet.from { - if specified_from != from.to_alloy() { - eyre::bail!( - "\ -The specified sender via CLI/env vars does not match the sender configured via -the hardware wallet's HD Path. -Please use the `--hd-path ` parameter to specify the BIP32 Path which -corresponds to the sender, or let foundry automatically detect it by not specifying any sender address." - ) - } - } + tx::validate_from_address(eth.wallet.from, from.to_alloy())?; if resend { tx.nonce = Some(provider.get_transaction_count(from, None).await?.to_alloy()); @@ -198,7 +184,8 @@ corresponds to the sender, or let foundry automatically detect it by not specify from, to, code, - (sig, args), + sig, + args, tx, chain, api_key, @@ -217,7 +204,8 @@ async fn cast_send, T: Into from: F, to: Option, code: Option, - args: (String, Vec), + sig: Option, + args: Vec, tx: TransactionOpts, chain: Chain, etherscan_api_key: Option, @@ -228,30 +216,8 @@ async fn cast_send, T: Into where M::Error: 'static, { - let (sig, params) = args; - let params = if !sig.is_empty() { Some((&sig[..], params)) } else { None }; - let mut builder = TxBuilder::new(&provider, from, to, chain, tx.legacy).await?; - builder - .etherscan_api_key(etherscan_api_key) - .gas(tx.gas_limit) - .gas_price(tx.gas_price) - .priority_gas_price(tx.priority_gas_price) - .value(tx.value) - .nonce(tx.nonce); - - if let Some(code) = code { - let mut data = hex::decode(code)?; - - if let Some((sig, args)) = params { - let (mut sigdata, _) = builder.create_args(sig, args).await?; - data.append(&mut sigdata); - } - - builder.set_data(data); - } else { - builder.args(params).await?; - }; - let builder_output = builder.build(); + let builder_output = + tx::build_tx(&provider, from, to, code, sig, args, tx, chain, etherscan_api_key).await?; let cast = Cast::new(provider); diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 754ca2e1a427..b577ee9858c9 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -26,6 +26,7 @@ use std::time::Instant; pub mod cmd; pub mod opts; +pub mod tx; use opts::{Cast as Opts, CastSubcommand, ToBaseArgs}; diff --git a/crates/cast/bin/tx.rs b/crates/cast/bin/tx.rs new file mode 100644 index 000000000000..cd155de011d2 --- /dev/null +++ b/crates/cast/bin/tx.rs @@ -0,0 +1,73 @@ +use alloy_primitives::Address; +use cast::{TxBuilder, TxBuilderOutput}; +use ethers_core::types::NameOrAddress; +use ethers_providers::Middleware; +use eyre::Result; +use foundry_cli::opts::TransactionOpts; +use foundry_config::Chain; + +/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from +pub fn validate_from_address( + specified_from: Option
, + signer_address: Address, +) -> Result<()> { + if let Some(specified_from) = specified_from { + if specified_from != signer_address { + eyre::bail!( + "\ +The specified sender via CLI/env vars does not match the sender configured via +the hardware wallet's HD Path. +Please use the `--hd-path ` parameter to specify the BIP32 Path which +corresponds to the sender, or let foundry automatically detect it by not specifying any sender address." + ) + } + } + Ok(()) +} + +/// Ensures the transaction is either a contract deployment or a recipient address is specified +pub fn validate_to_address(code: &Option, to: &Option) -> Result<()> { + if code.is_none() && to.is_none() { + eyre::bail!("Must specify a recipient address or contract code to deploy"); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub async fn build_tx, T: Into>( + provider: &M, + from: F, + to: Option, + code: Option, + sig: Option, + args: Vec, + tx: TransactionOpts, + chain: impl Into, + etherscan_api_key: Option, +) -> Result { + let mut builder = TxBuilder::new(provider, from, to, chain, tx.legacy).await?; + builder + .etherscan_api_key(etherscan_api_key) + .gas(tx.gas_limit) + .gas_price(tx.gas_price) + .priority_gas_price(tx.priority_gas_price) + .value(tx.value) + .nonce(tx.nonce); + + let params = sig.as_deref().map(|sig| (sig, args)); + if let Some(code) = code { + let mut data = hex::decode(code)?; + + if let Some((sig, args)) = params { + let (mut sigdata, _) = builder.create_args(sig, args).await?; + data.append(&mut sigdata); + } + + builder.set_data(data); + } else { + builder.args(params).await?; + } + + let builder_output = builder.build(); + Ok(builder_output) +} diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index 9eb81c4ce567..29256fc192e2 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -31,7 +31,7 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; use tokio::signal::ctrl_c; -use tx::{TxBuilderOutput, TxBuilderPeekOutput}; +use tx::TxBuilderPeekOutput; pub use foundry_evm::*; pub use rusoto_core::{ @@ -39,7 +39,7 @@ pub use rusoto_core::{ request::HttpClient as AwsHttpClient, Client as AwsClient, }; pub use rusoto_kms::KmsClient; -pub use tx::TxBuilder; +pub use tx::{TxBuilder, TxBuilderOutput}; pub mod base; pub mod errors; From 97efc694a5b58386ed598f0385b470aa1f03406e Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 1 Mar 2024 10:22:51 +0100 Subject: [PATCH 3/3] update clap --- crates/cast/bin/cmd/mktx.rs | 12 ++++++------ crates/cast/bin/opts.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/cast/bin/cmd/mktx.rs b/crates/cast/bin/cmd/mktx.rs index d6e5c901ab21..dc7f78461dd3 100644 --- a/crates/cast/bin/cmd/mktx.rs +++ b/crates/cast/bin/cmd/mktx.rs @@ -19,7 +19,7 @@ pub struct MakeTxArgs { /// The destination of the transaction. /// /// If not provided, you must use `cast mktx --create`. - #[clap(value_parser = NameOrAddress::from_str)] + #[arg(value_parser = NameOrAddress::from_str)] to: Option, /// The signature of the function to call. @@ -29,16 +29,16 @@ pub struct MakeTxArgs { args: Vec, /// Reuse the latest nonce for the sender account. - #[clap(long, conflicts_with = "nonce")] + #[arg(long, conflicts_with = "nonce")] resend: bool, - #[clap(subcommand)] + #[command(subcommand)] command: Option, - #[clap(flatten)] + #[command(flatten)] tx: TransactionOpts, - #[clap(flatten)] + #[command(flatten)] eth: EthereumOpts, } @@ -83,7 +83,7 @@ impl MakeTxArgs { let api_key = config.get_etherscan_api_key(Some(chain)); // Retrieve the signer, and bail if it can't be constructed. - let signer = eth.wallet.signer(chain.id()).await?; + let signer = eth.wallet.signer().await?; let from = signer.address(); tx::validate_from_address(eth.wallet.from, from.to_alloy())?; diff --git a/crates/cast/bin/opts.rs b/crates/cast/bin/opts.rs index c0bd9e7f9c9a..5a1af1fdc20a 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -383,7 +383,7 @@ pub enum CastSubcommand { }, /// Build and sign a transaction. - #[clap(name = "mktx", visible_alias = "m")] + #[command(name = "mktx", visible_alias = "m")] MakeTx(MakeTxArgs), /// Calculate the ENS namehash of a name.