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, TxHash, TxKind, U256, hex};
11use alloy_provider::{PendingTransactionBuilder, 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, WrapErr};
18use foundry_cli::{
19 opts::{CliAuthorizationList, EthereumOpts, TransactionOpts},
20 utils::{self, LoadConfig, get_provider_builder, parse_function_args},
21};
22use foundry_common::{
23 TransactionReceiptWithRevertReason, fmt::*, get_pretty_tx_receipt_attr,
24 provider::RetryProviderWithSigner, shell,
25};
26use foundry_config::{Chain, Config};
27use foundry_wallets::{WalletOpts, WalletSigner};
28use itertools::Itertools;
29use serde_json::value::RawValue;
30use std::{fmt::Write, str::FromStr, time::Duration};
31
32#[derive(Debug, Clone, Args)]
33pub struct SendTxOpts {
34 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
36 pub cast_async: bool,
37
38 #[arg(long, conflicts_with = "async")]
41 pub sync: bool,
42
43 #[arg(long, default_value = "1")]
45 pub confirmations: u64,
46
47 #[arg(long, env = "ETH_TIMEOUT")]
49 pub timeout: Option<u64>,
50
51 #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
53 pub poll_interval: Option<u64>,
54
55 #[command(flatten)]
57 pub eth: EthereumOpts,
58}
59
60pub enum SenderKind<'a> {
62 Address(Address),
65 Signer(&'a WalletSigner),
67 OwnedSigner(Box<WalletSigner>),
69}
70
71impl SenderKind<'_> {
72 pub fn address(&self) -> Address {
74 match self {
75 Self::Address(addr) => *addr,
76 Self::Signer(signer) => signer.address(),
77 Self::OwnedSigner(signer) => signer.address(),
78 }
79 }
80
81 pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
89 if let Some(from) = opts.from {
90 Ok(from.into())
91 } else if let Ok(signer) = opts.signer().await {
92 Ok(Self::OwnedSigner(Box::new(signer)))
93 } else {
94 Ok(Address::ZERO.into())
95 }
96 }
97
98 pub fn as_signer(&self) -> Option<&WalletSigner> {
100 match self {
101 Self::Signer(signer) => Some(signer),
102 Self::OwnedSigner(signer) => Some(signer.as_ref()),
103 _ => None,
104 }
105 }
106}
107
108impl From<Address> for SenderKind<'_> {
109 fn from(addr: Address) -> Self {
110 Self::Address(addr)
111 }
112}
113
114impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
115 fn from(signer: &'a WalletSigner) -> Self {
116 Self::Signer(signer)
117 }
118}
119
120impl From<WalletSigner> for SenderKind<'_> {
121 fn from(signer: WalletSigner) -> Self {
122 Self::OwnedSigner(Box::new(signer))
123 }
124}
125
126pub fn validate_from_address(
128 specified_from: Option<Address>,
129 signer_address: Address,
130) -> Result<()> {
131 if let Some(specified_from) = specified_from
132 && specified_from != signer_address
133 {
134 eyre::bail!(
135 "\
136The specified sender via CLI/env vars does not match the sender configured via
137the hardware wallet's HD Path.
138Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
139corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
140 )
141 }
142 Ok(())
143}
144
145#[derive(Debug)]
147pub struct InitState;
148
149#[derive(Debug)]
151pub struct ToState {
152 to: Option<Address>,
153}
154
155#[derive(Debug)]
157pub struct InputState {
158 kind: TxKind,
159 input: Vec<u8>,
160 func: Option<Function>,
161}
162
163pub struct CastTxSender<P> {
164 provider: P,
165}
166
167impl<P: Provider<AnyNetwork>> CastTxSender<P> {
168 pub fn new(provider: P) -> Self {
170 Self { provider }
171 }
172
173 pub async fn send_sync(&self, tx: WithOtherFields<TransactionRequest>) -> Result<String> {
175 let mut receipt: TransactionReceiptWithRevertReason =
176 self.provider.send_transaction_sync(tx).await?.into();
177
178 let _ = receipt.update_revert_reason(&self.provider).await;
180
181 self.format_receipt(receipt, None)
182 }
183
184 pub async fn send(
219 &self,
220 tx: WithOtherFields<TransactionRequest>,
221 ) -> Result<PendingTransactionBuilder<AnyNetwork>> {
222 let res = self.provider.send_transaction(tx).await?;
223
224 Ok(res)
225 }
226
227 pub async fn receipt(
244 &self,
245 tx_hash: String,
246 field: Option<String>,
247 confs: u64,
248 timeout: Option<u64>,
249 cast_async: bool,
250 ) -> Result<String> {
251 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
252
253 let mut receipt: TransactionReceiptWithRevertReason =
254 match self.provider.get_transaction_receipt(tx_hash).await? {
255 Some(r) => r,
256 None => {
257 if cast_async {
260 eyre::bail!("tx not found: {:?}", tx_hash)
261 } else {
262 PendingTransactionBuilder::new(self.provider.root().clone(), tx_hash)
263 .with_required_confirmations(confs)
264 .with_timeout(timeout.map(Duration::from_secs))
265 .get_receipt()
266 .await?
267 }
268 }
269 }
270 .into();
271
272 let _ = receipt.update_revert_reason(&self.provider).await;
274
275 self.format_receipt(receipt, field)
276 }
277
278 fn format_receipt(
280 &self,
281 receipt: TransactionReceiptWithRevertReason,
282 field: Option<String>,
283 ) -> Result<String> {
284 Ok(if let Some(ref field) = field {
285 get_pretty_tx_receipt_attr(&receipt, field)
286 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
287 } else if shell::is_json() {
288 serde_json::to_value(&receipt)?.to_string()
290 } else {
291 receipt.pretty()
292 })
293 }
294}
295
296#[derive(Debug)]
301pub struct CastTxBuilder<P, S> {
302 provider: P,
303 tx: WithOtherFields<TransactionRequest>,
304 legacy: bool,
306 blob: bool,
307 auth: Vec<CliAuthorizationList>,
308 chain: Chain,
309 etherscan_api_key: Option<String>,
310 access_list: Option<Option<AccessList>>,
311 state: S,
312}
313
314impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
315 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
318 let mut tx = WithOtherFields::<TransactionRequest>::default();
319
320 let chain = utils::get_chain(config.chain, &provider).await?;
321 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
322 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
324
325 if let Some(gas_limit) = tx_opts.gas_limit {
326 tx.set_gas_limit(gas_limit.to());
327 }
328
329 if let Some(value) = tx_opts.value {
330 tx.set_value(value);
331 }
332
333 if let Some(gas_price) = tx_opts.gas_price {
334 if legacy {
335 tx.set_gas_price(gas_price.to());
336 } else {
337 tx.set_max_fee_per_gas(gas_price.to());
338 }
339 }
340
341 if !legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
342 tx.set_max_priority_fee_per_gas(priority_fee.to());
343 }
344
345 if let Some(max_blob_fee) = tx_opts.blob_gas_price {
346 tx.set_max_fee_per_blob_gas(max_blob_fee.to())
347 }
348
349 if let Some(nonce) = tx_opts.nonce {
350 tx.set_nonce(nonce.to());
351 }
352
353 Ok(Self {
354 provider,
355 tx,
356 legacy,
357 blob: tx_opts.blob,
358 chain,
359 etherscan_api_key,
360 auth: tx_opts.auth,
361 access_list: tx_opts.access_list,
362 state: InitState,
363 })
364 }
365
366 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
368 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
369 Ok(CastTxBuilder {
370 provider: self.provider,
371 tx: self.tx,
372 legacy: self.legacy,
373 blob: self.blob,
374 chain: self.chain,
375 etherscan_api_key: self.etherscan_api_key,
376 auth: self.auth,
377 access_list: self.access_list,
378 state: ToState { to },
379 })
380 }
381}
382
383impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
384 pub async fn with_code_sig_and_args(
388 self,
389 code: Option<String>,
390 sig: Option<String>,
391 args: Vec<String>,
392 ) -> Result<CastTxBuilder<P, InputState>> {
393 let (mut args, func) = if let Some(sig) = sig {
394 parse_function_args(
395 &sig,
396 args,
397 self.state.to,
398 self.chain,
399 &self.provider,
400 self.etherscan_api_key.as_deref(),
401 )
402 .await?
403 } else {
404 (Vec::new(), None)
405 };
406
407 let input = if let Some(code) = &code {
408 let mut code = hex::decode(code)?;
409 code.append(&mut args);
410 code
411 } else {
412 args
413 };
414
415 if self.state.to.is_none() && code.is_none() {
416 let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
417 let has_auth = !self.auth.is_empty();
418 if !has_auth || has_value {
421 eyre::bail!("Must specify a recipient address or contract code to deploy");
422 }
423 }
424
425 Ok(CastTxBuilder {
426 provider: self.provider,
427 tx: self.tx,
428 legacy: self.legacy,
429 blob: self.blob,
430 chain: self.chain,
431 etherscan_api_key: self.etherscan_api_key,
432 auth: self.auth,
433 access_list: self.access_list,
434 state: InputState { kind: self.state.to.into(), input, func },
435 })
436 }
437}
438
439impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
440 pub async fn build(
443 self,
444 sender: impl Into<SenderKind<'_>>,
445 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
446 self._build(sender, true, false).await
447 }
448
449 pub async fn build_raw(
452 self,
453 sender: impl Into<SenderKind<'_>>,
454 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
455 self._build(sender, false, false).await
456 }
457
458 pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
462 let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
463 let tx = tx.build_unsigned()?;
464 match tx {
465 AnyTypedTransaction::Ethereum(t) => Ok(hex::encode_prefixed(t.encoded_for_signing())),
466 _ => eyre::bail!("Cannot generate unsigned transaction for non-Ethereum transactions"),
467 }
468 }
469
470 async fn _build(
471 mut self,
472 sender: impl Into<SenderKind<'_>>,
473 fill: bool,
474 unsigned: bool,
475 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
476 let sender = sender.into();
477 let from = sender.address();
478
479 self.tx.set_kind(self.state.kind);
480
481 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
483
484 self.tx.set_from(from);
485 self.tx.set_chain_id(self.chain.id());
486
487 let tx_nonce = if let Some(nonce) = self.tx.nonce {
488 nonce
489 } else {
490 let nonce = self.provider.get_transaction_count(from).await?;
491 if fill {
492 self.tx.nonce = Some(nonce);
493 }
494 nonce
495 };
496
497 if !unsigned {
498 self.resolve_auth(sender, tx_nonce).await?;
499 } else if !self.auth.is_empty() {
500 let mut signed_auths = Vec::with_capacity(self.auth.len());
501 for auth in std::mem::take(&mut self.auth) {
502 let CliAuthorizationList::Signed(signed_auth) = auth else {
503 eyre::bail!(
504 "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
505 )
506 };
507 signed_auths.push(signed_auth);
508 }
509
510 self.tx.set_authorization_list(signed_auths);
511 }
512
513 if let Some(access_list) = match self.access_list.take() {
514 None => None,
515 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
517 Some(Some(access_list)) => Some(access_list),
519 } {
520 self.tx.set_access_list(access_list);
521 }
522
523 if !fill {
524 return Ok((self.tx, self.state.func));
525 }
526
527 if self.legacy && self.tx.gas_price.is_none() {
528 self.tx.gas_price = Some(self.provider.get_gas_price().await?);
529 }
530
531 if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
532 self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
533 }
534
535 if !self.legacy
536 && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
537 {
538 let estimate = self.provider.estimate_eip1559_fees().await?;
539
540 if self.tx.max_fee_per_gas.is_none() {
541 self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
542 }
543
544 if self.tx.max_priority_fee_per_gas.is_none() {
545 self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
546 }
547 }
548
549 if self.tx.gas.is_none() {
550 self.estimate_gas().await?;
551 }
552
553 Ok((self.tx, self.state.func))
554 }
555
556 async fn estimate_gas(&mut self) -> Result<()> {
558 match self.provider.estimate_gas(self.tx.clone()).await {
559 Ok(estimated) => {
560 self.tx.gas = Some(estimated);
561 Ok(())
562 }
563 Err(err) => {
564 if let TransportError::ErrorResp(payload) = &err {
565 if payload.code == 3
568 && let Some(data) = &payload.data
569 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
570 {
571 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
572 }
573 }
574 eyre::bail!("Failed to estimate gas: {}", err)
575 }
576 }
577 }
578
579 async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
581 if self.auth.is_empty() {
582 return Ok(());
583 }
584
585 let auths = std::mem::take(&mut self.auth);
586
587 let address_auth_count =
590 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
591 if address_auth_count > 1 {
592 eyre::bail!(
593 "Multiple address-based authorizations provided. Only one address can be specified; \
594 use pre-signed authorizations (hex-encoded) for multiple authorizations."
595 );
596 }
597
598 let mut signed_auths = Vec::with_capacity(auths.len());
599
600 for auth in auths {
601 let signed_auth = match auth {
602 CliAuthorizationList::Address(address) => {
603 let auth = Authorization {
604 chain_id: U256::from(self.chain.id()),
605 nonce: tx_nonce + 1,
606 address,
607 };
608
609 let Some(signer) = sender.as_signer() else {
610 eyre::bail!("No signer available to sign authorization");
611 };
612 let signature = signer.sign_hash(&auth.signature_hash()).await?;
613
614 auth.into_signed(signature)
615 }
616 CliAuthorizationList::Signed(auth) => auth,
617 };
618 signed_auths.push(signed_auth);
619 }
620
621 self.tx.set_authorization_list(signed_auths);
622
623 Ok(())
624 }
625}
626
627impl<P, S> CastTxBuilder<P, S>
628where
629 P: Provider<AnyNetwork>,
630{
631 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
633 let Some(blob_data) = blob_data else { return Ok(self) };
634
635 let mut coder = SidecarBuilder::<SimpleCoder>::default();
636 coder.ingest(&blob_data);
637 let sidecar = coder.build()?;
638
639 self.tx.set_blob_sidecar(sidecar);
640 self.tx.populate_blob_hashes();
641
642 Ok(self)
643 }
644}
645
646async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
648 let err_data = serde_json::from_str::<Bytes>(data.get())?;
649 let Some(selector) = err_data.get(..4) else { return Ok(None) };
650 if let Some(known_error) =
651 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
652 {
653 let mut decoded_error = known_error.name.clone();
654 if !known_error.inputs.is_empty()
655 && let Ok(error) = known_error.decode_error(&err_data)
656 {
657 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
658 }
659 return Ok(Some(decoded_error));
660 }
661 Ok(None)
662}
663
664pub(crate) async fn signing_provider(
666 tx_opts: &SendTxOpts,
667) -> eyre::Result<RetryProviderWithSigner> {
668 let config = tx_opts.eth.load_config()?;
669 let signer = tx_opts.eth.wallet.signer().await?;
670 let wallet = alloy_network::EthereumWallet::from(signer);
671 let provider = get_provider_builder(&config)?.build_with_wallet(wallet)?;
672 if let Some(interval) = tx_opts.poll_interval {
673 provider.client().set_poll_interval(Duration::from_secs(interval))
674 }
675 Ok(provider)
676}