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, apply_gas_estimate_multiplier, 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(crate) fn validate_authorizations(
185 authorizations: &[CliAuthorizationList],
186 sender: &SenderKind<'_>,
187) -> Result<()> {
188 let address_auth_count = authorizations
189 .iter()
190 .filter(|auth| matches!(auth, CliAuthorizationList::Address(_)))
191 .count();
192 if address_auth_count > 1 {
193 eyre::bail!(
194 "Multiple address-based authorizations provided. Only one address can be specified; \
195 use pre-signed authorizations (hex-encoded) for multiple authorizations."
196 );
197 }
198 if address_auth_count == 1 && sender.as_signer().is_none() {
199 eyre::bail!(
200 "No signer available to sign authorization. \
201 Provide a pre-signed authorization (hex-encoded) instead."
202 );
203 }
204
205 Ok(())
206}
207
208pub fn validate_from_address(
210 specified_from: Option<Address>,
211 signer_address: Address,
212) -> Result<()> {
213 if let Some(specified_from) = specified_from
214 && specified_from != signer_address
215 {
216 eyre::bail!(
217 "\
218The specified sender via CLI/env vars does not match the sender configured via
219the hardware wallet's HD Path.
220Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
221corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
222 );
223 }
224 Ok(())
225}
226
227#[derive(Debug)]
229pub struct InitState;
230
231#[derive(Debug)]
233pub struct ToState {
234 to: Option<Address>,
235}
236
237#[derive(Debug)]
239pub struct InputState {
240 kind: TxKind,
241 input: Vec<u8>,
242 func: Option<Function>,
243}
244
245pub struct CastTxSender<N, P> {
246 provider: P,
247 _phantom: PhantomData<N>,
248}
249
250impl<N: Network, P: Provider<N>> CastTxSender<N, P>
251where
252 N::TransactionRequest: FoundryTransactionBuilder<N>,
253 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
254{
255 pub const fn new(provider: P) -> Self {
257 Self { provider, _phantom: PhantomData }
258 }
259
260 pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
262 let mut receipt = TransactionReceiptWithRevertReason::<N> {
263 receipt: self.provider.send_transaction_sync(tx).await?,
264 revert_reason: None,
265 };
266 let tx_hash = receipt.receipt.transaction_hash();
267 let _ = receipt.update_revert_reason(&self.provider).await;
269
270 self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
271 }
272
273 pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
308 let res = self.provider.send_transaction(tx).await?;
309
310 Ok(res)
311 }
312
313 pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
318 let res = self.provider.send_raw_transaction(raw_tx).await?;
319 Ok(res)
320 }
321
322 pub async fn send_raw_sync(&self, raw_tx: &[u8]) -> Result<(B256, String)> {
324 let mut receipt = TransactionReceiptWithRevertReason::<N> {
325 receipt: self.provider.send_raw_transaction_sync(raw_tx).await?,
326 revert_reason: None,
327 };
328 let tx_hash = receipt.receipt.transaction_hash();
329 let _ = receipt.update_revert_reason(&self.provider).await;
331
332 self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
333 }
334
335 pub async fn print_tx_result(
340 &self,
341 tx_hash: B256,
342 cast_async: bool,
343 confs: u64,
344 timeout: u64,
345 ) -> Result<()> {
346 if cast_async {
347 sh_println!("{tx_hash:#x}")?;
348 } else {
349 let receipt =
350 self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
351 sh_println!("{receipt}")?;
352 }
353 Ok(())
354 }
355
356 pub async fn receipt(
373 &self,
374 tx_hash: String,
375 field: Option<String>,
376 confs: u64,
377 timeout: Option<u64>,
378 cast_async: bool,
379 ) -> Result<String> {
380 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
381
382 let mut receipt = TransactionReceiptWithRevertReason::<N> {
383 receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
384 Some(r) => r,
385 None => {
386 if cast_async {
389 eyre::bail!("tx not found: {:?}", tx_hash);
390 }
391 PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
392 .with_required_confirmations(confs)
393 .with_timeout(timeout.map(Duration::from_secs))
394 .get_receipt()
395 .await?
396 }
397 },
398 revert_reason: None,
399 };
400
401 let _ = receipt.update_revert_reason(&self.provider).await;
403
404 self.format_receipt(receipt, field)
405 }
406
407 fn format_receipt(
409 &self,
410 receipt: TransactionReceiptWithRevertReason<N>,
411 field: Option<String>,
412 ) -> Result<String> {
413 Ok(if let Some(ref field) = field {
414 get_pretty_receipt_w_reason_attr(&receipt, field)
415 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
416 } else if shell::is_json() {
417 serde_json::to_value(&receipt)?.to_string()
419 } else {
420 receipt.pretty()
421 })
422 }
423}
424
425#[derive(Debug)]
430pub struct CastTxBuilder<N: Network, P, S> {
431 provider: P,
432 pub(crate) tx: N::TransactionRequest,
433 legacy: bool,
435 blob: bool,
436 eip4844: bool,
438 fill: bool,
441 browser: bool,
443 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
445 gas_estimate_multiplier: Option<u64>,
447 auth: Vec<CliAuthorizationList>,
448 chain: Chain,
449 etherscan_api_key: Option<String>,
450 etherscan_api_url: Option<String>,
451 access_list: Option<Option<AccessList>>,
452 state: S,
453}
454
455impl<N: Network, P, S> CastTxBuilder<N, P, S> {
456 pub const fn chain(&self) -> Chain {
458 self.chain
459 }
460
461 pub const fn with_browser_wallet(mut self) -> Self {
463 self.browser = true;
464 self
465 }
466
467 pub const fn with_gas_estimate_multiplier(mut self, multiplier: Option<u64>) -> Self {
469 self.gas_estimate_multiplier = multiplier;
470 self
471 }
472
473 pub(crate) const fn has_auth(&self) -> bool {
475 !self.auth.is_empty()
476 }
477
478 pub(crate) fn validate_auth(&self, sender: &SenderKind<'_>) -> Result<()> {
480 validate_authorizations(&self.auth, sender)
481 }
482
483 pub(crate) fn will_disclose_auth_during_build(&self) -> bool {
486 self.has_auth()
487 && (matches!(self.access_list, Some(None))
489 || (self.fill && self.tx.gas_limit().is_none()))
491 }
492}
493
494impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
495where
496 N::TransactionRequest: FoundryTransactionBuilder<N>,
497{
498 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
501 let mut tx = N::TransactionRequest::default();
502
503 let chain = utils::get_chain(config.chain, &provider).await?;
504 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
505 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
506 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
507 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
509
510 tx_opts.apply::<N>(&mut tx, legacy);
512
513 Ok(Self {
514 provider,
515 tx,
516 legacy,
517 blob: tx_opts.blob,
518 eip4844: tx_opts.eip4844,
519 fill: true,
520 browser: false,
521 eip1559_fee_estimate: config.eip1559_fee_estimate,
522 gas_estimate_multiplier: None,
523 chain,
524 etherscan_api_key,
525 etherscan_api_url,
526 auth: tx_opts.auth,
527 access_list: tx_opts.access_list,
528 state: InitState,
529 })
530 }
531
532 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
534 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
535 Ok(CastTxBuilder {
536 provider: self.provider,
537 tx: self.tx,
538 legacy: self.legacy,
539 blob: self.blob,
540 eip4844: self.eip4844,
541 fill: self.fill,
542 browser: self.browser,
543 eip1559_fee_estimate: self.eip1559_fee_estimate,
544 gas_estimate_multiplier: self.gas_estimate_multiplier,
545 chain: self.chain,
546 etherscan_api_key: self.etherscan_api_key,
547 etherscan_api_url: self.etherscan_api_url,
548 auth: self.auth,
549 access_list: self.access_list,
550 state: ToState { to },
551 })
552 }
553}
554
555impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
556where
557 N::TransactionRequest: FoundryTransactionBuilder<N>,
558{
559 pub async fn with_code_sig_and_args(
563 self,
564 code: Option<String>,
565 sig: Option<String>,
566 args: Vec<String>,
567 ) -> Result<CastTxBuilder<N, P, InputState>> {
568 let (mut args, func) = if let Some(sig) = sig {
569 parse_function_args(
570 &sig,
571 args,
572 self.state.to,
573 self.chain,
574 &self.provider,
575 self.etherscan_api_key.as_deref(),
576 self.etherscan_api_url.as_deref(),
577 )
578 .await?
579 } else {
580 (Vec::new(), None)
581 };
582
583 let input = if let Some(code) = &code {
584 let mut code = hex::decode(code)?;
585 code.append(&mut args);
586 code
587 } else {
588 args
589 };
590
591 if self.state.to.is_none() && code.is_none() {
592 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
593 let has_auth = !self.auth.is_empty();
594 if !has_auth || has_value {
597 eyre::bail!("Must specify a recipient address or contract code to deploy");
598 }
599 }
600
601 Ok(CastTxBuilder {
602 provider: self.provider,
603 tx: self.tx,
604 legacy: self.legacy,
605 blob: self.blob,
606 eip4844: self.eip4844,
607 fill: self.fill,
608 browser: self.browser,
609 eip1559_fee_estimate: self.eip1559_fee_estimate,
610 gas_estimate_multiplier: self.gas_estimate_multiplier,
611 chain: self.chain,
612 etherscan_api_key: self.etherscan_api_key,
613 etherscan_api_url: self.etherscan_api_url,
614 auth: self.auth,
615 access_list: self.access_list,
616 state: InputState { kind: self.state.to.into(), input, func },
617 })
618 }
619}
620
621impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
622where
623 N::TransactionRequest: FoundryTransactionBuilder<N>,
624{
625 pub async fn build(
628 self,
629 sender: impl Into<SenderKind<'_>>,
630 ) -> Result<(N::TransactionRequest, Option<Function>)> {
631 self.build_inner(sender, None).await
632 }
633
634 pub async fn build_with_tempo_wallet(
640 self,
641 wallet: &TempoAccountsWallet,
642 ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
643 let mut prepared = wallet.clone();
644 let (tx, func) = self.build_inner(wallet.account(), Some(&mut prepared)).await?;
645 Ok((tx, func, prepared))
646 }
647
648 async fn build_inner(
649 mut self,
650 sender: impl Into<SenderKind<'_>>,
651 tempo_wallet: Option<&mut TempoAccountsWallet>,
652 ) -> Result<(N::TransactionRequest, Option<Function>)> {
653 let fill = self.fill;
654
655 let sender = sender.into();
657 self.prepare(&sender);
658
659 self.tx.clear_batch_to();
663
664 let resolve_in_parallel =
668 fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
669 let tx_nonce = if resolve_in_parallel {
670 let nonce = self.tx.nonce();
671 let fees_are_complete = if self.legacy {
672 self.tx.gas_price().is_some()
673 } else {
674 matches!(
675 (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
676 (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
677 )
678 } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
679 let gas_request =
680 (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
681 .then(|| self.tx.clone());
682 let (tx_nonce, (), gas_limit) = tokio::try_join!(
683 Self::resolve_nonce(&self.provider, sender.address(), nonce),
684 Self::fill_fees(
685 &self.provider,
686 &mut self.tx,
687 self.blob,
688 self.legacy,
689 self.browser,
690 self.eip1559_fee_estimate,
691 ),
692 async {
693 match gas_request {
694 Some(request) => Self::estimate_gas(
695 &self.provider,
696 request,
697 self.gas_estimate_multiplier,
698 )
699 .await
700 .map(Some),
701 None => Ok(None),
702 }
703 },
704 )?;
705 if let Some(gas_limit) = gas_limit {
706 self.tx.set_gas_limit(gas_limit);
707 }
708 Some(tx_nonce)
709 } else if fill || !self.auth.is_empty() {
710 Some(Self::resolve_nonce(&self.provider, sender.address(), self.tx.nonce()).await?)
711 } else {
712 None
713 };
714 if let Some(tx_nonce) = tx_nonce {
715 if fill {
716 self.tx.set_nonce(tx_nonce);
717 }
718 self.resolve_auth(&sender, tx_nonce).await?;
719 }
720 if let Some(wallet) = tempo_wallet {
721 *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
722 }
723 if fill && !resolve_in_parallel {
724 Self::fill_fees(
725 &self.provider,
726 &mut self.tx,
727 self.blob,
728 self.legacy,
729 self.browser,
730 self.eip1559_fee_estimate,
731 )
732 .await?;
733 }
734 self.resolve_access_list().await?;
735 if fill {
736 self.fill_gas_limit().await?;
737 }
738
739 Ok((self.tx, self.state.func))
740 }
741
742 fn prepare(&mut self, sender: &SenderKind<'_>) {
745 self.tx.set_kind(self.state.kind);
746 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
749 let sender = sender.address();
750 if !sender.is_zero() {
751 self.tx.set_from(sender);
752 }
753 self.tx.set_chain_id(self.chain.id());
754 }
755
756 async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
758 if let Some(nonce) = nonce {
759 Ok(nonce)
760 } else {
761 Ok(provider.get_transaction_count(from).await?)
762 }
763 }
764
765 async fn resolve_access_list(&mut self) -> Result<()> {
768 if let Some(access_list) = match self.access_list.take() {
769 None => None,
770 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
771 Some(Some(access_list)) => Some(access_list),
772 } {
773 self.tx.set_access_list(access_list);
774 }
775 Ok(())
776 }
777
778 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
783 if self.auth.is_empty() {
784 return Ok(());
785 }
786
787 self.validate_auth(sender)?;
788 let auths = std::mem::take(&mut self.auth);
789
790 let mut signed_auths = Vec::with_capacity(auths.len());
791
792 for auth in auths {
793 let signed_auth = match auth {
794 CliAuthorizationList::Address(address) => {
795 let auth = Authorization {
796 chain_id: U256::from(self.chain.id()),
797 nonce: tx_nonce + 1,
798 address,
799 };
800
801 let signer =
802 sender.as_signer().expect("address-based authorization requires a signer");
803 let signature = signer.sign_hash(&auth.signature_hash()).await?;
804
805 auth.into_signed(signature)
806 }
807 CliAuthorizationList::Signed(auth) => auth,
808 };
809 signed_auths.push(signed_auth);
810 }
811
812 self.tx.set_authorization_list(signed_auths);
813
814 Ok(())
815 }
816
817 async fn fill_fees(
821 provider: &P,
822 tx: &mut N::TransactionRequest,
823 blob: bool,
824 legacy: bool,
825 browser: bool,
826 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
827 ) -> Result<()> {
828 if blob && tx.max_fee_per_blob_gas().is_none() {
829 tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
830 }
831
832 fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
833 }
834
835 async fn fill_gas_limit(&mut self) -> Result<()> {
837 if self.tx.gas_limit().is_none() {
838 let request = if self.browser && self.chain.is_tempo() {
839 self.tx.browser_wallet_gas_estimation_request()
840 } else {
841 self.tx.clone()
842 };
843 let estimated =
844 Self::estimate_gas(&self.provider, request, self.gas_estimate_multiplier).await?;
845 self.tx.set_gas_limit(estimated);
846 }
847
848 Ok(())
849 }
850
851 async fn estimate_gas(
853 provider: &P,
854 request: N::TransactionRequest,
855 multiplier: Option<u64>,
856 ) -> Result<u64> {
857 match provider.estimate_gas(request).await {
858 Ok(estimated) => apply_gas_estimate_multiplier(estimated, multiplier),
859 Err(err) => {
860 if let TransportError::ErrorResp(payload) = &err {
861 if payload.code == 3
864 && let Some(data) = &payload.data
865 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
866 {
867 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error);
868 }
869 }
870 eyre::bail!("Failed to estimate gas: {}", err);
871 }
872 }
873 }
874
875 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
877 let Some(blob_data) = blob_data else { return Ok(self) };
878
879 let mut coder = SidecarBuilder::<SimpleCoder>::default();
880 coder.ingest(&blob_data);
881
882 if self.eip4844 {
883 let sidecar = coder.build_4844()?;
884 self.tx.set_blob_sidecar_4844(sidecar);
885 } else {
886 let sidecar = coder.build_7594()?;
887 self.tx.set_blob_sidecar_7594(sidecar);
888 }
889
890 Ok(self)
891 }
892
893 pub const fn raw(mut self) -> Self {
896 self.fill = false;
897 self
898 }
899}
900
901pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
903 provider: &P,
904 tx: &mut N::TransactionRequest,
905 legacy: bool,
906 browser: bool,
907 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
908) -> Result<()>
909where
910 N::TransactionRequest: FoundryTransactionBuilder<N>,
911{
912 if legacy {
913 if tx.gas_price().is_none() {
914 tx.set_gas_price(provider.get_gas_price().await?);
915 }
916 return Ok(());
917 }
918
919 if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
920 let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
921
922 let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
925 provider.get_max_priority_fee_per_gas().await.ok()
926 } else {
927 None
928 };
929
930 let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
933
934 if tx.max_fee_per_gas().is_none() {
935 tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
936 }
937
938 if tx.max_priority_fee_per_gas().is_none() {
939 tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
940 }
941 }
942
943 if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
944 eyre::ensure!(
945 priority <= max_fee,
946 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
947 );
948 }
949
950 Ok(())
951}
952
953async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
955 let data = serde_json::from_str::<Bytes>(data.get())?;
956 decode_custom_error(&data).await
957}
958
959pub(crate) async fn decode_custom_error(data: &[u8]) -> Result<Option<String>> {
960 let Some(selector) = data.get(..4) else { return Ok(None) };
961 if let Some(known_error) =
962 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
963 {
964 let mut decoded_error = known_error.name.clone();
965 if !known_error.inputs.is_empty()
966 && let Ok(error) = known_error.decode_error(data)
967 {
968 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
969 }
970 return Ok(Some(decoded_error));
971 }
972 Ok(None)
973}
974
975#[cfg(test)]
976mod tests {
977 use super::*;
978 use alloy_json_rpc::{RequestPacket, ResponsePacket};
979 use alloy_network::Ethereum;
980 use alloy_provider::{ProviderBuilder, mock::Asserter};
981 use alloy_rpc_client::RpcClient;
982 use alloy_transport::{TransportFut, mock::MockTransport};
983 use clap::Parser;
984 use std::{
985 sync::{Arc, Mutex},
986 task::{Context, Poll},
987 };
988 use tokio::{sync::Barrier, time::timeout};
989 use tower::Service;
990
991 #[derive(Clone)]
992 struct BarrierTransport {
993 inner: MockTransport,
994 barrier: Arc<Barrier>,
995 fill_methods: Arc<Mutex<Vec<String>>>,
996 }
997
998 impl Service<RequestPacket> for BarrierTransport {
999 type Response = ResponsePacket;
1000 type Error = TransportError;
1001 type Future = TransportFut<'static>;
1002
1003 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1004 self.inner.poll_ready(cx)
1005 }
1006
1007 fn call(&mut self, req: RequestPacket) -> Self::Future {
1008 let fill_method = match &req {
1009 RequestPacket::Single(req)
1010 if matches!(
1011 req.method(),
1012 "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
1013 ) =>
1014 {
1015 Some(req.method().to_string())
1016 }
1017 _ => None,
1018 };
1019 let Some(fill_method) = fill_method else {
1020 return self.inner.call(req);
1021 };
1022 self.fill_methods.lock().unwrap().push(fill_method);
1023
1024 let barrier = self.barrier.clone();
1025 let mut inner = self.inner.clone();
1026 Box::pin(async move {
1027 barrier.wait().await;
1028 inner.call(req).await
1029 })
1030 }
1031 }
1032
1033 #[tokio::test]
1034 async fn filled_build_applies_multiplier_to_concurrent_gas_estimate() {
1035 let asserter = Asserter::new();
1036 for _ in 0..2 {
1037 asserter.push_success(&U64::from(100));
1038 }
1039 let fill_methods = Arc::new(Mutex::new(Vec::new()));
1040 let transport = BarrierTransport {
1041 inner: MockTransport::new(asserter),
1042 barrier: Arc::new(Barrier::new(2)),
1043 fill_methods: fill_methods.clone(),
1044 };
1045 let provider = ProviderBuilder::new_with_network::<Ethereum>()
1046 .connect_client(RpcClient::new(transport, true));
1047 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1048
1049 let builder = CastTxBuilder::new(
1050 &provider,
1051 TransactionOpts::parse_from(["test", "--legacy", "--gas-price", "1"]),
1052 &config,
1053 )
1054 .await
1055 .unwrap()
1056 .with_gas_estimate_multiplier(Some(150))
1057 .with_to(Some(Address::repeat_byte(0x11).into()))
1058 .await
1059 .unwrap()
1060 .with_code_sig_and_args(None, None, Vec::new())
1061 .await
1062 .unwrap();
1063 let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
1064 .await
1065 .expect("nonce and gas requests were not in flight together")
1066 .unwrap();
1067
1068 assert_eq!(tx.nonce, Some(100));
1069 assert_eq!(tx.gas_price, Some(1));
1070 assert_eq!(tx.gas, Some(150));
1071 let mut fill_methods = fill_methods.lock().unwrap().clone();
1072 fill_methods.sort();
1073 assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
1074 }
1075
1076 #[tokio::test]
1077 async fn raw_build_skips_nonce_request() {
1078 let provider =
1081 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1082 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1083
1084 CastTxBuilder::new(&provider, TransactionOpts::parse_from(["test"]), &config)
1085 .await
1086 .unwrap()
1087 .with_to(Some(Address::repeat_byte(0x11).into()))
1088 .await
1089 .unwrap()
1090 .with_code_sig_and_args(None, None, Vec::new())
1091 .await
1092 .unwrap()
1093 .raw()
1094 .build(Address::repeat_byte(0x22))
1095 .await
1096 .unwrap();
1097 }
1098
1099 #[tokio::test]
1100 async fn detects_auth_rpc_disclosure() {
1101 let provider =
1102 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1103 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1104 let address = Address::repeat_byte(0x11);
1105
1106 let no_auth = CastTxBuilder::new(
1107 &provider,
1108 TransactionOpts::parse_from(["test", "--gas-limit", "21000"]),
1109 &config,
1110 )
1111 .await
1112 .unwrap()
1113 .with_to(Some(address.into()))
1114 .await
1115 .unwrap()
1116 .with_code_sig_and_args(None, None, Vec::new())
1117 .await
1118 .unwrap()
1119 .raw();
1120 assert!(!no_auth.has_auth());
1121 assert!(!no_auth.will_disclose_auth_during_build());
1122
1123 let rpc_call = CastTxBuilder::new(
1124 &provider,
1125 TransactionOpts::parse_from([
1126 "test",
1127 "--auth",
1128 &address.to_string(),
1129 "--gas-limit",
1130 "21000",
1131 ]),
1132 &config,
1133 )
1134 .await
1135 .unwrap()
1136 .with_to(Some(address.into()))
1137 .await
1138 .unwrap()
1139 .with_code_sig_and_args(None, None, Vec::new())
1140 .await
1141 .unwrap()
1142 .raw();
1143 assert!(rpc_call.has_auth());
1144 assert!(!rpc_call.will_disclose_auth_during_build());
1145
1146 let generated_access_list = CastTxBuilder::new(
1147 &provider,
1148 TransactionOpts::parse_from(["test", "--auth", &address.to_string(), "--access-list"]),
1149 &config,
1150 )
1151 .await
1152 .unwrap()
1153 .with_to(Some(address.into()))
1154 .await
1155 .unwrap()
1156 .with_code_sig_and_args(None, None, Vec::new())
1157 .await
1158 .unwrap()
1159 .raw();
1160 assert!(generated_access_list.will_disclose_auth_during_build());
1161
1162 let explicit_access_list = CastTxBuilder::new(
1163 &provider,
1164 TransactionOpts::parse_from([
1165 "test",
1166 "--auth",
1167 &address.to_string(),
1168 "--access-list",
1169 "[]",
1170 ]),
1171 &config,
1172 )
1173 .await
1174 .unwrap()
1175 .with_to(Some(address.into()))
1176 .await
1177 .unwrap()
1178 .with_code_sig_and_args(None, None, Vec::new())
1179 .await
1180 .unwrap()
1181 .raw();
1182 assert!(!explicit_access_list.will_disclose_auth_during_build());
1183
1184 let estimated_gas = CastTxBuilder::new(
1185 &provider,
1186 TransactionOpts::parse_from(["test", "--auth", &address.to_string()]),
1187 &config,
1188 )
1189 .await
1190 .unwrap();
1191 assert!(estimated_gas.will_disclose_auth_during_build());
1192 }
1193}