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, ReceiptResponse, 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,
20 fmt::*,
21 get_pretty_receipt_w_reason_attr,
22 provider::fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
23 shell,
24};
25use foundry_config::{Chain, Config, Eip1559FeeEstimatePreset};
26use foundry_wallets::{BrowserWalletOpts, TempoAccountsWallet, WalletOpts, WalletSigner};
27use itertools::Itertools;
28use serde_json::value::RawValue;
29use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration};
30
31#[derive(Debug, Clone, Args)]
32pub struct SendTxOpts {
33 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
35 pub cast_async: bool,
36
37 #[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 #[command(flatten)]
61 pub browser: BrowserWalletOpts,
62}
63
64#[derive(Debug, Clone, Args)]
66#[command(next_help_heading = "Transaction options")]
67pub struct TxParams {
68 #[arg(long, env = "ETH_GAS_LIMIT")]
70 pub gas_limit: Option<U256>,
71
72 #[arg(long, env = "ETH_GAS_PRICE")]
74 pub gas_price: Option<U256>,
75
76 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
78 pub priority_gas_price: Option<U256>,
79
80 #[arg(long)]
82 pub nonce: Option<U64>,
83
84 #[command(flatten)]
85 pub tempo: TempoOpts,
86}
87
88impl TxParams {
89 pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
90 where
91 N::TransactionRequest: FoundryTransactionBuilder<N>,
92 {
93 if let Some(gas_limit) = self.gas_limit {
94 tx.set_gas_limit(gas_limit.to());
95 }
96
97 if let Some(gas_price) = self.gas_price {
98 if legacy {
99 tx.set_gas_price(gas_price.to());
100 } else {
101 tx.set_max_fee_per_gas(gas_price.to());
102 }
103 }
104
105 if !legacy && let Some(priority_fee) = self.priority_gas_price {
106 tx.set_max_priority_fee_per_gas(priority_fee.to());
107 }
108
109 self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
110 }
111}
112
113pub enum SenderKind<'a> {
115 Address(Address),
118 Signer(&'a WalletSigner),
120 OwnedSigner(Box<WalletSigner>),
122}
123
124impl SenderKind<'_> {
125 pub fn address(&self) -> Address {
127 match self {
128 Self::Address(addr) => *addr,
129 Self::Signer(signer) => signer.address(),
130 Self::OwnedSigner(signer) => signer.address(),
131 }
132 }
133
134 pub async fn from_wallet_opts(mut opts: WalletOpts) -> Result<Self> {
142 let from = opts.from.take();
143 let (signer, tempo_wallet) = opts.maybe_signer().await?;
144 if let Some(signer) = signer {
145 Ok(Self::OwnedSigner(Box::new(signer)))
146 } else if let Some(tempo_wallet) = tempo_wallet {
147 Ok(tempo_wallet.account().into())
148 } else if let Some(from) = from {
149 Ok(from.into())
150 } else {
151 Ok(Address::ZERO.into())
152 }
153 }
154
155 pub fn as_signer(&self) -> Option<&WalletSigner> {
157 match self {
158 Self::Signer(signer) => Some(signer),
159 Self::OwnedSigner(signer) => Some(signer.as_ref()),
160 _ => None,
161 }
162 }
163}
164
165impl From<Address> for SenderKind<'_> {
166 fn from(addr: Address) -> Self {
167 Self::Address(addr)
168 }
169}
170
171impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
172 fn from(signer: &'a WalletSigner) -> Self {
173 Self::Signer(signer)
174 }
175}
176
177impl From<WalletSigner> for SenderKind<'_> {
178 fn from(signer: WalletSigner) -> Self {
179 Self::OwnedSigner(Box::new(signer))
180 }
181}
182
183pub fn validate_from_address(
185 specified_from: Option<Address>,
186 signer_address: Address,
187) -> Result<()> {
188 if let Some(specified_from) = specified_from
189 && specified_from != signer_address
190 {
191 eyre::bail!(
192 "\
193The specified sender via CLI/env vars does not match the sender configured via
194the hardware wallet's HD Path.
195Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
196corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
197 );
198 }
199 Ok(())
200}
201
202#[derive(Debug)]
204pub struct InitState;
205
206#[derive(Debug)]
208pub struct ToState {
209 to: Option<Address>,
210}
211
212#[derive(Debug)]
214pub struct InputState {
215 kind: TxKind,
216 input: Vec<u8>,
217 func: Option<Function>,
218}
219
220pub struct CastTxSender<N, P> {
221 provider: P,
222 _phantom: PhantomData<N>,
223}
224
225impl<N: Network, P: Provider<N>> CastTxSender<N, P>
226where
227 N::TransactionRequest: FoundryTransactionBuilder<N>,
228 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
229{
230 pub const fn new(provider: P) -> Self {
232 Self { provider, _phantom: PhantomData }
233 }
234
235 pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
237 let mut receipt = TransactionReceiptWithRevertReason::<N> {
238 receipt: self.provider.send_transaction_sync(tx).await?,
239 revert_reason: None,
240 };
241 let tx_hash = receipt.receipt.transaction_hash();
242 let _ = receipt.update_revert_reason(&self.provider).await;
244
245 self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
246 }
247
248 pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
283 let res = self.provider.send_transaction(tx).await?;
284
285 Ok(res)
286 }
287
288 pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
293 let res = self.provider.send_raw_transaction(raw_tx).await?;
294 Ok(res)
295 }
296
297 pub async fn send_raw_sync(&self, raw_tx: &[u8]) -> Result<(B256, String)> {
299 let mut receipt = TransactionReceiptWithRevertReason::<N> {
300 receipt: self.provider.send_raw_transaction_sync(raw_tx).await?,
301 revert_reason: None,
302 };
303 let tx_hash = receipt.receipt.transaction_hash();
304 let _ = receipt.update_revert_reason(&self.provider).await;
306
307 self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
308 }
309
310 pub async fn print_tx_result(
315 &self,
316 tx_hash: B256,
317 cast_async: bool,
318 confs: u64,
319 timeout: u64,
320 ) -> Result<()> {
321 if cast_async {
322 sh_println!("{tx_hash:#x}")?;
323 } else {
324 let receipt =
325 self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
326 sh_println!("{receipt}")?;
327 }
328 Ok(())
329 }
330
331 pub async fn receipt(
348 &self,
349 tx_hash: String,
350 field: Option<String>,
351 confs: u64,
352 timeout: Option<u64>,
353 cast_async: bool,
354 ) -> Result<String> {
355 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
356
357 let mut receipt = TransactionReceiptWithRevertReason::<N> {
358 receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
359 Some(r) => r,
360 None => {
361 if cast_async {
364 eyre::bail!("tx not found: {:?}", tx_hash);
365 }
366 PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
367 .with_required_confirmations(confs)
368 .with_timeout(timeout.map(Duration::from_secs))
369 .get_receipt()
370 .await?
371 }
372 },
373 revert_reason: None,
374 };
375
376 let _ = receipt.update_revert_reason(&self.provider).await;
378
379 self.format_receipt(receipt, field)
380 }
381
382 fn format_receipt(
384 &self,
385 receipt: TransactionReceiptWithRevertReason<N>,
386 field: Option<String>,
387 ) -> Result<String> {
388 Ok(if let Some(ref field) = field {
389 get_pretty_receipt_w_reason_attr(&receipt, field)
390 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
391 } else if shell::is_json() {
392 serde_json::to_value(&receipt)?.to_string()
394 } else {
395 receipt.pretty()
396 })
397 }
398}
399
400#[derive(Debug)]
405pub struct CastTxBuilder<N: Network, P, S> {
406 provider: P,
407 pub(crate) tx: N::TransactionRequest,
408 legacy: bool,
410 blob: bool,
411 eip4844: bool,
413 fill: bool,
416 browser: bool,
418 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
420 auth: Vec<CliAuthorizationList>,
421 chain: Chain,
422 etherscan_api_key: Option<String>,
423 etherscan_api_url: Option<String>,
424 access_list: Option<Option<AccessList>>,
425 state: S,
426}
427
428impl<N: Network, P, S> CastTxBuilder<N, P, S> {
429 pub const fn chain(&self) -> Chain {
431 self.chain
432 }
433
434 pub const fn with_browser_wallet(mut self) -> Self {
436 self.browser = true;
437 self
438 }
439
440 pub(crate) const fn has_auth(&self) -> bool {
442 !self.auth.is_empty()
443 }
444
445 pub(crate) fn validate_auth(&self, sender: &SenderKind<'_>) -> Result<()> {
447 let address_auth_count = self
448 .auth
449 .iter()
450 .filter(|auth| matches!(auth, CliAuthorizationList::Address(_)))
451 .count();
452 if address_auth_count > 1 {
453 eyre::bail!(
454 "Multiple address-based authorizations provided. Only one address can be specified; \
455 use pre-signed authorizations (hex-encoded) for multiple authorizations."
456 );
457 }
458 if address_auth_count == 1 && sender.as_signer().is_none() {
459 eyre::bail!(
460 "No signer available to sign authorization. \
461 Provide a pre-signed authorization (hex-encoded) instead."
462 );
463 }
464
465 Ok(())
466 }
467
468 pub(crate) fn will_disclose_auth_during_build(&self) -> bool {
471 self.has_auth()
472 && (matches!(self.access_list, Some(None))
474 || (self.fill && self.tx.gas_limit().is_none()))
476 }
477}
478
479impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
480where
481 N::TransactionRequest: FoundryTransactionBuilder<N>,
482{
483 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
486 let mut tx = N::TransactionRequest::default();
487
488 let chain = utils::get_chain(config.chain, &provider).await?;
489 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
490 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
491 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
492 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
494
495 tx_opts.apply::<N>(&mut tx, legacy);
497
498 Ok(Self {
499 provider,
500 tx,
501 legacy,
502 blob: tx_opts.blob,
503 eip4844: tx_opts.eip4844,
504 fill: true,
505 browser: false,
506 eip1559_fee_estimate: config.eip1559_fee_estimate,
507 chain,
508 etherscan_api_key,
509 etherscan_api_url,
510 auth: tx_opts.auth,
511 access_list: tx_opts.access_list,
512 state: InitState,
513 })
514 }
515
516 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
518 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
519 Ok(CastTxBuilder {
520 provider: self.provider,
521 tx: self.tx,
522 legacy: self.legacy,
523 blob: self.blob,
524 eip4844: self.eip4844,
525 fill: self.fill,
526 browser: self.browser,
527 eip1559_fee_estimate: self.eip1559_fee_estimate,
528 chain: self.chain,
529 etherscan_api_key: self.etherscan_api_key,
530 etherscan_api_url: self.etherscan_api_url,
531 auth: self.auth,
532 access_list: self.access_list,
533 state: ToState { to },
534 })
535 }
536}
537
538impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
539where
540 N::TransactionRequest: FoundryTransactionBuilder<N>,
541{
542 pub async fn with_code_sig_and_args(
546 self,
547 code: Option<String>,
548 sig: Option<String>,
549 args: Vec<String>,
550 ) -> Result<CastTxBuilder<N, P, InputState>> {
551 let (mut args, func) = if let Some(sig) = sig {
552 parse_function_args(
553 &sig,
554 args,
555 self.state.to,
556 self.chain,
557 &self.provider,
558 self.etherscan_api_key.as_deref(),
559 self.etherscan_api_url.as_deref(),
560 )
561 .await?
562 } else {
563 (Vec::new(), None)
564 };
565
566 let input = if let Some(code) = &code {
567 let mut code = hex::decode(code)?;
568 code.append(&mut args);
569 code
570 } else {
571 args
572 };
573
574 if self.state.to.is_none() && code.is_none() {
575 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
576 let has_auth = !self.auth.is_empty();
577 if !has_auth || has_value {
580 eyre::bail!("Must specify a recipient address or contract code to deploy");
581 }
582 }
583
584 Ok(CastTxBuilder {
585 provider: self.provider,
586 tx: self.tx,
587 legacy: self.legacy,
588 blob: self.blob,
589 eip4844: self.eip4844,
590 fill: self.fill,
591 browser: self.browser,
592 eip1559_fee_estimate: self.eip1559_fee_estimate,
593 chain: self.chain,
594 etherscan_api_key: self.etherscan_api_key,
595 etherscan_api_url: self.etherscan_api_url,
596 auth: self.auth,
597 access_list: self.access_list,
598 state: InputState { kind: self.state.to.into(), input, func },
599 })
600 }
601}
602
603impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
604where
605 N::TransactionRequest: FoundryTransactionBuilder<N>,
606{
607 pub async fn build(
610 self,
611 sender: impl Into<SenderKind<'_>>,
612 ) -> Result<(N::TransactionRequest, Option<Function>)> {
613 let fill = self.fill;
614 self._build(sender, fill, None).await
615 }
616
617 pub async fn build_with_tempo_wallet(
623 self,
624 wallet: &TempoAccountsWallet,
625 ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
626 let fill = self.fill;
627 let mut prepared = wallet.clone();
628 let (tx, func) = self._build(wallet.account(), fill, Some(&mut prepared)).await?;
629 Ok((tx, func, prepared))
630 }
631
632 async fn _build(
633 mut self,
634 sender: impl Into<SenderKind<'_>>,
635 fill: bool,
636 tempo_wallet: Option<&mut TempoAccountsWallet>,
637 ) -> Result<(N::TransactionRequest, Option<Function>)> {
638 let sender = sender.into();
640 self.prepare(&sender);
641
642 self.tx.clear_batch_to();
646
647 let resolve_in_parallel =
651 fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
652 let tx_nonce = if resolve_in_parallel {
653 let nonce = self.tx.nonce();
654 let fees_are_complete = if self.legacy {
655 self.tx.gas_price().is_some()
656 } else {
657 matches!(
658 (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
659 (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
660 )
661 } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
662 let gas_request =
663 (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
664 .then(|| self.tx.clone());
665 let (tx_nonce, (), gas_limit) = tokio::try_join!(
666 Self::resolve_nonce(&self.provider, sender.address(), nonce),
667 Self::fill_fees(
668 &self.provider,
669 &mut self.tx,
670 self.blob,
671 self.legacy,
672 self.browser,
673 self.eip1559_fee_estimate,
674 ),
675 async {
676 match gas_request {
677 Some(request) => {
678 Self::estimate_gas(&self.provider, request).await.map(Some)
679 }
680 None => Ok(None),
681 }
682 },
683 )?;
684 if let Some(gas_limit) = gas_limit {
685 self.tx.set_gas_limit(gas_limit);
686 }
687 Some(tx_nonce)
688 } else if fill || !self.auth.is_empty() {
689 Some(Self::resolve_nonce(&self.provider, sender.address(), self.tx.nonce()).await?)
690 } else {
691 None
692 };
693 if let Some(tx_nonce) = tx_nonce {
694 if fill {
695 self.tx.set_nonce(tx_nonce);
696 }
697 self.resolve_auth(&sender, tx_nonce).await?;
698 }
699 if let Some(wallet) = tempo_wallet {
700 *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
701 }
702 if fill && !resolve_in_parallel {
703 Self::fill_fees(
704 &self.provider,
705 &mut self.tx,
706 self.blob,
707 self.legacy,
708 self.browser,
709 self.eip1559_fee_estimate,
710 )
711 .await?;
712 }
713 self.resolve_access_list().await?;
714 if fill {
715 self.fill_gas_limit().await?;
716 }
717
718 Ok((self.tx, self.state.func))
719 }
720
721 fn prepare(&mut self, sender: &SenderKind<'_>) {
724 self.tx.set_kind(self.state.kind);
725 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
728 let sender = sender.address();
729 if !sender.is_zero() {
730 self.tx.set_from(sender);
731 }
732 self.tx.set_chain_id(self.chain.id());
733 }
734
735 async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
737 if let Some(nonce) = nonce {
738 Ok(nonce)
739 } else {
740 Ok(provider.get_transaction_count(from).await?)
741 }
742 }
743
744 async fn resolve_access_list(&mut self) -> Result<()> {
747 if let Some(access_list) = match self.access_list.take() {
748 None => None,
749 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
750 Some(Some(access_list)) => Some(access_list),
751 } {
752 self.tx.set_access_list(access_list);
753 }
754 Ok(())
755 }
756
757 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
762 if self.auth.is_empty() {
763 return Ok(());
764 }
765
766 self.validate_auth(sender)?;
767 let auths = std::mem::take(&mut self.auth);
768
769 let mut signed_auths = Vec::with_capacity(auths.len());
770
771 for auth in auths {
772 let signed_auth = match auth {
773 CliAuthorizationList::Address(address) => {
774 let auth = Authorization {
775 chain_id: U256::from(self.chain.id()),
776 nonce: tx_nonce + 1,
777 address,
778 };
779
780 let signer =
781 sender.as_signer().expect("address-based authorization requires a signer");
782 let signature = signer.sign_hash(&auth.signature_hash()).await?;
783
784 auth.into_signed(signature)
785 }
786 CliAuthorizationList::Signed(auth) => auth,
787 };
788 signed_auths.push(signed_auth);
789 }
790
791 self.tx.set_authorization_list(signed_auths);
792
793 Ok(())
794 }
795
796 async fn fill_fees(
800 provider: &P,
801 tx: &mut N::TransactionRequest,
802 blob: bool,
803 legacy: bool,
804 browser: bool,
805 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
806 ) -> Result<()> {
807 if blob && tx.max_fee_per_blob_gas().is_none() {
808 tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
809 }
810
811 fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
812 }
813
814 async fn fill_gas_limit(&mut self) -> Result<()> {
816 if self.tx.gas_limit().is_none() {
817 let request = if self.browser && self.chain.is_tempo() {
818 self.tx.browser_wallet_gas_estimation_request()
819 } else {
820 self.tx.clone()
821 };
822 let estimated = Self::estimate_gas(&self.provider, request).await?;
823 self.tx.set_gas_limit(estimated);
824 }
825
826 Ok(())
827 }
828
829 async fn estimate_gas(provider: &P, request: N::TransactionRequest) -> Result<u64> {
831 match provider.estimate_gas(request).await {
832 Ok(estimated) => Ok(estimated),
833 Err(err) => {
834 if let TransportError::ErrorResp(payload) = &err {
835 if payload.code == 3
838 && let Some(data) = &payload.data
839 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
840 {
841 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error);
842 }
843 }
844 eyre::bail!("Failed to estimate gas: {}", err);
845 }
846 }
847 }
848
849 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
851 let Some(blob_data) = blob_data else { return Ok(self) };
852
853 let mut coder = SidecarBuilder::<SimpleCoder>::default();
854 coder.ingest(&blob_data);
855
856 if self.eip4844 {
857 let sidecar = coder.build_4844()?;
858 self.tx.set_blob_sidecar_4844(sidecar);
859 } else {
860 let sidecar = coder.build_7594()?;
861 self.tx.set_blob_sidecar_7594(sidecar);
862 }
863
864 Ok(self)
865 }
866
867 pub const fn raw(mut self) -> Self {
870 self.fill = false;
871 self
872 }
873}
874
875pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
877 provider: &P,
878 tx: &mut N::TransactionRequest,
879 legacy: bool,
880 browser: bool,
881 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
882) -> Result<()>
883where
884 N::TransactionRequest: FoundryTransactionBuilder<N>,
885{
886 if legacy {
887 if tx.gas_price().is_none() {
888 tx.set_gas_price(provider.get_gas_price().await?);
889 }
890 return Ok(());
891 }
892
893 if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
894 let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
895
896 let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
899 provider.get_max_priority_fee_per_gas().await.ok()
900 } else {
901 None
902 };
903
904 let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
907
908 if tx.max_fee_per_gas().is_none() {
909 tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
910 }
911
912 if tx.max_priority_fee_per_gas().is_none() {
913 tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
914 }
915 }
916
917 if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
918 eyre::ensure!(
919 priority <= max_fee,
920 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
921 );
922 }
923
924 Ok(())
925}
926
927async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
929 let err_data = serde_json::from_str::<Bytes>(data.get())?;
930 let Some(selector) = err_data.get(..4) else { return Ok(None) };
931 if let Some(known_error) =
932 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
933 {
934 let mut decoded_error = known_error.name.clone();
935 if !known_error.inputs.is_empty()
936 && let Ok(error) = known_error.decode_error(&err_data)
937 {
938 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
939 }
940 return Ok(Some(decoded_error));
941 }
942 Ok(None)
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948 use alloy_json_rpc::{RequestPacket, ResponsePacket};
949 use alloy_network::Ethereum;
950 use alloy_provider::{ProviderBuilder, mock::Asserter};
951 use alloy_rpc_client::RpcClient;
952 use alloy_transport::{TransportFut, mock::MockTransport};
953 use clap::Parser;
954 use std::{
955 sync::{Arc, Mutex},
956 task::{Context, Poll},
957 };
958 use tokio::{sync::Barrier, time::timeout};
959 use tower::Service;
960
961 #[derive(Clone)]
962 struct BarrierTransport {
963 inner: MockTransport,
964 barrier: Arc<Barrier>,
965 fill_methods: Arc<Mutex<Vec<String>>>,
966 }
967
968 impl Service<RequestPacket> for BarrierTransport {
969 type Response = ResponsePacket;
970 type Error = TransportError;
971 type Future = TransportFut<'static>;
972
973 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
974 self.inner.poll_ready(cx)
975 }
976
977 fn call(&mut self, req: RequestPacket) -> Self::Future {
978 let fill_method = match &req {
979 RequestPacket::Single(req)
980 if matches!(
981 req.method(),
982 "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
983 ) =>
984 {
985 Some(req.method().to_string())
986 }
987 _ => None,
988 };
989 let Some(fill_method) = fill_method else {
990 return self.inner.call(req);
991 };
992 self.fill_methods.lock().unwrap().push(fill_method);
993
994 let barrier = self.barrier.clone();
995 let mut inner = self.inner.clone();
996 Box::pin(async move {
997 barrier.wait().await;
998 inner.call(req).await
999 })
1000 }
1001 }
1002
1003 #[tokio::test]
1004 async fn filled_build_fetches_nonce_and_gas_concurrently_with_explicit_fees() {
1005 let asserter = Asserter::new();
1006 for _ in 0..2 {
1007 asserter.push_success(&U64::from(1));
1008 }
1009 let fill_methods = Arc::new(Mutex::new(Vec::new()));
1010 let transport = BarrierTransport {
1011 inner: MockTransport::new(asserter),
1012 barrier: Arc::new(Barrier::new(2)),
1013 fill_methods: fill_methods.clone(),
1014 };
1015 let provider = ProviderBuilder::new_with_network::<Ethereum>()
1016 .connect_client(RpcClient::new(transport, true));
1017 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1018
1019 let builder = CastTxBuilder::new(
1020 &provider,
1021 TransactionOpts::parse_from(["test", "--legacy", "--gas-price", "1"]),
1022 &config,
1023 )
1024 .await
1025 .unwrap()
1026 .with_to(Some(Address::repeat_byte(0x11).into()))
1027 .await
1028 .unwrap()
1029 .with_code_sig_and_args(None, None, Vec::new())
1030 .await
1031 .unwrap();
1032 let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
1033 .await
1034 .expect("nonce and gas requests were not in flight together")
1035 .unwrap();
1036
1037 assert_eq!(tx.nonce, Some(1));
1038 assert_eq!(tx.gas_price, Some(1));
1039 assert_eq!(tx.gas, Some(1));
1040 let mut fill_methods = fill_methods.lock().unwrap().clone();
1041 fill_methods.sort();
1042 assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
1043 }
1044
1045 #[tokio::test]
1046 async fn raw_build_skips_nonce_request() {
1047 let provider =
1050 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1051 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1052
1053 CastTxBuilder::new(&provider, TransactionOpts::parse_from(["test"]), &config)
1054 .await
1055 .unwrap()
1056 .with_to(Some(Address::repeat_byte(0x11).into()))
1057 .await
1058 .unwrap()
1059 .with_code_sig_and_args(None, None, Vec::new())
1060 .await
1061 .unwrap()
1062 .raw()
1063 .build(Address::repeat_byte(0x22))
1064 .await
1065 .unwrap();
1066 }
1067
1068 #[tokio::test]
1069 async fn detects_auth_rpc_disclosure() {
1070 let provider =
1071 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1072 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1073 let address = Address::repeat_byte(0x11);
1074
1075 let no_auth = CastTxBuilder::new(
1076 &provider,
1077 TransactionOpts::parse_from(["test", "--gas-limit", "21000"]),
1078 &config,
1079 )
1080 .await
1081 .unwrap()
1082 .with_to(Some(address.into()))
1083 .await
1084 .unwrap()
1085 .with_code_sig_and_args(None, None, Vec::new())
1086 .await
1087 .unwrap()
1088 .raw();
1089 assert!(!no_auth.has_auth());
1090 assert!(!no_auth.will_disclose_auth_during_build());
1091
1092 let rpc_call = CastTxBuilder::new(
1093 &provider,
1094 TransactionOpts::parse_from([
1095 "test",
1096 "--auth",
1097 &address.to_string(),
1098 "--gas-limit",
1099 "21000",
1100 ]),
1101 &config,
1102 )
1103 .await
1104 .unwrap()
1105 .with_to(Some(address.into()))
1106 .await
1107 .unwrap()
1108 .with_code_sig_and_args(None, None, Vec::new())
1109 .await
1110 .unwrap()
1111 .raw();
1112 assert!(rpc_call.has_auth());
1113 assert!(!rpc_call.will_disclose_auth_during_build());
1114
1115 let generated_access_list = CastTxBuilder::new(
1116 &provider,
1117 TransactionOpts::parse_from(["test", "--auth", &address.to_string(), "--access-list"]),
1118 &config,
1119 )
1120 .await
1121 .unwrap()
1122 .with_to(Some(address.into()))
1123 .await
1124 .unwrap()
1125 .with_code_sig_and_args(None, None, Vec::new())
1126 .await
1127 .unwrap()
1128 .raw();
1129 assert!(generated_access_list.will_disclose_auth_during_build());
1130
1131 let explicit_access_list = CastTxBuilder::new(
1132 &provider,
1133 TransactionOpts::parse_from([
1134 "test",
1135 "--auth",
1136 &address.to_string(),
1137 "--access-list",
1138 "[]",
1139 ]),
1140 &config,
1141 )
1142 .await
1143 .unwrap()
1144 .with_to(Some(address.into()))
1145 .await
1146 .unwrap()
1147 .with_code_sig_and_args(None, None, Vec::new())
1148 .await
1149 .unwrap()
1150 .raw();
1151 assert!(!explicit_access_list.will_disclose_auth_during_build());
1152
1153 let estimated_gas = CastTxBuilder::new(
1154 &provider,
1155 TransactionOpts::parse_from(["test", "--auth", &address.to_string()]),
1156 &config,
1157 )
1158 .await
1159 .unwrap();
1160 assert!(estimated_gas.will_disclose_auth_during_build());
1161 }
1162}