Skip to content

Commit e57e82c

Browse files
aymattsse
andauthored
feat: cast mktx (#7056)
* feat: `cast mktx` * refactor: similar code in `cast send` and `cast mktx` * update clap --------- Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
1 parent b671576 commit e57e82c

File tree

8 files changed

+290
-51
lines changed

8 files changed

+290
-51
lines changed

crates/cast/bin/cmd/mktx.rs

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use crate::tx;
2+
use clap::Parser;
3+
use ethers_core::types::NameOrAddress;
4+
use ethers_middleware::MiddlewareBuilder;
5+
use ethers_providers::Middleware;
6+
use ethers_signers::Signer;
7+
use eyre::Result;
8+
use foundry_cli::{
9+
opts::{EthereumOpts, TransactionOpts},
10+
utils,
11+
};
12+
use foundry_common::types::ToAlloy;
13+
use foundry_config::Config;
14+
use std::str::FromStr;
15+
16+
/// CLI arguments for `cast mktx`.
17+
#[derive(Debug, Parser)]
18+
pub struct MakeTxArgs {
19+
/// The destination of the transaction.
20+
///
21+
/// If not provided, you must use `cast mktx --create`.
22+
#[arg(value_parser = NameOrAddress::from_str)]
23+
to: Option<NameOrAddress>,
24+
25+
/// The signature of the function to call.
26+
sig: Option<String>,
27+
28+
/// The arguments of the function to call.
29+
args: Vec<String>,
30+
31+
/// Reuse the latest nonce for the sender account.
32+
#[arg(long, conflicts_with = "nonce")]
33+
resend: bool,
34+
35+
#[command(subcommand)]
36+
command: Option<MakeTxSubcommands>,
37+
38+
#[command(flatten)]
39+
tx: TransactionOpts,
40+
41+
#[command(flatten)]
42+
eth: EthereumOpts,
43+
}
44+
45+
#[derive(Debug, Parser)]
46+
pub enum MakeTxSubcommands {
47+
/// Use to deploy raw contract bytecode.
48+
#[clap(name = "--create")]
49+
Create {
50+
/// The initialization bytecode of the contract to deploy.
51+
code: String,
52+
53+
/// The signature of the constructor.
54+
sig: Option<String>,
55+
56+
/// The constructor arguments.
57+
args: Vec<String>,
58+
},
59+
}
60+
61+
impl MakeTxArgs {
62+
pub async fn run(self) -> Result<()> {
63+
let MakeTxArgs { to, mut sig, mut args, resend, command, mut tx, eth } = self;
64+
65+
let code = if let Some(MakeTxSubcommands::Create {
66+
code,
67+
sig: constructor_sig,
68+
args: constructor_args,
69+
}) = command
70+
{
71+
sig = constructor_sig;
72+
args = constructor_args;
73+
Some(code)
74+
} else {
75+
None
76+
};
77+
78+
tx::validate_to_address(&code, &to)?;
79+
80+
let config = Config::from(&eth);
81+
let provider = utils::get_provider(&config)?;
82+
let chain = utils::get_chain(config.chain, &provider).await?;
83+
let api_key = config.get_etherscan_api_key(Some(chain));
84+
85+
// Retrieve the signer, and bail if it can't be constructed.
86+
let signer = eth.wallet.signer().await?;
87+
let from = signer.address();
88+
89+
tx::validate_from_address(eth.wallet.from, from.to_alloy())?;
90+
91+
if resend {
92+
tx.nonce = Some(provider.get_transaction_count(from, None).await?.to_alloy());
93+
}
94+
95+
let provider = provider.with_signer(signer);
96+
97+
let (mut tx, _) =
98+
tx::build_tx(&provider, from, to, code, sig, args, tx, chain, api_key).await?;
99+
100+
// Fill nonce, gas limit, gas price, and max priority fee per gas if needed
101+
provider.fill_transaction(&mut tx, None).await?;
102+
103+
let signature = provider.sign_transaction(&tx, from).await?;
104+
let signed_tx = tx.rlp_signed(&signature);
105+
println!("{signed_tx}");
106+
107+
Ok(())
108+
}
109+
}

crates/cast/bin/cmd/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod estimate;
1313
pub mod find_block;
1414
pub mod interface;
1515
pub mod logs;
16+
pub mod mktx;
1617
pub mod rpc;
1718
pub mod run;
1819
pub mod send;

crates/cast/bin/cmd/send.rs

+14-48
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use cast::{Cast, TxBuilder};
1+
use crate::tx;
2+
use cast::Cast;
23
use clap::Parser;
34
use ethers_core::types::NameOrAddress;
45
use ethers_middleware::SignerMiddleware;
@@ -82,7 +83,7 @@ impl SendTxArgs {
8283
let SendTxArgs {
8384
eth,
8485
to,
85-
sig,
86+
mut sig,
8687
cast_async,
8788
mut args,
8889
mut tx,
@@ -93,24 +94,20 @@ impl SendTxArgs {
9394
unlocked,
9495
} = self;
9596

96-
let mut sig = sig.unwrap_or_default();
9797
let code = if let Some(SendTxSubcommands::Create {
9898
code,
9999
sig: constructor_sig,
100100
args: constructor_args,
101101
}) = command
102102
{
103-
sig = constructor_sig.unwrap_or_default();
103+
sig = constructor_sig;
104104
args = constructor_args;
105105
Some(code)
106106
} else {
107107
None
108108
};
109109

110-
// ensure mandatory fields are provided
111-
if code.is_none() && to.is_none() {
112-
eyre::bail!("Must specify a recipient address or contract code to deploy");
113-
}
110+
tx::validate_to_address(&code, &to)?;
114111

115112
let config = Config::from(&eth);
116113
let provider = utils::get_provider(&config)?;
@@ -155,7 +152,8 @@ impl SendTxArgs {
155152
config.sender.to_ethers(),
156153
to,
157154
code,
158-
(sig, args),
155+
sig,
156+
args,
159157
tx,
160158
chain,
161159
api_key,
@@ -173,19 +171,7 @@ impl SendTxArgs {
173171
let signer = eth.wallet.signer().await?;
174172
let from = signer.address();
175173

176-
// prevent misconfigured hwlib from sending a transaction that defies
177-
// user-specified --from
178-
if let Some(specified_from) = eth.wallet.from {
179-
if specified_from != from.to_alloy() {
180-
eyre::bail!(
181-
"\
182-
The specified sender via CLI/env vars does not match the sender configured via
183-
the hardware wallet's HD Path.
184-
Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
185-
corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
186-
)
187-
}
188-
}
174+
tx::validate_from_address(eth.wallet.from, from.to_alloy())?;
189175

190176
if resend {
191177
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
198184
from,
199185
to,
200186
code,
201-
(sig, args),
187+
sig,
188+
args,
202189
tx,
203190
chain,
204191
api_key,
@@ -217,7 +204,8 @@ async fn cast_send<M: Middleware, F: Into<NameOrAddress>, T: Into<NameOrAddress>
217204
from: F,
218205
to: Option<T>,
219206
code: Option<String>,
220-
args: (String, Vec<String>),
207+
sig: Option<String>,
208+
args: Vec<String>,
221209
tx: TransactionOpts,
222210
chain: Chain,
223211
etherscan_api_key: Option<String>,
@@ -228,30 +216,8 @@ async fn cast_send<M: Middleware, F: Into<NameOrAddress>, T: Into<NameOrAddress>
228216
where
229217
M::Error: 'static,
230218
{
231-
let (sig, params) = args;
232-
let params = if !sig.is_empty() { Some((&sig[..], params)) } else { None };
233-
let mut builder = TxBuilder::new(&provider, from, to, chain, tx.legacy).await?;
234-
builder
235-
.etherscan_api_key(etherscan_api_key)
236-
.gas(tx.gas_limit)
237-
.gas_price(tx.gas_price)
238-
.priority_gas_price(tx.priority_gas_price)
239-
.value(tx.value)
240-
.nonce(tx.nonce);
241-
242-
if let Some(code) = code {
243-
let mut data = hex::decode(code)?;
244-
245-
if let Some((sig, args)) = params {
246-
let (mut sigdata, _) = builder.create_args(sig, args).await?;
247-
data.append(&mut sigdata);
248-
}
249-
250-
builder.set_data(data);
251-
} else {
252-
builder.args(params).await?;
253-
};
254-
let builder_output = builder.build();
219+
let builder_output =
220+
tx::build_tx(&provider, from, to, code, sig, args, tx, chain, etherscan_api_key).await?;
255221

256222
let cast = Cast::new(provider);
257223

crates/cast/bin/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::time::Instant;
2626

2727
pub mod cmd;
2828
pub mod opts;
29+
pub mod tx;
2930

3031
use opts::{Cast as Opts, CastSubcommand, ToBaseArgs};
3132

@@ -366,6 +367,7 @@ async fn main() -> Result<()> {
366367
// Calls & transactions
367368
CastSubcommand::Call(cmd) => cmd.run().await?,
368369
CastSubcommand::Estimate(cmd) => cmd.run().await?,
370+
CastSubcommand::MakeTx(cmd) => cmd.run().await?,
369371
CastSubcommand::PublishTx { raw_tx, cast_async, rpc } => {
370372
let config = Config::from(&rpc);
371373
let provider = utils::get_provider(&config)?;

crates/cast/bin/opts.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::cmd::{
22
access_list::AccessListArgs, bind::BindArgs, call::CallArgs, create2::Create2Args,
33
estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs,
4-
rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, wallet::WalletSubcommands,
4+
mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs,
5+
wallet::WalletSubcommands,
56
};
67
use alloy_primitives::{Address, B256, U256};
78
use clap::{Parser, Subcommand, ValueHint};
@@ -381,6 +382,10 @@ pub enum CastSubcommand {
381382
bytecode: String,
382383
},
383384

385+
/// Build and sign a transaction.
386+
#[command(name = "mktx", visible_alias = "m")]
387+
MakeTx(MakeTxArgs),
388+
384389
/// Calculate the ENS namehash of a name.
385390
#[command(visible_aliases = &["na", "nh"])]
386391
Namehash { name: Option<String> },

crates/cast/bin/tx.rs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use alloy_primitives::Address;
2+
use cast::{TxBuilder, TxBuilderOutput};
3+
use ethers_core::types::NameOrAddress;
4+
use ethers_providers::Middleware;
5+
use eyre::Result;
6+
use foundry_cli::opts::TransactionOpts;
7+
use foundry_config::Chain;
8+
9+
/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
10+
pub fn validate_from_address(
11+
specified_from: Option<Address>,
12+
signer_address: Address,
13+
) -> Result<()> {
14+
if let Some(specified_from) = specified_from {
15+
if specified_from != signer_address {
16+
eyre::bail!(
17+
"\
18+
The specified sender via CLI/env vars does not match the sender configured via
19+
the hardware wallet's HD Path.
20+
Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
21+
corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
22+
)
23+
}
24+
}
25+
Ok(())
26+
}
27+
28+
/// Ensures the transaction is either a contract deployment or a recipient address is specified
29+
pub fn validate_to_address(code: &Option<String>, to: &Option<NameOrAddress>) -> Result<()> {
30+
if code.is_none() && to.is_none() {
31+
eyre::bail!("Must specify a recipient address or contract code to deploy");
32+
}
33+
Ok(())
34+
}
35+
36+
#[allow(clippy::too_many_arguments)]
37+
pub async fn build_tx<M: Middleware, F: Into<NameOrAddress>, T: Into<NameOrAddress>>(
38+
provider: &M,
39+
from: F,
40+
to: Option<T>,
41+
code: Option<String>,
42+
sig: Option<String>,
43+
args: Vec<String>,
44+
tx: TransactionOpts,
45+
chain: impl Into<Chain>,
46+
etherscan_api_key: Option<String>,
47+
) -> Result<TxBuilderOutput> {
48+
let mut builder = TxBuilder::new(provider, from, to, chain, tx.legacy).await?;
49+
builder
50+
.etherscan_api_key(etherscan_api_key)
51+
.gas(tx.gas_limit)
52+
.gas_price(tx.gas_price)
53+
.priority_gas_price(tx.priority_gas_price)
54+
.value(tx.value)
55+
.nonce(tx.nonce);
56+
57+
let params = sig.as_deref().map(|sig| (sig, args));
58+
if let Some(code) = code {
59+
let mut data = hex::decode(code)?;
60+
61+
if let Some((sig, args)) = params {
62+
let (mut sigdata, _) = builder.create_args(sig, args).await?;
63+
data.append(&mut sigdata);
64+
}
65+
66+
builder.set_data(data);
67+
} else {
68+
builder.args(params).await?;
69+
}
70+
71+
let builder_output = builder.build();
72+
Ok(builder_output)
73+
}

crates/cast/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use std::{
3434
sync::atomic::{AtomicBool, Ordering},
3535
};
3636
use tokio::signal::ctrl_c;
37-
use tx::{TxBuilderOutput, TxBuilderPeekOutput};
37+
use tx::TxBuilderPeekOutput;
3838

3939
use foundry_common::abi::encode_function_args_packed;
4040
pub use foundry_evm::*;
@@ -43,7 +43,7 @@ pub use rusoto_core::{
4343
request::HttpClient as AwsHttpClient, Client as AwsClient,
4444
};
4545
pub use rusoto_kms::KmsClient;
46-
pub use tx::TxBuilder;
46+
pub use tx::{TxBuilder, TxBuilderOutput};
4747

4848
pub mod base;
4949
pub mod errors;

0 commit comments

Comments
 (0)