1use crate::traces::identifier::SignaturesIdentifier;
2use alloy_consensus::{SidecarBuilder, SimpleCoder};
3use alloy_dyn_abi::ErrorExt;
4use alloy_ens::NameOrAddress;
5use alloy_json_abi::Function;
6use alloy_network::{Network, TransactionBuilder};
7use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U64, U256, hex};
8use alloy_provider::{PendingTransactionBuilder, Provider};
9use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind};
10use alloy_signer::Signer;
11use alloy_transport::TransportError;
12use clap::Args;
13use eyre::{Result, WrapErr};
14use foundry_cli::{
15 opts::{CliAuthorizationList, EthereumOpts, TempoOpts, TransactionOpts},
16 utils::{self, parse_function_args},
17};
18use foundry_common::{
19 FoundryTransactionBuilder, TransactionReceiptWithRevertReason, fmt::*,
20 get_pretty_receipt_w_reason_attr, shell,
21};
22use foundry_config::{Chain, Config};
23use foundry_wallets::{BrowserWalletOpts, TempoAccessKeyConfig, WalletOpts, WalletSigner};
24use itertools::Itertools;
25use serde_json::value::RawValue;
26use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration};
27
28#[derive(Debug, Clone, Args)]
29pub struct SendTxOpts {
30 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
32 pub cast_async: bool,
33
34 #[arg(long, conflicts_with = "async")]
37 pub sync: bool,
38
39 #[arg(long, default_value = "1")]
41 pub confirmations: u64,
42
43 #[arg(long, env = "ETH_TIMEOUT")]
45 pub timeout: Option<u64>,
46
47 #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
49 pub poll_interval: Option<u64>,
50
51 #[command(flatten)]
53 pub eth: EthereumOpts,
54
55 #[command(flatten)]
57 pub browser: BrowserWalletOpts,
58}
59
60#[derive(Debug, Clone, Args)]
62#[command(next_help_heading = "Transaction options")]
63pub struct TxParams {
64 #[arg(long, env = "ETH_GAS_LIMIT")]
66 pub gas_limit: Option<U256>,
67
68 #[arg(long, env = "ETH_GAS_PRICE")]
70 pub gas_price: Option<U256>,
71
72 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
74 pub priority_gas_price: Option<U256>,
75
76 #[arg(long)]
78 pub nonce: Option<U64>,
79
80 #[command(flatten)]
81 pub tempo: TempoOpts,
82}
83
84impl TxParams {
85 pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
86 where
87 N::TransactionRequest: FoundryTransactionBuilder<N>,
88 {
89 if let Some(gas_limit) = self.gas_limit {
90 tx.set_gas_limit(gas_limit.to());
91 }
92
93 if let Some(gas_price) = self.gas_price {
94 if legacy {
95 tx.set_gas_price(gas_price.to());
96 } else {
97 tx.set_max_fee_per_gas(gas_price.to());
98 }
99 }
100
101 if !legacy && let Some(priority_fee) = self.priority_gas_price {
102 tx.set_max_priority_fee_per_gas(priority_fee.to());
103 }
104
105 self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
106 }
107}
108
109pub enum SenderKind<'a> {
111 Address(Address),
114 Signer(&'a WalletSigner),
116 OwnedSigner(Box<WalletSigner>),
118}
119
120impl SenderKind<'_> {
121 pub fn address(&self) -> Address {
123 match self {
124 Self::Address(addr) => *addr,
125 Self::Signer(signer) => signer.address(),
126 Self::OwnedSigner(signer) => signer.address(),
127 }
128 }
129
130 pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
138 if let (Some(signer), _) = opts.maybe_signer().await? {
139 Ok(Self::OwnedSigner(Box::new(signer)))
140 } else if let Some(from) = opts.from {
141 Ok(from.into())
142 } else {
143 Ok(Address::ZERO.into())
144 }
145 }
146
147 pub fn as_signer(&self) -> Option<&WalletSigner> {
149 match self {
150 Self::Signer(signer) => Some(signer),
151 Self::OwnedSigner(signer) => Some(signer.as_ref()),
152 _ => None,
153 }
154 }
155}
156
157impl From<Address> for SenderKind<'_> {
158 fn from(addr: Address) -> Self {
159 Self::Address(addr)
160 }
161}
162
163impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
164 fn from(signer: &'a WalletSigner) -> Self {
165 Self::Signer(signer)
166 }
167}
168
169impl From<WalletSigner> for SenderKind<'_> {
170 fn from(signer: WalletSigner) -> Self {
171 Self::OwnedSigner(Box::new(signer))
172 }
173}
174
175pub fn validate_from_address(
177 specified_from: Option<Address>,
178 signer_address: Address,
179) -> Result<()> {
180 if let Some(specified_from) = specified_from
181 && specified_from != signer_address
182 {
183 eyre::bail!(
184 "\
185The specified sender via CLI/env vars does not match the sender configured via
186the hardware wallet's HD Path.
187Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
188corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
189 )
190 }
191 Ok(())
192}
193
194#[derive(Debug)]
196pub struct InitState;
197
198#[derive(Debug)]
200pub struct ToState {
201 to: Option<Address>,
202}
203
204#[derive(Debug)]
206pub struct InputState {
207 kind: TxKind,
208 input: Vec<u8>,
209 func: Option<Function>,
210}
211
212pub struct CastTxSender<N, P> {
213 provider: P,
214 _phantom: PhantomData<N>,
215}
216
217impl<N: Network, P: Provider<N>> CastTxSender<N, P>
218where
219 N::TransactionRequest: FoundryTransactionBuilder<N>,
220 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
221{
222 pub const fn new(provider: P) -> Self {
224 Self { provider, _phantom: PhantomData }
225 }
226
227 pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<String> {
229 let mut receipt = TransactionReceiptWithRevertReason::<N> {
230 receipt: self.provider.send_transaction_sync(tx).await?,
231 revert_reason: None,
232 };
233 let _ = receipt.update_revert_reason(&self.provider).await;
235
236 self.format_receipt(receipt, None)
237 }
238
239 pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
274 let res = self.provider.send_transaction(tx).await?;
275
276 Ok(res)
277 }
278
279 pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
284 let res = self.provider.send_raw_transaction(raw_tx).await?;
285 Ok(res)
286 }
287
288 pub async fn print_tx_result(
293 &self,
294 tx_hash: B256,
295 cast_async: bool,
296 confs: u64,
297 timeout: u64,
298 ) -> Result<()> {
299 if cast_async {
300 sh_println!("{tx_hash:#x}")?;
301 } else {
302 let receipt =
303 self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
304 sh_println!("{receipt}")?;
305 }
306 Ok(())
307 }
308
309 pub async fn receipt(
326 &self,
327 tx_hash: String,
328 field: Option<String>,
329 confs: u64,
330 timeout: Option<u64>,
331 cast_async: bool,
332 ) -> Result<String> {
333 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
334
335 let mut receipt = TransactionReceiptWithRevertReason::<N> {
336 receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
337 Some(r) => r,
338 None => {
339 if cast_async {
342 eyre::bail!("tx not found: {:?}", tx_hash)
343 }
344 PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
345 .with_required_confirmations(confs)
346 .with_timeout(timeout.map(Duration::from_secs))
347 .get_receipt()
348 .await?
349 }
350 },
351 revert_reason: None,
352 };
353
354 let _ = receipt.update_revert_reason(&self.provider).await;
356
357 self.format_receipt(receipt, field)
358 }
359
360 fn format_receipt(
362 &self,
363 receipt: TransactionReceiptWithRevertReason<N>,
364 field: Option<String>,
365 ) -> Result<String> {
366 Ok(if let Some(ref field) = field {
367 get_pretty_receipt_w_reason_attr(&receipt, field)
368 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
369 } else if shell::is_json() {
370 serde_json::to_value(&receipt)?.to_string()
372 } else {
373 receipt.pretty()
374 })
375 }
376}
377
378#[derive(Debug)]
383pub struct CastTxBuilder<N: Network, P, S> {
384 provider: P,
385 pub(crate) tx: N::TransactionRequest,
386 legacy: bool,
388 blob: bool,
389 eip4844: bool,
391 fill: bool,
394 auth: Vec<CliAuthorizationList>,
395 chain: Chain,
396 etherscan_api_key: Option<String>,
397 etherscan_api_url: Option<String>,
398 access_list: Option<Option<AccessList>>,
399 state: S,
400}
401
402impl<N: Network, P, S> CastTxBuilder<N, P, S> {
403 pub const fn chain(&self) -> Chain {
405 self.chain
406 }
407}
408
409impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
410where
411 N::TransactionRequest: FoundryTransactionBuilder<N>,
412{
413 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
416 let mut tx = N::TransactionRequest::default();
417
418 let chain = utils::get_chain(config.chain, &provider).await?;
419 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
420 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
421 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
422 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
424
425 tx_opts.apply::<N>(&mut tx, legacy);
427
428 Ok(Self {
429 provider,
430 tx,
431 legacy,
432 blob: tx_opts.blob,
433 eip4844: tx_opts.eip4844,
434 fill: true,
435 chain,
436 etherscan_api_key,
437 etherscan_api_url,
438 auth: tx_opts.auth,
439 access_list: tx_opts.access_list,
440 state: InitState,
441 })
442 }
443
444 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
446 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
447 Ok(CastTxBuilder {
448 provider: self.provider,
449 tx: self.tx,
450 legacy: self.legacy,
451 blob: self.blob,
452 eip4844: self.eip4844,
453 fill: self.fill,
454 chain: self.chain,
455 etherscan_api_key: self.etherscan_api_key,
456 etherscan_api_url: self.etherscan_api_url,
457 auth: self.auth,
458 access_list: self.access_list,
459 state: ToState { to },
460 })
461 }
462}
463
464impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
465where
466 N::TransactionRequest: FoundryTransactionBuilder<N>,
467{
468 pub async fn with_code_sig_and_args(
472 self,
473 code: Option<String>,
474 sig: Option<String>,
475 args: Vec<String>,
476 ) -> Result<CastTxBuilder<N, P, InputState>> {
477 let (mut args, func) = if let Some(sig) = sig {
478 parse_function_args(
479 &sig,
480 args,
481 self.state.to,
482 self.chain,
483 &self.provider,
484 self.etherscan_api_key.as_deref(),
485 self.etherscan_api_url.as_deref(),
486 )
487 .await?
488 } else {
489 (Vec::new(), None)
490 };
491
492 let input = if let Some(code) = &code {
493 let mut code = hex::decode(code)?;
494 code.append(&mut args);
495 code
496 } else {
497 args
498 };
499
500 if self.state.to.is_none() && code.is_none() {
501 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
502 let has_auth = !self.auth.is_empty();
503 if !has_auth || has_value {
506 eyre::bail!("Must specify a recipient address or contract code to deploy");
507 }
508 }
509
510 Ok(CastTxBuilder {
511 provider: self.provider,
512 tx: self.tx,
513 legacy: self.legacy,
514 blob: self.blob,
515 eip4844: self.eip4844,
516 fill: self.fill,
517 chain: self.chain,
518 etherscan_api_key: self.etherscan_api_key,
519 etherscan_api_url: self.etherscan_api_url,
520 auth: self.auth,
521 access_list: self.access_list,
522 state: InputState { kind: self.state.to.into(), input, func },
523 })
524 }
525}
526
527impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
528where
529 N::TransactionRequest: FoundryTransactionBuilder<N>,
530{
531 pub async fn build(
534 self,
535 sender: impl Into<SenderKind<'_>>,
536 ) -> Result<(N::TransactionRequest, Option<Function>)> {
537 let fill = self.fill;
538 self._build(sender, fill, None).await
539 }
540
541 pub async fn build_with_access_key(
547 mut self,
548 sender: impl Into<SenderKind<'_>>,
549 access_key: &TempoAccessKeyConfig,
550 ) -> Result<(N::TransactionRequest, Option<Function>)> {
551 self.tx.set_key_id(access_key.key_address);
552 let fill = self.fill;
553 self._build(sender, fill, Some(access_key)).await
554 }
555
556 async fn _build(
557 mut self,
558 sender: impl Into<SenderKind<'_>>,
559 fill: bool,
560 access_key: Option<&TempoAccessKeyConfig>,
561 ) -> Result<(N::TransactionRequest, Option<Function>)> {
562 let sender = sender.into();
564 self.prepare(&sender);
565
566 self.tx.clear_batch_to();
570
571 let tx_nonce = self.resolve_nonce(sender.address(), fill).await?;
573 self.resolve_auth(&sender, tx_nonce).await?;
574 if let Some(access_key) = access_key {
575 self.tx
576 .prepare_access_key_authorization(
577 &self.provider,
578 access_key.wallet_address,
579 access_key.key_address,
580 access_key.key_authorization.as_ref(),
581 )
582 .await?;
583 }
584 self.resolve_access_list().await?;
585
586 if fill {
588 self.fill_fees().await?;
589 }
590
591 Ok((self.tx, self.state.func))
592 }
593
594 fn prepare(&mut self, sender: &SenderKind<'_>) {
596 self.tx.set_kind(self.state.kind);
597 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
600 self.tx.set_from(sender.address());
601 self.tx.set_chain_id(self.chain.id());
602 }
603
604 async fn resolve_nonce(&mut self, from: Address, fill: bool) -> Result<u64> {
607 if let Some(nonce) = self.tx.nonce() {
608 Ok(nonce)
609 } else {
610 let nonce = self.provider.get_transaction_count(from).await?;
611 if fill {
612 self.tx.set_nonce(nonce);
613 }
614 Ok(nonce)
615 }
616 }
617
618 async fn resolve_access_list(&mut self) -> Result<()> {
621 if let Some(access_list) = match self.access_list.take() {
622 None => None,
623 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
624 Some(Some(access_list)) => Some(access_list),
625 } {
626 self.tx.set_access_list(access_list);
627 }
628 Ok(())
629 }
630
631 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
636 if self.auth.is_empty() {
637 return Ok(());
638 }
639
640 let auths = std::mem::take(&mut self.auth);
641
642 let address_auth_count =
645 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
646 if address_auth_count > 1 {
647 eyre::bail!(
648 "Multiple address-based authorizations provided. Only one address can be specified; \
649 use pre-signed authorizations (hex-encoded) for multiple authorizations."
650 );
651 }
652
653 let mut signed_auths = Vec::with_capacity(auths.len());
654
655 for auth in auths {
656 let signed_auth = match auth {
657 CliAuthorizationList::Address(address) => {
658 let auth = Authorization {
659 chain_id: U256::from(self.chain.id()),
660 nonce: tx_nonce + 1,
661 address,
662 };
663
664 let Some(signer) = sender.as_signer() else {
665 eyre::bail!(
666 "No signer available to sign authorization. \
667 Provide a pre-signed authorization (hex-encoded) instead."
668 );
669 };
670 let signature = signer.sign_hash(&auth.signature_hash()).await?;
671
672 auth.into_signed(signature)
673 }
674 CliAuthorizationList::Signed(auth) => auth,
675 };
676 signed_auths.push(signed_auth);
677 }
678
679 self.tx.set_authorization_list(signed_auths);
680
681 Ok(())
682 }
683
684 async fn fill_fees(&mut self) -> Result<()> {
688 if self.legacy && self.tx.gas_price().is_none() {
689 self.tx.set_gas_price(self.provider.get_gas_price().await?);
690 }
691
692 if self.blob && self.tx.max_fee_per_blob_gas().is_none() {
693 self.tx.set_max_fee_per_blob_gas(self.provider.get_blob_base_fee().await?)
694 }
695
696 if !self.legacy
697 && (self.tx.max_fee_per_gas().is_none() || self.tx.max_priority_fee_per_gas().is_none())
698 {
699 let estimate = self.provider.estimate_eip1559_fees().await?;
700
701 if self.tx.max_fee_per_gas().is_none() {
702 self.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
703 }
704
705 if self.tx.max_priority_fee_per_gas().is_none() {
706 self.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
707 }
708 }
709
710 if self.tx.gas_limit().is_none() {
711 self.estimate_gas().await?;
712 }
713
714 Ok(())
715 }
716
717 async fn estimate_gas(&mut self) -> Result<()> {
719 match self.provider.estimate_gas(self.tx.clone()).await {
720 Ok(estimated) => {
721 self.tx.set_gas_limit(estimated);
722 Ok(())
723 }
724 Err(err) => {
725 if let TransportError::ErrorResp(payload) = &err {
726 if payload.code == 3
729 && let Some(data) = &payload.data
730 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
731 {
732 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
733 }
734 }
735 eyre::bail!("Failed to estimate gas: {}", err)
736 }
737 }
738 }
739
740 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
742 let Some(blob_data) = blob_data else { return Ok(self) };
743
744 let mut coder = SidecarBuilder::<SimpleCoder>::default();
745 coder.ingest(&blob_data);
746
747 if self.eip4844 {
748 let sidecar = coder.build_4844()?;
749 self.tx.set_blob_sidecar_4844(sidecar);
750 } else {
751 let sidecar = coder.build_7594()?;
752 self.tx.set_blob_sidecar_7594(sidecar);
753 }
754
755 Ok(self)
756 }
757
758 pub const fn raw(mut self) -> Self {
761 self.fill = false;
762 self
763 }
764}
765
766async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
768 let err_data = serde_json::from_str::<Bytes>(data.get())?;
769 let Some(selector) = err_data.get(..4) else { return Ok(None) };
770 if let Some(known_error) =
771 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
772 {
773 let mut decoded_error = known_error.name.clone();
774 if !known_error.inputs.is_empty()
775 && let Ok(error) = known_error.decode_error(&err_data)
776 {
777 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
778 }
779 return Ok(Some(decoded_error));
780 }
781 Ok(None)
782}