Skip to main content

cast/
tx.rs

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    /// Only print the transaction hash and exit immediately.
34    #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
35    pub cast_async: bool,
36
37    /// Wait for transaction receipt synchronously instead of polling.
38    /// Note: uses `eth_sendTransactionSync` or `eth_sendRawTransactionSync`, which may not be
39    /// supported by all clients.
40    #[arg(long, conflicts_with = "async")]
41    pub sync: bool,
42
43    /// The number of confirmations until the receipt is fetched.
44    #[arg(long, default_value = "1")]
45    pub confirmations: u64,
46
47    /// Timeout for sending the transaction.
48    #[arg(long, env = "ETH_TIMEOUT")]
49    pub timeout: Option<u64>,
50
51    /// Polling interval for transaction receipts (in seconds).
52    #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
53    pub poll_interval: Option<u64>,
54
55    /// Ethereum options
56    #[command(flatten)]
57    pub eth: EthereumOpts,
58
59    /// Browser wallet options
60    #[command(flatten)]
61    pub browser: BrowserWalletOpts,
62}
63
64/// Transaction options shared across cast commands that submit on-chain transactions.
65#[derive(Debug, Clone, Args)]
66#[command(next_help_heading = "Transaction options")]
67pub struct TxParams {
68    /// Gas limit for the transaction.
69    #[arg(long, env = "ETH_GAS_LIMIT")]
70    pub gas_limit: Option<U256>,
71
72    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.
73    #[arg(long, env = "ETH_GAS_PRICE")]
74    pub gas_price: Option<U256>,
75
76    /// Max priority fee per gas for EIP1559 transactions.
77    #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
78    pub priority_gas_price: Option<U256>,
79
80    /// Nonce for the transaction.
81    #[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
113/// Different sender kinds used by [`CastTxBuilder`].
114pub enum SenderKind<'a> {
115    /// An address without signer. Used for read-only calls and transactions sent through unlocked
116    /// accounts.
117    Address(Address),
118    /// A reference to a signer.
119    Signer(&'a WalletSigner),
120    /// An owned signer.
121    OwnedSigner(Box<WalletSigner>),
122}
123
124impl SenderKind<'_> {
125    /// Resolves the name to an Ethereum Address.
126    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    /// Resolves the sender from the wallet options.
135    ///
136    /// This function prefers the `from` field and may return a different address from the
137    /// configured signer
138    /// If from is specified, returns it
139    /// If from is not specified, but there is a signer configured, returns the signer's address
140    /// If from is not specified and there is no signer configured, returns zero address
141    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    /// Returns the signer if available.
156    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
183/// Validates that `sender` can resolve every EIP-7702 authorization.
184pub(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
208/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
209pub 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/// Initial state.
228#[derive(Debug)]
229pub struct InitState;
230
231/// State with known [TxKind].
232#[derive(Debug)]
233pub struct ToState {
234    to: Option<Address>,
235}
236
237/// State with known input for the transaction.
238#[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    /// Creates a new Cast instance responsible for sending transactions.
256    pub const fn new(provider: P) -> Self {
257        Self { provider, _phantom: PhantomData }
258    }
259
260    /// Sends a transaction and waits for receipt synchronously
261    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        // Allow to fail silently
268        let _ = receipt.update_revert_reason(&self.provider).await;
269
270        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
271    }
272
273    /// Sends a transaction to the specified address
274    ///
275    /// # Example
276    ///
277    /// ```
278    /// use cast::tx::CastTxSender;
279    /// use alloy_primitives::{Address, U256, Bytes};
280    /// use alloy_serde::WithOtherFields;
281    /// use alloy_rpc_types::{TransactionRequest};
282    /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork};
283    /// use std::str::FromStr;
284    /// use alloy_sol_types::{sol, SolCall};    ///
285    ///
286    /// sol!(
287    ///     function greet(string greeting) public;
288    /// );
289    ///
290    /// # async fn foo() -> eyre::Result<()> {
291    /// let provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;;
292    /// let from = Address::from_str("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")?;
293    /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?;
294    /// let greeting = greetCall { greeting: "hello".to_string() }.abi_encode();
295    /// let bytes = Bytes::from_iter(greeting.iter());
296    /// let gas = U256::from_str("200000").unwrap();
297    /// let value = U256::from_str("1").unwrap();
298    /// let nonce = U256::from_str("1").unwrap();
299    /// let tx = TransactionRequest::default().to(to).input(bytes.into()).from(from);
300    /// let tx = WithOtherFields::new(tx);
301    /// let cast = CastTxSender::new(provider);
302    /// let data = cast.send(tx).await?;
303    /// println!("{:#?}", data);
304    /// # Ok(())
305    /// # }
306    /// ```
307    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    /// Sends a raw RLP-encoded transaction via `eth_sendRawTransaction`.
314    ///
315    /// Used for transaction types that the standard Alloy network stack doesn't understand
316    /// (e.g., Tempo transactions).
317    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    /// Sends a raw RLP-encoded transaction and waits for its receipt synchronously.
323    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        // Allow this to fail silently.
330        let _ = receipt.update_revert_reason(&self.provider).await;
331
332        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
333    }
334
335    /// Prints the transaction hash (if async) or waits for the receipt and prints it.
336    ///
337    /// This is the shared "output" path used by both the normal send flow and the browser wallet
338    /// flow (which sends the transaction out-of-band and only has a tx hash).
339    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    /// # Example
357    ///
358    /// ```
359    /// use alloy_provider::{ProviderBuilder, RootProvider, network::AnyNetwork};
360    /// use cast::tx::CastTxSender;
361    ///
362    /// async fn foo() -> eyre::Result<()> {
363    /// let provider =
364    ///     ProviderBuilder::<_, _, AnyNetwork>::default().connect("http://localhost:8545").await?;
365    /// let cast = CastTxSender::new(provider);
366    /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc";
367    /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?;
368    /// println!("{}", receipt);
369    /// # Ok(())
370    /// # }
371    /// ```
372    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 the async flag is provided, immediately exit if no tx is found, otherwise
387                    // try to poll for it
388                    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        // Allow to fail silently
402        let _ = receipt.update_revert_reason(&self.provider).await;
403
404        self.format_receipt(receipt, field)
405    }
406
407    /// Helper method to format transaction receipts consistently
408    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            // to_value first to sort json object keys
418            serde_json::to_value(&receipt)?.to_string()
419        } else {
420            receipt.pretty()
421        })
422    }
423}
424
425/// Builder type constructing generic TransactionRequest from cast send/mktx inputs.
426///
427/// It is implemented as a stateful builder with expected state transition of [InitState] ->
428/// [ToState] -> [InputState].
429#[derive(Debug)]
430pub struct CastTxBuilder<N: Network, P, S> {
431    provider: P,
432    pub(crate) tx: N::TransactionRequest,
433    /// Whether the transaction should be sent as a legacy transaction.
434    legacy: bool,
435    blob: bool,
436    /// Whether the blob transaction should use EIP-4844 (legacy) format instead of EIP-7594.
437    eip4844: bool,
438    /// Whether to fill gas, fees and nonce. Set to `false` for read-only calls
439    /// (eth_call, eth_estimateGas, eth_createAccessList).
440    fill: bool,
441    /// Whether the filled transaction will be submitted through a browser wallet.
442    browser: bool,
443    /// The preset used when estimating EIP-1559 fees.
444    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
445    /// Optional percentage applied to provider gas estimates.
446    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    /// Returns the resolved chain for this builder.
457    pub const fn chain(&self) -> Chain {
458        self.chain
459    }
460
461    /// Marks this transaction as destined for browser wallet submission.
462    pub const fn with_browser_wallet(mut self) -> Self {
463        self.browser = true;
464        self
465    }
466
467    /// Applies a percentage multiplier to provider gas estimates.
468    pub const fn with_gas_estimate_multiplier(mut self, multiplier: Option<u64>) -> Self {
469        self.gas_estimate_multiplier = multiplier;
470        self
471    }
472
473    /// Returns whether this builder contains any EIP-7702 authorizations.
474    pub(crate) const fn has_auth(&self) -> bool {
475        !self.auth.is_empty()
476    }
477
478    /// Validates that the configured sender can resolve all EIP-7702 authorizations.
479    pub(crate) fn validate_auth(&self, sender: &SenderKind<'_>) -> Result<()> {
480        validate_authorizations(&self.auth, sender)
481    }
482
483    /// Returns whether building this request will disclose an EIP-7702 authorization to an RPC
484    /// endpoint.
485    pub(crate) fn will_disclose_auth_during_build(&self) -> bool {
486        self.has_auth()
487            // Generating an access list sends the authorization-bearing request to the RPC.
488            && (matches!(self.access_list, Some(None))
489                // Gas estimation also sends the authorization-bearing request to the RPC.
490                || (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    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
499    /// provided [TransactionOpts].
500    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        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
508        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
509
510        // Apply gas, value, fee, and network-specific options.
511        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    /// Sets [TxKind] for this builder and changes state to [ToState].
533    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    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
560    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
561    /// present, input is set to just provided arguments.
562    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            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
595            // without a value.
596            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    /// Builds the TransactionRequest. Fills gas, fees and nonce unless [`raw`](Self::raw) was
626    /// called.
627    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    /// Builds a transaction that will be signed by a Tempo access key.
635    ///
636    /// The access-key id is set before gas estimation. If the access key needs on-chain
637    /// provisioning, its authorization is embedded before access-list/gas estimation and before
638    /// any sponsor digest can be computed.
639    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        // prepare
656        let sender = sender.into();
657        self.prepare(&sender);
658
659        // For batch transactions with calls, clear `to` and `value` so the node correctly
660        // identifies this as an AA batch transaction. The `calls` field determines the actual
661        // targets. If `to` is set, `build_aa()` would add a spurious extra call.
662        self.tx.clear_batch_to();
663
664        // resolve
665        // Read-only calls do not need a nonce unless it is required to sign an authorization.
666        // Avoid an otherwise unused `eth_getTransactionCount` request for raw transactions.
667        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    /// Sets the core transaction fields from the builder state: kind, input, optional from, and
743    /// chain id.
744    fn prepare(&mut self, sender: &SenderKind<'_>) {
745        self.tx.set_kind(self.state.kind);
746        // We set both fields to the same value because some nodes only accept the legacy
747        // `data` field: https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249
748        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    /// Resolves the transaction nonce. Returns the existing nonce or fetches one from the provider.
757    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    /// Resolves the access list. Fetches from the provider if `--access-list` was passed without
766    /// a value.
767    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    /// Parses the passed --auth values and sets the authorization list on the transaction.
779    ///
780    /// If a signer is available in `sender`, address-based auths will be signed.
781    /// If no signer is available, all auths must be pre-signed.
782    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    /// Fills gas price, EIP-1559 fees, and blob fees from the provider.
818    ///
819    /// Only fills values that haven't been explicitly set by the user.
820    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    /// Fills gas limit from the provider.
836    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    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
852    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 execution reverted with code 3 during provider gas estimation then try
862                    // to decode custom errors and append it to the error message.
863                    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    /// Populates the blob sidecar for the transaction if any blob data was provided.
876    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    /// Skips gas, fee and nonce filling. Use for read-only calls
894    /// (eth_call, eth_estimateGas, eth_createAccessList).
895    pub const fn raw(mut self) -> Self {
896        self.fill = false;
897        self
898    }
899}
900
901/// Fills gas price or EIP-1559 fee fields from the provider and validates the final pair.
902pub(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        // Only honor the browser-suggested tip when the user has not pinned a
923        // priority fee; `resolve_broadcast_eip1559_fees` ignores a lower tip.
924        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        // User `--gas-price`/`--priority-gas-price` overrides are already applied
931        // to `tx`; pass `None` so they are not double-applied here.
932        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
953/// Helper function that tries to decode custom error name and inputs from error payload data.
954async 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        // No responses are queued, so any RPC request would fail this test. In particular, this
1079        // guards against restoring the unused `eth_getTransactionCount` request.
1080        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}