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