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
441impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
442where
443 N::TransactionRequest: FoundryTransactionBuilder<N>,
444{
445 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
448 let mut tx = N::TransactionRequest::default();
449
450 let chain = utils::get_chain(config.chain, &provider).await?;
451 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
452 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
453 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
454 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
456
457 tx_opts.apply::<N>(&mut tx, legacy);
459
460 Ok(Self {
461 provider,
462 tx,
463 legacy,
464 blob: tx_opts.blob,
465 eip4844: tx_opts.eip4844,
466 fill: true,
467 browser: false,
468 eip1559_fee_estimate: config.eip1559_fee_estimate,
469 chain,
470 etherscan_api_key,
471 etherscan_api_url,
472 auth: tx_opts.auth,
473 access_list: tx_opts.access_list,
474 state: InitState,
475 })
476 }
477
478 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
480 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
481 Ok(CastTxBuilder {
482 provider: self.provider,
483 tx: self.tx,
484 legacy: self.legacy,
485 blob: self.blob,
486 eip4844: self.eip4844,
487 fill: self.fill,
488 browser: self.browser,
489 eip1559_fee_estimate: self.eip1559_fee_estimate,
490 chain: self.chain,
491 etherscan_api_key: self.etherscan_api_key,
492 etherscan_api_url: self.etherscan_api_url,
493 auth: self.auth,
494 access_list: self.access_list,
495 state: ToState { to },
496 })
497 }
498}
499
500impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
501where
502 N::TransactionRequest: FoundryTransactionBuilder<N>,
503{
504 pub async fn with_code_sig_and_args(
508 self,
509 code: Option<String>,
510 sig: Option<String>,
511 args: Vec<String>,
512 ) -> Result<CastTxBuilder<N, P, InputState>> {
513 let (mut args, func) = if let Some(sig) = sig {
514 parse_function_args(
515 &sig,
516 args,
517 self.state.to,
518 self.chain,
519 &self.provider,
520 self.etherscan_api_key.as_deref(),
521 self.etherscan_api_url.as_deref(),
522 )
523 .await?
524 } else {
525 (Vec::new(), None)
526 };
527
528 let input = if let Some(code) = &code {
529 let mut code = hex::decode(code)?;
530 code.append(&mut args);
531 code
532 } else {
533 args
534 };
535
536 if self.state.to.is_none() && code.is_none() {
537 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
538 let has_auth = !self.auth.is_empty();
539 if !has_auth || has_value {
542 eyre::bail!("Must specify a recipient address or contract code to deploy");
543 }
544 }
545
546 Ok(CastTxBuilder {
547 provider: self.provider,
548 tx: self.tx,
549 legacy: self.legacy,
550 blob: self.blob,
551 eip4844: self.eip4844,
552 fill: self.fill,
553 browser: self.browser,
554 eip1559_fee_estimate: self.eip1559_fee_estimate,
555 chain: self.chain,
556 etherscan_api_key: self.etherscan_api_key,
557 etherscan_api_url: self.etherscan_api_url,
558 auth: self.auth,
559 access_list: self.access_list,
560 state: InputState { kind: self.state.to.into(), input, func },
561 })
562 }
563}
564
565impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
566where
567 N::TransactionRequest: FoundryTransactionBuilder<N>,
568{
569 pub async fn build(
572 self,
573 sender: impl Into<SenderKind<'_>>,
574 ) -> Result<(N::TransactionRequest, Option<Function>)> {
575 let fill = self.fill;
576 self._build(sender, fill, None).await
577 }
578
579 pub async fn build_with_tempo_wallet(
585 self,
586 wallet: &TempoAccountsWallet,
587 ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
588 let fill = self.fill;
589 let mut prepared = wallet.clone();
590 let (tx, func) = self._build(wallet.account(), fill, Some(&mut prepared)).await?;
591 Ok((tx, func, prepared))
592 }
593
594 async fn _build(
595 mut self,
596 sender: impl Into<SenderKind<'_>>,
597 fill: bool,
598 tempo_wallet: Option<&mut TempoAccountsWallet>,
599 ) -> Result<(N::TransactionRequest, Option<Function>)> {
600 let sender = sender.into();
602 self.prepare(&sender);
603
604 self.tx.clear_batch_to();
608
609 let resolve_in_parallel =
613 fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
614 let tx_nonce = if resolve_in_parallel {
615 let nonce = self.tx.nonce();
616 let fees_are_complete = if self.legacy {
617 self.tx.gas_price().is_some()
618 } else {
619 matches!(
620 (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
621 (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
622 )
623 } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
624 let gas_request =
625 (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
626 .then(|| self.tx.clone());
627 let (tx_nonce, (), gas_limit) = tokio::try_join!(
628 Self::resolve_nonce(&self.provider, sender.address(), nonce),
629 Self::fill_fees(
630 &self.provider,
631 &mut self.tx,
632 self.blob,
633 self.legacy,
634 self.browser,
635 self.eip1559_fee_estimate,
636 ),
637 async {
638 match gas_request {
639 Some(request) => {
640 Self::estimate_gas(&self.provider, request).await.map(Some)
641 }
642 None => Ok(None),
643 }
644 },
645 )?;
646 if let Some(gas_limit) = gas_limit {
647 self.tx.set_gas_limit(gas_limit);
648 }
649 Some(tx_nonce)
650 } else if fill || !self.auth.is_empty() {
651 Some(Self::resolve_nonce(&self.provider, sender.address(), self.tx.nonce()).await?)
652 } else {
653 None
654 };
655 if let Some(tx_nonce) = tx_nonce {
656 if fill {
657 self.tx.set_nonce(tx_nonce);
658 }
659 self.resolve_auth(&sender, tx_nonce).await?;
660 }
661 if let Some(wallet) = tempo_wallet {
662 *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
663 }
664 if fill && !resolve_in_parallel {
665 Self::fill_fees(
666 &self.provider,
667 &mut self.tx,
668 self.blob,
669 self.legacy,
670 self.browser,
671 self.eip1559_fee_estimate,
672 )
673 .await?;
674 }
675 self.resolve_access_list().await?;
676 if fill {
677 self.fill_gas_limit().await?;
678 }
679
680 Ok((self.tx, self.state.func))
681 }
682
683 fn prepare(&mut self, sender: &SenderKind<'_>) {
686 self.tx.set_kind(self.state.kind);
687 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
690 let sender = sender.address();
691 if !sender.is_zero() {
692 self.tx.set_from(sender);
693 }
694 self.tx.set_chain_id(self.chain.id());
695 }
696
697 async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
699 if let Some(nonce) = nonce {
700 Ok(nonce)
701 } else {
702 Ok(provider.get_transaction_count(from).await?)
703 }
704 }
705
706 async fn resolve_access_list(&mut self) -> Result<()> {
709 if let Some(access_list) = match self.access_list.take() {
710 None => None,
711 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
712 Some(Some(access_list)) => Some(access_list),
713 } {
714 self.tx.set_access_list(access_list);
715 }
716 Ok(())
717 }
718
719 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
724 if self.auth.is_empty() {
725 return Ok(());
726 }
727
728 let auths = std::mem::take(&mut self.auth);
729
730 let address_auth_count =
733 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
734 if address_auth_count > 1 {
735 eyre::bail!(
736 "Multiple address-based authorizations provided. Only one address can be specified; \
737 use pre-signed authorizations (hex-encoded) for multiple authorizations."
738 );
739 }
740
741 let mut signed_auths = Vec::with_capacity(auths.len());
742
743 for auth in auths {
744 let signed_auth = match auth {
745 CliAuthorizationList::Address(address) => {
746 let auth = Authorization {
747 chain_id: U256::from(self.chain.id()),
748 nonce: tx_nonce + 1,
749 address,
750 };
751
752 let Some(signer) = sender.as_signer() else {
753 eyre::bail!(
754 "No signer available to sign authorization. \
755 Provide a pre-signed authorization (hex-encoded) instead."
756 );
757 };
758 let signature = signer.sign_hash(&auth.signature_hash()).await?;
759
760 auth.into_signed(signature)
761 }
762 CliAuthorizationList::Signed(auth) => auth,
763 };
764 signed_auths.push(signed_auth);
765 }
766
767 self.tx.set_authorization_list(signed_auths);
768
769 Ok(())
770 }
771
772 async fn fill_fees(
776 provider: &P,
777 tx: &mut N::TransactionRequest,
778 blob: bool,
779 legacy: bool,
780 browser: bool,
781 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
782 ) -> Result<()> {
783 if blob && tx.max_fee_per_blob_gas().is_none() {
784 tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
785 }
786
787 fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
788 }
789
790 async fn fill_gas_limit(&mut self) -> Result<()> {
792 if self.tx.gas_limit().is_none() {
793 let request = if self.browser && self.chain.is_tempo() {
794 self.tx.browser_wallet_gas_estimation_request()
795 } else {
796 self.tx.clone()
797 };
798 let estimated = Self::estimate_gas(&self.provider, request).await?;
799 self.tx.set_gas_limit(estimated);
800 }
801
802 Ok(())
803 }
804
805 async fn estimate_gas(provider: &P, request: N::TransactionRequest) -> Result<u64> {
807 match provider.estimate_gas(request).await {
808 Ok(estimated) => Ok(estimated),
809 Err(err) => {
810 if let TransportError::ErrorResp(payload) = &err {
811 if payload.code == 3
814 && let Some(data) = &payload.data
815 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
816 {
817 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error);
818 }
819 }
820 eyre::bail!("Failed to estimate gas: {}", err);
821 }
822 }
823 }
824
825 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
827 let Some(blob_data) = blob_data else { return Ok(self) };
828
829 let mut coder = SidecarBuilder::<SimpleCoder>::default();
830 coder.ingest(&blob_data);
831
832 if self.eip4844 {
833 let sidecar = coder.build_4844()?;
834 self.tx.set_blob_sidecar_4844(sidecar);
835 } else {
836 let sidecar = coder.build_7594()?;
837 self.tx.set_blob_sidecar_7594(sidecar);
838 }
839
840 Ok(self)
841 }
842
843 pub const fn raw(mut self) -> Self {
846 self.fill = false;
847 self
848 }
849}
850
851pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
853 provider: &P,
854 tx: &mut N::TransactionRequest,
855 legacy: bool,
856 browser: bool,
857 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
858) -> Result<()>
859where
860 N::TransactionRequest: FoundryTransactionBuilder<N>,
861{
862 if legacy {
863 if tx.gas_price().is_none() {
864 tx.set_gas_price(provider.get_gas_price().await?);
865 }
866 return Ok(());
867 }
868
869 if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
870 let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
871
872 let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
875 provider.get_max_priority_fee_per_gas().await.ok()
876 } else {
877 None
878 };
879
880 let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
883
884 if tx.max_fee_per_gas().is_none() {
885 tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
886 }
887
888 if tx.max_priority_fee_per_gas().is_none() {
889 tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
890 }
891 }
892
893 if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
894 eyre::ensure!(
895 priority <= max_fee,
896 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
897 );
898 }
899
900 Ok(())
901}
902
903async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
905 let err_data = serde_json::from_str::<Bytes>(data.get())?;
906 let Some(selector) = err_data.get(..4) else { return Ok(None) };
907 if let Some(known_error) =
908 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
909 {
910 let mut decoded_error = known_error.name.clone();
911 if !known_error.inputs.is_empty()
912 && let Ok(error) = known_error.decode_error(&err_data)
913 {
914 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
915 }
916 return Ok(Some(decoded_error));
917 }
918 Ok(None)
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924 use alloy_json_rpc::{RequestPacket, ResponsePacket};
925 use alloy_network::Ethereum;
926 use alloy_provider::{ProviderBuilder, mock::Asserter};
927 use alloy_rpc_client::RpcClient;
928 use alloy_transport::{TransportFut, mock::MockTransport};
929 use clap::Parser;
930 use std::{
931 sync::{Arc, Mutex},
932 task::{Context, Poll},
933 };
934 use tokio::{sync::Barrier, time::timeout};
935 use tower::Service;
936
937 #[derive(Clone)]
938 struct BarrierTransport {
939 inner: MockTransport,
940 barrier: Arc<Barrier>,
941 fill_methods: Arc<Mutex<Vec<String>>>,
942 }
943
944 impl Service<RequestPacket> for BarrierTransport {
945 type Response = ResponsePacket;
946 type Error = TransportError;
947 type Future = TransportFut<'static>;
948
949 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
950 self.inner.poll_ready(cx)
951 }
952
953 fn call(&mut self, req: RequestPacket) -> Self::Future {
954 let fill_method = match &req {
955 RequestPacket::Single(req)
956 if matches!(
957 req.method(),
958 "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
959 ) =>
960 {
961 Some(req.method().to_string())
962 }
963 _ => None,
964 };
965 let Some(fill_method) = fill_method else {
966 return self.inner.call(req);
967 };
968 self.fill_methods.lock().unwrap().push(fill_method);
969
970 let barrier = self.barrier.clone();
971 let mut inner = self.inner.clone();
972 Box::pin(async move {
973 barrier.wait().await;
974 inner.call(req).await
975 })
976 }
977 }
978
979 #[tokio::test]
980 async fn filled_build_fetches_nonce_and_gas_concurrently_with_explicit_fees() {
981 let asserter = Asserter::new();
982 for _ in 0..2 {
983 asserter.push_success(&U64::from(1));
984 }
985 let fill_methods = Arc::new(Mutex::new(Vec::new()));
986 let transport = BarrierTransport {
987 inner: MockTransport::new(asserter),
988 barrier: Arc::new(Barrier::new(2)),
989 fill_methods: fill_methods.clone(),
990 };
991 let provider = ProviderBuilder::new_with_network::<Ethereum>()
992 .connect_client(RpcClient::new(transport, true));
993 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
994
995 let builder = CastTxBuilder::new(
996 &provider,
997 TransactionOpts::parse_from(["test", "--legacy", "--gas-price", "1"]),
998 &config,
999 )
1000 .await
1001 .unwrap()
1002 .with_to(Some(Address::repeat_byte(0x11).into()))
1003 .await
1004 .unwrap()
1005 .with_code_sig_and_args(None, None, Vec::new())
1006 .await
1007 .unwrap();
1008 let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
1009 .await
1010 .expect("nonce and gas requests were not in flight together")
1011 .unwrap();
1012
1013 assert_eq!(tx.nonce, Some(1));
1014 assert_eq!(tx.gas_price, Some(1));
1015 assert_eq!(tx.gas, Some(1));
1016 let mut fill_methods = fill_methods.lock().unwrap().clone();
1017 fill_methods.sort();
1018 assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
1019 }
1020
1021 #[tokio::test]
1022 async fn raw_build_skips_nonce_request() {
1023 let provider =
1026 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1027 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1028
1029 CastTxBuilder::new(&provider, TransactionOpts::parse_from(["test"]), &config)
1030 .await
1031 .unwrap()
1032 .with_to(Some(Address::repeat_byte(0x11).into()))
1033 .await
1034 .unwrap()
1035 .with_code_sig_and_args(None, None, Vec::new())
1036 .await
1037 .unwrap()
1038 .raw()
1039 .build(Address::repeat_byte(0x22))
1040 .await
1041 .unwrap();
1042 }
1043}