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_ether_value, 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 tempo::ensure_expiring_nonce_discriminator_active,
25};
26use foundry_config::{Chain, Config, Eip1559FeeEstimatePreset};
27use foundry_wallets::{BrowserWalletOpts, TempoAccountsWallet, WalletOpts, WalletSigner};
28use itertools::Itertools;
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
64pub(crate) fn apply_poll_interval<N: Network>(provider: &impl Provider<N>, interval: Option<u64>) {
66 if let Some(interval) = interval {
67 provider.client().set_poll_interval(Duration::from_secs(interval));
68 }
69}
70
71#[derive(Debug, Clone, Args)]
73#[command(next_help_heading = "Transaction options")]
74pub struct TxParams {
75 #[arg(long, env = "ETH_GAS_LIMIT")]
77 pub gas_limit: Option<U256>,
78
79 #[arg(
84 long,
85 env = "ETH_GAS_PRICE",
86 value_parser = parse_ether_value,
87 value_name = "PRICE"
88 )]
89 pub gas_price: Option<U256>,
90
91 #[arg(
93 long,
94 env = "ETH_PRIORITY_GAS_PRICE",
95 value_parser = parse_ether_value,
96 value_name = "PRICE"
97 )]
98 pub priority_gas_price: Option<U256>,
99
100 #[arg(long)]
102 pub nonce: Option<U64>,
103
104 #[command(flatten)]
105 pub tempo: TempoOpts,
106}
107
108impl TxParams {
109 pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
110 where
111 N::TransactionRequest: FoundryTransactionBuilder<N>,
112 {
113 self.clone().into_transaction_opts().apply::<N>(tx, legacy);
114 }
115
116 pub(crate) fn into_transaction_opts(self) -> TransactionOpts {
118 TransactionOpts {
119 gas_limit: self.gas_limit,
120 gas_price: self.gas_price,
121 priority_gas_price: self.priority_gas_price,
122 value: None,
123 nonce: self.nonce,
124 legacy: false,
125 blob: false,
126 eip4844: false,
127 blob_gas_price: None,
128 auth: Vec::new(),
129 access_list: None,
130 tempo: self.tempo,
131 }
132 }
133}
134
135pub enum SenderKind<'a> {
137 Address(Address),
140 Signer(&'a WalletSigner),
142 OwnedSigner(Box<WalletSigner>),
144}
145
146impl SenderKind<'_> {
147 pub fn address(&self) -> Address {
149 match self {
150 Self::Address(addr) => *addr,
151 Self::Signer(signer) => signer.address(),
152 Self::OwnedSigner(signer) => signer.address(),
153 }
154 }
155
156 pub async fn from_wallet_opts(
162 mut opts: WalletOpts,
163 authorizations: &[CliAuthorizationList],
164 ) -> Result<Self> {
165 let from = opts.from.take();
166 if let Some(from) = from
167 && opts.tempo_access_key.is_none()
168 && !authorizations.iter().any(|auth| matches!(auth, CliAuthorizationList::Address(_)))
169 {
170 return Ok(from.into());
171 }
172 let (signer, tempo_wallet) = opts.maybe_signer().await?;
173 Ok(if let Some(signer) = signer {
174 signer.into()
175 } else if let Some(tempo_wallet) = tempo_wallet {
176 tempo_wallet.account().into()
177 } else {
178 from.unwrap_or_default().into()
179 })
180 }
181
182 pub fn as_signer(&self) -> Option<&WalletSigner> {
184 match self {
185 Self::Signer(signer) => Some(signer),
186 Self::OwnedSigner(signer) => Some(signer.as_ref()),
187 Self::Address(_) => None,
188 }
189 }
190}
191
192impl From<Address> for SenderKind<'_> {
193 fn from(addr: Address) -> Self {
194 Self::Address(addr)
195 }
196}
197
198impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
199 fn from(signer: &'a WalletSigner) -> Self {
200 Self::Signer(signer)
201 }
202}
203
204impl From<WalletSigner> for SenderKind<'_> {
205 fn from(signer: WalletSigner) -> Self {
206 Self::OwnedSigner(Box::new(signer))
207 }
208}
209
210pub(crate) async fn read_only_sender<N: Network>(
213 browser: &BrowserWalletOpts,
214 wallet: WalletOpts,
215 tx: &TransactionOpts,
216 chain_id: u64,
217) -> Result<(SenderKind<'static>, bool)> {
218 crate::tempo::ensure_session_not_browser(&tx.tempo, browser.browser)?;
219 if let Some(session) = tx.tempo.session_signer_for_wallet(&wallet, chain_id)? {
220 return Ok((session.access_key.account().into(), false));
221 }
222
223 Ok(match browser.run::<N>().await? {
224 Some(browser) => (browser.address().into(), true),
225 None => (SenderKind::from_wallet_opts(wallet, &tx.auth).await?, false),
226 })
227}
228
229pub(crate) fn validate_authorizations(
231 authorizations: &[CliAuthorizationList],
232 sender: &SenderKind<'_>,
233) -> Result<()> {
234 let address_auth_count = authorizations
235 .iter()
236 .filter(|auth| matches!(auth, CliAuthorizationList::Address(_)))
237 .count();
238 if address_auth_count > 1 {
239 eyre::bail!(
240 "Multiple address-based authorizations provided. Only one address can be specified; \
241 use pre-signed authorizations (hex-encoded) for multiple authorizations."
242 );
243 }
244 if address_auth_count == 1 && sender.as_signer().is_none() {
245 eyre::bail!(
246 "No signer available to sign authorization. \
247 Provide a pre-signed authorization (hex-encoded) instead."
248 );
249 }
250
251 Ok(())
252}
253
254pub(crate) async fn resolve_send_signer(
257 pre_resolved: Option<WalletSigner>,
258 eth: &EthereumOpts,
259) -> Result<(WalletSigner, Address)> {
260 let signer = match pre_resolved {
261 Some(signer) => signer,
262 None => eth.wallet.signer().await?,
263 };
264 let from = signer.address();
265 validate_from_address(eth.wallet.from, from)?;
266 Ok((signer, from))
267}
268
269pub fn validate_from_address(
271 specified_from: Option<Address>,
272 signer_address: Address,
273) -> Result<()> {
274 if let Some(specified_from) = specified_from
275 && specified_from != signer_address
276 {
277 eyre::bail!(
278 "\
279The specified sender via CLI/env vars does not match the sender configured via
280the hardware wallet's HD Path.
281Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
282corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
283 );
284 }
285 Ok(())
286}
287
288#[derive(Debug)]
290pub struct InitState;
291
292#[derive(Debug)]
294pub struct ToState {
295 to: Option<Address>,
296}
297
298#[derive(Debug)]
300pub struct InputState {
301 kind: TxKind,
302 input: Vec<u8>,
303 func: Option<Function>,
304}
305
306pub struct CastTxSender<N, P> {
307 provider: P,
308 _phantom: PhantomData<N>,
309}
310
311impl<N: Network, P: Provider<N>> CastTxSender<N, P>
312where
313 N::TransactionRequest: FoundryTransactionBuilder<N>,
314 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
315{
316 pub const fn new(provider: P) -> Self {
318 Self { provider, _phantom: PhantomData }
319 }
320
321 pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
323 let receipt = self.provider.send_transaction_sync(tx).await?;
324 self.finish(receipt, None).await
325 }
326
327 pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
329 Ok(self.provider.send_transaction(tx).await?)
330 }
331
332 pub async fn send_raw_sync(&self, raw_tx: &[u8]) -> Result<(B256, String)> {
334 let receipt = self.provider.send_raw_transaction_sync(raw_tx).await?;
335 self.finish(receipt, None).await
336 }
337
338 pub async fn print_tx_result(
343 &self,
344 tx_hash: B256,
345 cast_async: bool,
346 confs: u64,
347 timeout: u64,
348 ) -> Result<()> {
349 if cast_async {
350 sh_println!("{tx_hash:#x}")?;
351 } else {
352 let receipt =
353 self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
354 sh_println!("{receipt}")?;
355 }
356 Ok(())
357 }
358
359 pub async fn receipt(
362 &self,
363 tx_hash: String,
364 field: Option<String>,
365 confs: u64,
366 timeout: Option<u64>,
367 cast_async: bool,
368 ) -> Result<String> {
369 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
370 let receipt = match self.provider.get_transaction_receipt(tx_hash).await? {
371 Some(receipt) => receipt,
372 None if cast_async => eyre::bail!("tx not found: {tx_hash:?}"),
373 None => {
374 PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
375 .with_required_confirmations(confs)
376 .with_timeout(timeout.map(Duration::from_secs))
377 .get_receipt()
378 .await?
379 }
380 };
381 Ok(self.finish(receipt, field).await?.1)
382 }
383
384 async fn finish(
386 &self,
387 receipt: N::ReceiptResponse,
388 field: Option<String>,
389 ) -> Result<(B256, String)> {
390 let mut receipt = TransactionReceiptWithRevertReason::<N> { receipt, revert_reason: None };
391 let tx_hash = receipt.receipt.transaction_hash();
392 let _ = receipt.update_revert_reason(&self.provider).await;
393 let formatted = if let Some(field) = field {
394 get_pretty_receipt_w_reason_attr(&receipt, &field)
395 .ok_or_else(|| eyre::eyre!("invalid receipt field: {field}"))?
396 } else if shell::is_json() {
397 serde_json::to_value(&receipt)?.to_string()
399 } else {
400 receipt.pretty()
401 };
402 Ok((tx_hash, formatted))
403 }
404}
405
406#[derive(Debug)]
411pub struct CastTxBuilder<N: Network, P, S> {
412 inner: TxBuilderInner<N, P>,
413 state: S,
414}
415
416#[derive(Debug)]
418struct TxBuilderInner<N: Network, P> {
419 provider: P,
420 tx: N::TransactionRequest,
421 legacy: bool,
423 blob: bool,
424 eip4844: bool,
426 fill: bool,
429 browser: bool,
431 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
433 gas_estimate_multiplier: Option<u64>,
435 auth: Vec<CliAuthorizationList>,
436 chain: Chain,
437 etherscan_api_key: Option<String>,
438 etherscan_api_url: Option<String>,
439 access_list: Option<Option<AccessList>>,
440}
441
442impl<N: Network, P, S> CastTxBuilder<N, P, S> {
443 pub const fn chain(&self) -> Chain {
445 self.inner.chain
446 }
447
448 pub(crate) fn etherscan_api(&self) -> (Option<&str>, Option<&str>) {
450 (self.inner.etherscan_api_key.as_deref(), self.inner.etherscan_api_url.as_deref())
451 }
452
453 pub(crate) const fn tx_mut(&mut self) -> &mut N::TransactionRequest {
455 &mut self.inner.tx
456 }
457
458 pub const fn with_browser_wallet(mut self) -> Self {
460 self.inner.browser = true;
461 self
462 }
463
464 pub const fn with_gas_estimate_multiplier(mut self, multiplier: Option<u64>) -> Self {
466 self.inner.gas_estimate_multiplier = multiplier;
467 self
468 }
469
470 pub(crate) const fn has_auth(&self) -> bool {
472 !self.inner.auth.is_empty()
473 }
474
475 pub(crate) fn validate_auth(&self, sender: &SenderKind<'_>) -> Result<()> {
477 validate_authorizations(&self.inner.auth, sender)
478 }
479
480 pub(crate) fn will_disclose_auth_during_build(&self) -> bool {
483 self.has_auth()
486 && (matches!(self.inner.access_list, Some(None))
487 || (self.inner.fill && self.inner.tx.gas_limit().is_none()))
488 }
489
490 fn with_state<S2>(self, state: S2) -> CastTxBuilder<N, P, S2> {
491 CastTxBuilder { inner: self.inner, state }
492 }
493}
494
495impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
496where
497 N::TransactionRequest: FoundryTransactionBuilder<N>,
498{
499 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
502 let mut tx = N::TransactionRequest::default();
503
504 let chain = utils::get_chain(config.chain, &provider).await?;
505 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
506 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
507 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
508 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
510
511 tx_opts.apply::<N>(&mut tx, legacy);
512 if !config.eth_rpc_curl {
515 ensure_expiring_nonce_discriminator_active(&provider, &tx).await?;
516 }
517
518 Ok(Self {
519 inner: TxBuilderInner {
520 provider,
521 tx,
522 legacy,
523 blob: tx_opts.blob,
524 eip4844: tx_opts.eip4844,
525 fill: true,
526 browser: false,
527 eip1559_fee_estimate: config.eip1559_fee_estimate,
528 gas_estimate_multiplier: None,
529 chain,
530 etherscan_api_key,
531 etherscan_api_url,
532 auth: tx_opts.auth,
533 access_list: tx_opts.access_list,
534 },
535 state: InitState,
536 })
537 }
538
539 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
541 let to = match to {
542 Some(to) => Some(to.resolve(&self.inner.provider).await?),
543 None => None,
544 };
545 Ok(self.with_state(ToState { to }))
546 }
547}
548
549impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
550where
551 N::TransactionRequest: FoundryTransactionBuilder<N>,
552{
553 pub async fn with_code_sig_and_args(
557 self,
558 code: Option<String>,
559 sig: Option<String>,
560 args: Vec<String>,
561 ) -> Result<CastTxBuilder<N, P, InputState>> {
562 let to = self.state.to;
563 let (mut args, func) = if let Some(sig) = sig {
564 let (key, url) = self.etherscan_api();
565 parse_function_args(&sig, args, to, self.chain(), &self.inner.provider, key, url)
566 .await?
567 } else {
568 (Vec::new(), None)
569 };
570
571 let input = if let Some(code) = &code {
572 let mut code = hex::decode(code)?;
573 code.append(&mut args);
574 code
575 } else {
576 args
577 };
578
579 if to.is_none()
582 && code.is_none()
583 && (!self.has_auth() || self.inner.tx.value().is_some_and(|v| !v.is_zero()))
584 {
585 eyre::bail!("Must specify a recipient address or contract code to deploy");
586 }
587 if to.is_none() && code.is_some() && self.has_auth() {
588 eyre::bail!(
589 "EIP-7702 transactions can't be CREATE transactions and require a destination address"
590 );
591 }
592
593 Ok(self.with_state(InputState { kind: to.into(), input, func }))
594 }
595}
596
597impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
598where
599 N::TransactionRequest: FoundryTransactionBuilder<N>,
600{
601 pub async fn build(
604 self,
605 sender: impl Into<SenderKind<'_>>,
606 ) -> Result<(N::TransactionRequest, Option<Function>)> {
607 self.inner.build(self.state, &sender.into(), None).await
608 }
609
610 pub async fn build_with_tempo_wallet(
616 self,
617 wallet: &TempoAccountsWallet,
618 ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
619 let mut prepared = wallet.clone();
620 let (tx, func) =
621 self.inner.build(self.state, &wallet.account().into(), Some(&mut prepared)).await?;
622 Ok((tx, func, prepared))
623 }
624
625 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
627 let Some(blob_data) = blob_data else { return Ok(self) };
628
629 let mut coder = SidecarBuilder::<SimpleCoder>::default();
630 coder.ingest(&blob_data);
631 if self.inner.eip4844 {
632 self.inner.tx.set_blob_sidecar_4844(coder.build_4844()?);
633 } else {
634 self.inner.tx.set_blob_sidecar_7594(coder.build_7594()?);
635 }
636 Ok(self)
637 }
638
639 pub const fn raw(mut self) -> Self {
642 self.inner.fill = false;
643 self
644 }
645}
646
647impl<N: Network, P: Provider<N>> TxBuilderInner<N, P>
648where
649 N::TransactionRequest: FoundryTransactionBuilder<N>,
650{
651 async fn build(
652 mut self,
653 state: InputState,
654 sender: &SenderKind<'_>,
655 tempo_wallet: Option<&mut TempoAccountsWallet>,
656 ) -> Result<(N::TransactionRequest, Option<Function>)> {
657 let fill = self.fill;
658 let from = sender.address();
659
660 let kind = match state.kind {
663 TxKind::Create if !self.auth.is_empty() => TxKind::Call(from),
664 kind => kind,
665 };
666 self.tx.set_kind(kind);
667 self.tx.set_input_kind(state.input, TransactionInputKind::Both);
670 if !from.is_zero() {
671 self.tx.set_from(from);
672 }
673 self.tx.set_chain_id(self.chain.id());
674 self.tx.clear_batch_to();
678
679 let resolve_in_parallel =
682 fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
683 let tx_nonce = if resolve_in_parallel {
684 let fees_are_complete = if self.legacy {
685 self.tx.gas_price().is_some()
686 } else {
687 matches!(
688 (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
689 (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
690 )
691 } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
692 let gas_request =
693 (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
694 .then(|| self.tx.clone());
695 let (blob, legacy, browser, eip1559_fee_estimate, gas_estimate_multiplier) = (
696 self.blob,
697 self.legacy,
698 self.browser,
699 self.eip1559_fee_estimate,
700 self.gas_estimate_multiplier,
701 );
702 let Self { provider, tx, .. } = &mut self;
703 let provider = &*provider;
704 let (tx_nonce, (), gas_limit) = tokio::try_join!(
705 Self::resolve_nonce(provider, from, tx.nonce()),
706 Self::fill_fees(provider, tx, blob, legacy, browser, eip1559_fee_estimate),
707 async {
708 match gas_request {
709 Some(request) => {
710 Self::estimate_gas(provider, request, gas_estimate_multiplier)
711 .await
712 .map(Some)
713 }
714 None => Ok(None),
715 }
716 },
717 )?;
718 if let Some(gas_limit) = gas_limit {
719 self.tx.set_gas_limit(gas_limit);
720 }
721 Some(tx_nonce)
722 } else if fill || !self.auth.is_empty() {
723 Some(Self::resolve_nonce(&self.provider, from, self.tx.nonce()).await?)
724 } else {
725 None
726 };
727 if let Some(tx_nonce) = tx_nonce {
728 if fill {
729 self.tx.set_nonce(tx_nonce);
730 }
731 self.resolve_auth(sender, tx_nonce).await?;
732 }
733 if let Some(wallet) = tempo_wallet {
734 *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
735 }
736 if fill && !resolve_in_parallel {
737 Self::fill_fees(
738 &self.provider,
739 &mut self.tx,
740 self.blob,
741 self.legacy,
742 self.browser,
743 self.eip1559_fee_estimate,
744 )
745 .await?;
746 }
747 let access_list = match self.access_list.take() {
749 None => None,
750 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
751 Some(Some(access_list)) => Some(access_list),
752 };
753 if let Some(access_list) = access_list {
754 self.tx.set_access_list(access_list);
755 }
756 if fill && self.tx.gas_limit().is_none() {
757 let request = if self.browser {
758 self.tx.browser_wallet_gas_estimation_request()
759 } else {
760 self.tx.clone()
761 };
762 let estimated =
763 Self::estimate_gas(&self.provider, request, self.gas_estimate_multiplier).await?;
764 self.tx.set_gas_limit(estimated);
765 }
766
767 Ok((self.tx, state.func))
768 }
769
770 async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
772 match nonce {
773 Some(nonce) => Ok(nonce),
774 None => Ok(provider.get_transaction_count(from).await?),
775 }
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 validate_authorizations(&self.auth, sender)?;
787
788 let mut signed_auths = Vec::with_capacity(self.auth.len());
789 for auth in std::mem::take(&mut self.auth) {
790 signed_auths.push(match auth {
791 CliAuthorizationList::Address(address) => {
792 let auth = Authorization {
793 chain_id: U256::from(self.chain.id()),
794 nonce: tx_nonce + 1,
795 address,
796 };
797 let signer =
798 sender.as_signer().expect("address-based authorization requires a signer");
799 let signature = signer.sign_hash(&auth.signature_hash()).await?;
800 auth.into_signed(signature)
801 }
802 CliAuthorizationList::Signed(auth) => auth,
803 });
804 }
805 self.tx.set_authorization_list(signed_auths);
806 Ok(())
807 }
808
809 async fn fill_fees(
813 provider: &P,
814 tx: &mut N::TransactionRequest,
815 blob: bool,
816 legacy: bool,
817 browser: bool,
818 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
819 ) -> Result<()> {
820 if blob && tx.max_fee_per_blob_gas().is_none() {
821 tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
822 }
823 fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
824 }
825
826 async fn estimate_gas(
828 provider: &P,
829 request: N::TransactionRequest,
830 multiplier: Option<u64>,
831 ) -> Result<u64> {
832 let err = match provider.estimate_gas(request).await {
833 Ok(estimated) => return apply_gas_estimate_multiplier(estimated, multiplier),
834 Err(err) => err,
835 };
836 if let TransportError::ErrorResp(payload) = &err
839 && payload.code == 3
840 && let Some(data) = &payload.data
841 && let Ok(data) = serde_json::from_str::<Bytes>(data.get())
842 && let Ok(Some(decoded_error)) = decode_custom_error(&data).await
843 {
844 eyre::bail!("Failed to estimate gas: {err}: {decoded_error}");
845 }
846 eyre::bail!("Failed to estimate gas: {err}");
847 }
848}
849
850pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
852 provider: &P,
853 tx: &mut N::TransactionRequest,
854 legacy: bool,
855 browser: bool,
856 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
857) -> Result<()>
858where
859 N::TransactionRequest: FoundryTransactionBuilder<N>,
860{
861 if legacy {
862 if tx.gas_price().is_none() {
863 tx.set_gas_price(provider.get_gas_price().await?);
864 }
865 return Ok(());
866 }
867
868 if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
869 let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
870
871 let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
874 provider.get_max_priority_fee_per_gas().await.ok()
875 } else {
876 None
877 };
878
879 let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
882
883 if tx.max_fee_per_gas().is_none() {
884 tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
885 }
886
887 if tx.max_priority_fee_per_gas().is_none() {
888 tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
889 }
890 }
891
892 if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
893 eyre::ensure!(
894 priority <= max_fee,
895 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
896 );
897 }
898
899 Ok(())
900}
901
902pub(crate) async fn decode_custom_error(data: &[u8]) -> Result<Option<String>> {
904 let Some(selector) = data.get(..4) else { return Ok(None) };
905 let Some(known_error) =
906 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
907 else {
908 return Ok(None);
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(data)
913 {
914 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
915 }
916 Ok(Some(decoded_error))
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922 use alloy_json_rpc::{RequestPacket, ResponsePacket};
923 use alloy_network::{Ethereum, NetworkTransactionBuilder};
924 use alloy_provider::{ProviderBuilder, mock::Asserter};
925 use alloy_rpc_client::RpcClient;
926 use alloy_sol_types::SolValue;
927 use alloy_transport::{TransportFut, mock::MockTransport};
928 use clap::Parser;
929 use foundry_common::tempo::resolve_and_set_fee_token;
930 use std::{
931 sync::{Arc, Mutex},
932 task::{Context, Poll},
933 };
934 use tempo_alloy::TempoNetwork;
935 use tokio::{sync::Barrier, time::timeout};
936 use tower::Service;
937
938 #[derive(Clone)]
939 struct BarrierTransport {
940 inner: MockTransport,
941 barrier: Arc<Barrier>,
942 fill_methods: Arc<Mutex<Vec<String>>>,
943 }
944
945 impl Service<RequestPacket> for BarrierTransport {
946 type Response = ResponsePacket;
947 type Error = TransportError;
948 type Future = TransportFut<'static>;
949
950 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
951 self.inner.poll_ready(cx)
952 }
953
954 fn call(&mut self, req: RequestPacket) -> Self::Future {
955 let fill_method = match &req {
956 RequestPacket::Single(req)
957 if matches!(
958 req.method(),
959 "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
960 ) =>
961 {
962 Some(req.method().to_string())
963 }
964 _ => None,
965 };
966 let Some(fill_method) = fill_method else {
967 return self.inner.call(req);
968 };
969 self.fill_methods.lock().unwrap().push(fill_method);
970
971 let barrier = self.barrier.clone();
972 let mut inner = self.inner.clone();
973 Box::pin(async move {
974 barrier.wait().await;
975 inner.call(req).await
976 })
977 }
978 }
979
980 const TO: Address = Address::repeat_byte(0x11);
981
982 async fn builder<P: Provider<Ethereum>>(
984 provider: P,
985 args: &[&str],
986 ) -> CastTxBuilder<Ethereum, P, InputState> {
987 let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
988 let opts = TransactionOpts::parse_from([&["test"], args].concat());
989 CastTxBuilder::new(provider, opts, &config)
990 .await
991 .unwrap()
992 .with_to(Some(TO.into()))
993 .await
994 .unwrap()
995 .with_code_sig_and_args(None, None, Vec::new())
996 .await
997 .unwrap()
998 }
999
1000 #[tokio::test]
1001 async fn filled_build_applies_multiplier_to_concurrent_gas_estimate() {
1002 let asserter = Asserter::new();
1003 for _ in 0..2 {
1004 asserter.push_success(&U64::from(100));
1005 }
1006 let fill_methods = Arc::new(Mutex::new(Vec::new()));
1007 let transport = BarrierTransport {
1008 inner: MockTransport::new(asserter),
1009 barrier: Arc::new(Barrier::new(2)),
1010 fill_methods: fill_methods.clone(),
1011 };
1012 let provider = ProviderBuilder::new_with_network::<Ethereum>()
1013 .connect_client(RpcClient::new(transport, true));
1014
1015 let builder = builder(&provider, &["--legacy", "--gas-price", "1"])
1016 .await
1017 .with_gas_estimate_multiplier(Some(150));
1018 let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
1019 .await
1020 .expect("nonce and gas requests were not in flight together")
1021 .unwrap();
1022
1023 assert_eq!(tx.nonce, Some(100));
1024 assert_eq!(tx.gas_price, Some(1));
1025 assert_eq!(tx.gas, Some(150));
1026 let mut fill_methods = fill_methods.lock().unwrap().clone();
1027 fill_methods.sort();
1028 assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
1029 }
1030
1031 #[tokio::test]
1032 async fn browser_submission_resolves_type_after_tempo_fee_token() {
1033 let asserter = Asserter::new();
1034 let token = Address::repeat_byte(0x42);
1035 asserter.push_success(&token.abi_encode());
1036 let provider =
1037 ProviderBuilder::new_with_network::<TempoNetwork>().connect_mocked_client(asserter);
1038 let chain = Chain::from_id(4217);
1039 let config = Config { chain: Some(chain), ..Default::default() };
1040 let opts = TransactionOpts::parse_from([
1041 "test",
1042 "--gas-limit",
1043 "21000",
1044 "--nonce",
1045 "0",
1046 "--gas-price",
1047 "1",
1048 "--priority-gas-price",
1049 "1",
1050 ]);
1051 let (mut tx, _) = CastTxBuilder::new(&provider, opts, &config)
1052 .await
1053 .unwrap()
1054 .with_browser_wallet()
1055 .with_to(Some(TO.into()))
1056 .await
1057 .unwrap()
1058 .with_code_sig_and_args(None, None, Vec::new())
1059 .await
1060 .unwrap()
1061 .build(Address::repeat_byte(0x22))
1062 .await
1063 .unwrap();
1064 assert_eq!(tx.inner.transaction_type, None);
1065 assert_eq!(tx.fee_token, None);
1066
1067 resolve_and_set_fee_token(Some(&provider), &mut tx, None).await.unwrap();
1069 tx.prep_for_submission();
1070 assert_eq!(tx.inner.transaction_type, Some(0x76));
1071 assert_eq!(tx.fee_token, Some(token));
1072 }
1073
1074 #[tokio::test]
1075 async fn raw_build_skips_nonce_request() {
1076 let provider =
1079 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1080 builder(&provider, &[]).await.raw().build(Address::repeat_byte(0x22)).await.unwrap();
1081 }
1082
1083 #[tokio::test]
1084 async fn detects_auth_rpc_disclosure() {
1085 let provider =
1086 ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1087 let auth = TO.to_string();
1088
1089 let no_auth = builder(&provider, &["--gas-limit", "21000"]).await.raw();
1090 assert!(!no_auth.has_auth());
1091 assert!(!no_auth.will_disclose_auth_during_build());
1092
1093 let rpc_call = builder(&provider, &["--auth", &auth, "--gas-limit", "21000"]).await.raw();
1094 assert!(rpc_call.has_auth());
1095 assert!(!rpc_call.will_disclose_auth_during_build());
1096
1097 let generated_access_list = builder(&provider, &["--auth", &auth, "--access-list"]).await;
1098 assert!(generated_access_list.raw().will_disclose_auth_during_build());
1099
1100 let explicit_access_list =
1101 builder(&provider, &["--auth", &auth, "--access-list", "[]"]).await;
1102 assert!(!explicit_access_list.raw().will_disclose_auth_during_build());
1103
1104 let estimated_gas = builder(&provider, &["--auth", &auth]).await;
1105 assert!(estimated_gas.will_disclose_auth_during_build());
1106 }
1107
1108 #[test]
1109 fn tx_params_parses_gas_price_units() {
1110 #[derive(Parser)]
1111 struct Cli {
1112 #[command(flatten)]
1113 tx: TxParams,
1114 }
1115
1116 let cli =
1117 Cli::try_parse_from(["test", "--gas-price", "10gwei", "--priority-gas-price", "2gwei"])
1118 .unwrap();
1119 assert_eq!(cli.tx.gas_price, Some(U256::from(10_000_000_000u64)));
1120 assert_eq!(cli.tx.priority_gas_price, Some(U256::from(2_000_000_000u64)));
1121
1122 let cli = Cli::try_parse_from(["test", "--gas-price", "100", "--priority-gas-price", "2"])
1123 .unwrap();
1124 assert_eq!(cli.tx.gas_price, Some(U256::from(100)));
1125 assert_eq!(cli.tx.priority_gas_price, Some(U256::from(2)));
1126 }
1127}