1use crate::traces::identifier::SignaturesIdentifier;
2use alloy_consensus::{SidecarBuilder, SignableTransaction, SimpleCoder};
3use alloy_dyn_abi::ErrorExt;
4use alloy_ens::NameOrAddress;
5use alloy_json_abi::Function;
6use alloy_network::{
7 AnyNetwork, AnyTypedTransaction, TransactionBuilder, TransactionBuilder4844,
8 TransactionBuilder7702,
9};
10use alloy_primitives::{Address, Bytes, TxKind, U256, hex};
11use alloy_provider::Provider;
12use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind, TransactionRequest};
13use alloy_serde::WithOtherFields;
14use alloy_signer::Signer;
15use alloy_transport::TransportError;
16use clap::Args;
17use eyre::Result;
18use foundry_cli::{
19 opts::{CliAuthorizationList, EthereumOpts, TransactionOpts},
20 utils::{self, LoadConfig, get_provider_builder, parse_function_args},
21};
22use foundry_common::{fmt::format_tokens, provider::RetryProviderWithSigner};
23use foundry_config::{Chain, Config};
24use foundry_wallets::{WalletOpts, WalletSigner};
25use itertools::Itertools;
26use serde_json::value::RawValue;
27use std::{fmt::Write, time::Duration};
28
29#[derive(Debug, Clone, Args)]
30pub struct SendTxOpts {
31 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
33 pub cast_async: bool,
34
35 #[arg(long, conflicts_with = "async")]
38 pub sync: bool,
39
40 #[arg(long, default_value = "1")]
42 pub confirmations: u64,
43
44 #[arg(long, env = "ETH_TIMEOUT")]
46 pub timeout: Option<u64>,
47
48 #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
50 pub poll_interval: Option<u64>,
51
52 #[command(flatten)]
54 pub eth: EthereumOpts,
55}
56
57pub enum SenderKind<'a> {
59 Address(Address),
62 Signer(&'a WalletSigner),
64 OwnedSigner(Box<WalletSigner>),
66}
67
68impl SenderKind<'_> {
69 pub fn address(&self) -> Address {
71 match self {
72 Self::Address(addr) => *addr,
73 Self::Signer(signer) => signer.address(),
74 Self::OwnedSigner(signer) => signer.address(),
75 }
76 }
77
78 pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
86 if let Some(from) = opts.from {
87 Ok(from.into())
88 } else if let Ok(signer) = opts.signer().await {
89 Ok(Self::OwnedSigner(Box::new(signer)))
90 } else {
91 Ok(Address::ZERO.into())
92 }
93 }
94
95 pub fn as_signer(&self) -> Option<&WalletSigner> {
97 match self {
98 Self::Signer(signer) => Some(signer),
99 Self::OwnedSigner(signer) => Some(signer.as_ref()),
100 _ => None,
101 }
102 }
103}
104
105impl From<Address> for SenderKind<'_> {
106 fn from(addr: Address) -> Self {
107 Self::Address(addr)
108 }
109}
110
111impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
112 fn from(signer: &'a WalletSigner) -> Self {
113 Self::Signer(signer)
114 }
115}
116
117impl From<WalletSigner> for SenderKind<'_> {
118 fn from(signer: WalletSigner) -> Self {
119 Self::OwnedSigner(Box::new(signer))
120 }
121}
122
123pub fn validate_from_address(
125 specified_from: Option<Address>,
126 signer_address: Address,
127) -> Result<()> {
128 if let Some(specified_from) = specified_from
129 && specified_from != signer_address
130 {
131 eyre::bail!(
132 "\
133The specified sender via CLI/env vars does not match the sender configured via
134the hardware wallet's HD Path.
135Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
136corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
137 )
138 }
139 Ok(())
140}
141
142#[derive(Debug)]
144pub struct InitState;
145
146#[derive(Debug)]
148pub struct ToState {
149 to: Option<Address>,
150}
151
152#[derive(Debug)]
154pub struct InputState {
155 kind: TxKind,
156 input: Vec<u8>,
157 func: Option<Function>,
158}
159
160#[derive(Debug)]
165pub struct CastTxBuilder<P, S> {
166 provider: P,
167 tx: WithOtherFields<TransactionRequest>,
168 legacy: bool,
170 blob: bool,
171 auth: Vec<CliAuthorizationList>,
172 chain: Chain,
173 etherscan_api_key: Option<String>,
174 access_list: Option<Option<AccessList>>,
175 state: S,
176}
177
178impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
179 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
182 let mut tx = WithOtherFields::<TransactionRequest>::default();
183
184 let chain = utils::get_chain(config.chain, &provider).await?;
185 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
186 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
188
189 if let Some(gas_limit) = tx_opts.gas_limit {
190 tx.set_gas_limit(gas_limit.to());
191 }
192
193 if let Some(value) = tx_opts.value {
194 tx.set_value(value);
195 }
196
197 if let Some(gas_price) = tx_opts.gas_price {
198 if legacy {
199 tx.set_gas_price(gas_price.to());
200 } else {
201 tx.set_max_fee_per_gas(gas_price.to());
202 }
203 }
204
205 if !legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
206 tx.set_max_priority_fee_per_gas(priority_fee.to());
207 }
208
209 if let Some(max_blob_fee) = tx_opts.blob_gas_price {
210 tx.set_max_fee_per_blob_gas(max_blob_fee.to())
211 }
212
213 if let Some(nonce) = tx_opts.nonce {
214 tx.set_nonce(nonce.to());
215 }
216
217 Ok(Self {
218 provider,
219 tx,
220 legacy,
221 blob: tx_opts.blob,
222 chain,
223 etherscan_api_key,
224 auth: tx_opts.auth,
225 access_list: tx_opts.access_list,
226 state: InitState,
227 })
228 }
229
230 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
232 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
233 Ok(CastTxBuilder {
234 provider: self.provider,
235 tx: self.tx,
236 legacy: self.legacy,
237 blob: self.blob,
238 chain: self.chain,
239 etherscan_api_key: self.etherscan_api_key,
240 auth: self.auth,
241 access_list: self.access_list,
242 state: ToState { to },
243 })
244 }
245}
246
247impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
248 pub async fn with_code_sig_and_args(
252 self,
253 code: Option<String>,
254 sig: Option<String>,
255 args: Vec<String>,
256 ) -> Result<CastTxBuilder<P, InputState>> {
257 let (mut args, func) = if let Some(sig) = sig {
258 parse_function_args(
259 &sig,
260 args,
261 self.state.to,
262 self.chain,
263 &self.provider,
264 self.etherscan_api_key.as_deref(),
265 )
266 .await?
267 } else {
268 (Vec::new(), None)
269 };
270
271 let input = if let Some(code) = &code {
272 let mut code = hex::decode(code)?;
273 code.append(&mut args);
274 code
275 } else {
276 args
277 };
278
279 if self.state.to.is_none() && code.is_none() {
280 let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
281 let has_auth = !self.auth.is_empty();
282 if !has_auth || has_value {
285 eyre::bail!("Must specify a recipient address or contract code to deploy");
286 }
287 }
288
289 Ok(CastTxBuilder {
290 provider: self.provider,
291 tx: self.tx,
292 legacy: self.legacy,
293 blob: self.blob,
294 chain: self.chain,
295 etherscan_api_key: self.etherscan_api_key,
296 auth: self.auth,
297 access_list: self.access_list,
298 state: InputState { kind: self.state.to.into(), input, func },
299 })
300 }
301}
302
303impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
304 pub async fn build(
307 self,
308 sender: impl Into<SenderKind<'_>>,
309 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
310 self._build(sender, true, false).await
311 }
312
313 pub async fn build_raw(
316 self,
317 sender: impl Into<SenderKind<'_>>,
318 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
319 self._build(sender, false, false).await
320 }
321
322 pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
326 let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
327 let tx = tx.build_unsigned()?;
328 match tx {
329 AnyTypedTransaction::Ethereum(t) => Ok(hex::encode_prefixed(t.encoded_for_signing())),
330 _ => eyre::bail!("Cannot generate unsigned transaction for non-Ethereum transactions"),
331 }
332 }
333
334 async fn _build(
335 mut self,
336 sender: impl Into<SenderKind<'_>>,
337 fill: bool,
338 unsigned: bool,
339 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
340 let sender = sender.into();
341 let from = sender.address();
342
343 self.tx.set_kind(self.state.kind);
344
345 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
347
348 self.tx.set_from(from);
349 self.tx.set_chain_id(self.chain.id());
350
351 let tx_nonce = if let Some(nonce) = self.tx.nonce {
352 nonce
353 } else {
354 let nonce = self.provider.get_transaction_count(from).await?;
355 if fill {
356 self.tx.nonce = Some(nonce);
357 }
358 nonce
359 };
360
361 if !unsigned {
362 self.resolve_auth(sender, tx_nonce).await?;
363 } else if !self.auth.is_empty() {
364 let mut signed_auths = Vec::with_capacity(self.auth.len());
365 for auth in std::mem::take(&mut self.auth) {
366 let CliAuthorizationList::Signed(signed_auth) = auth else {
367 eyre::bail!(
368 "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
369 )
370 };
371 signed_auths.push(signed_auth);
372 }
373
374 self.tx.set_authorization_list(signed_auths);
375 }
376
377 if let Some(access_list) = match self.access_list.take() {
378 None => None,
379 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
381 Some(Some(access_list)) => Some(access_list),
383 } {
384 self.tx.set_access_list(access_list);
385 }
386
387 if !fill {
388 return Ok((self.tx, self.state.func));
389 }
390
391 if self.legacy && self.tx.gas_price.is_none() {
392 self.tx.gas_price = Some(self.provider.get_gas_price().await?);
393 }
394
395 if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
396 self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
397 }
398
399 if !self.legacy
400 && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
401 {
402 let estimate = self.provider.estimate_eip1559_fees().await?;
403
404 if self.tx.max_fee_per_gas.is_none() {
405 self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
406 }
407
408 if self.tx.max_priority_fee_per_gas.is_none() {
409 self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
410 }
411 }
412
413 if self.tx.gas.is_none() {
414 self.estimate_gas().await?;
415 }
416
417 Ok((self.tx, self.state.func))
418 }
419
420 async fn estimate_gas(&mut self) -> Result<()> {
422 match self.provider.estimate_gas(self.tx.clone()).await {
423 Ok(estimated) => {
424 self.tx.gas = Some(estimated);
425 Ok(())
426 }
427 Err(err) => {
428 if let TransportError::ErrorResp(payload) = &err {
429 if payload.code == 3
432 && let Some(data) = &payload.data
433 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
434 {
435 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
436 }
437 }
438 eyre::bail!("Failed to estimate gas: {}", err)
439 }
440 }
441 }
442
443 async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
445 if self.auth.is_empty() {
446 return Ok(());
447 }
448
449 let auths = std::mem::take(&mut self.auth);
450
451 let address_auth_count =
454 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
455 if address_auth_count > 1 {
456 eyre::bail!(
457 "Multiple address-based authorizations provided. Only one address can be specified; \
458 use pre-signed authorizations (hex-encoded) for multiple authorizations."
459 );
460 }
461
462 let mut signed_auths = Vec::with_capacity(auths.len());
463
464 for auth in auths {
465 let signed_auth = match auth {
466 CliAuthorizationList::Address(address) => {
467 let auth = Authorization {
468 chain_id: U256::from(self.chain.id()),
469 nonce: tx_nonce + 1,
470 address,
471 };
472
473 let Some(signer) = sender.as_signer() else {
474 eyre::bail!("No signer available to sign authorization");
475 };
476 let signature = signer.sign_hash(&auth.signature_hash()).await?;
477
478 auth.into_signed(signature)
479 }
480 CliAuthorizationList::Signed(auth) => auth,
481 };
482 signed_auths.push(signed_auth);
483 }
484
485 self.tx.set_authorization_list(signed_auths);
486
487 Ok(())
488 }
489}
490
491impl<P, S> CastTxBuilder<P, S>
492where
493 P: Provider<AnyNetwork>,
494{
495 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
497 let Some(blob_data) = blob_data else { return Ok(self) };
498
499 let mut coder = SidecarBuilder::<SimpleCoder>::default();
500 coder.ingest(&blob_data);
501 let sidecar = coder.build()?;
502
503 self.tx.set_blob_sidecar(sidecar);
504 self.tx.populate_blob_hashes();
505
506 Ok(self)
507 }
508}
509
510async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
512 let err_data = serde_json::from_str::<Bytes>(data.get())?;
513 let Some(selector) = err_data.get(..4) else { return Ok(None) };
514 if let Some(known_error) =
515 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
516 {
517 let mut decoded_error = known_error.name.clone();
518 if !known_error.inputs.is_empty()
519 && let Ok(error) = known_error.decode_error(&err_data)
520 {
521 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
522 }
523 return Ok(Some(decoded_error));
524 }
525 Ok(None)
526}
527
528pub(crate) async fn signing_provider(
530 tx_opts: &SendTxOpts,
531) -> eyre::Result<RetryProviderWithSigner> {
532 let config = tx_opts.eth.load_config()?;
533 let signer = tx_opts.eth.wallet.signer().await?;
534 let wallet = alloy_network::EthereumWallet::from(signer);
535 let provider = get_provider_builder(&config)?.build_with_wallet(wallet)?;
536 if let Some(interval) = tx_opts.poll_interval {
537 provider.client().set_poll_interval(Duration::from_secs(interval))
538 }
539 Ok(provider)
540}