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, 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/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
184pub fn validate_from_address(
185    specified_from: Option<Address>,
186    signer_address: Address,
187) -> Result<()> {
188    if let Some(specified_from) = specified_from
189        && specified_from != signer_address
190    {
191        eyre::bail!(
192                "\
193The specified sender via CLI/env vars does not match the sender configured via
194the hardware wallet's HD Path.
195Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
196corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
197            );
198    }
199    Ok(())
200}
201
202/// Initial state.
203#[derive(Debug)]
204pub struct InitState;
205
206/// State with known [TxKind].
207#[derive(Debug)]
208pub struct ToState {
209    to: Option<Address>,
210}
211
212/// State with known input for the transaction.
213#[derive(Debug)]
214pub struct InputState {
215    kind: TxKind,
216    input: Vec<u8>,
217    func: Option<Function>,
218}
219
220pub struct CastTxSender<N, P> {
221    provider: P,
222    _phantom: PhantomData<N>,
223}
224
225impl<N: Network, P: Provider<N>> CastTxSender<N, P>
226where
227    N::TransactionRequest: FoundryTransactionBuilder<N>,
228    N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
229{
230    /// Creates a new Cast instance responsible for sending transactions.
231    pub const fn new(provider: P) -> Self {
232        Self { provider, _phantom: PhantomData }
233    }
234
235    /// Sends a transaction and waits for receipt synchronously
236    pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
237        let mut receipt = TransactionReceiptWithRevertReason::<N> {
238            receipt: self.provider.send_transaction_sync(tx).await?,
239            revert_reason: None,
240        };
241        let tx_hash = receipt.receipt.transaction_hash();
242        // Allow to fail silently
243        let _ = receipt.update_revert_reason(&self.provider).await;
244
245        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
246    }
247
248    /// Sends a transaction to the specified address
249    ///
250    /// # Example
251    ///
252    /// ```
253    /// use cast::tx::CastTxSender;
254    /// use alloy_primitives::{Address, U256, Bytes};
255    /// use alloy_serde::WithOtherFields;
256    /// use alloy_rpc_types::{TransactionRequest};
257    /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork};
258    /// use std::str::FromStr;
259    /// use alloy_sol_types::{sol, SolCall};    ///
260    ///
261    /// sol!(
262    ///     function greet(string greeting) public;
263    /// );
264    ///
265    /// # async fn foo() -> eyre::Result<()> {
266    /// let provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;;
267    /// let from = Address::from_str("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")?;
268    /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?;
269    /// let greeting = greetCall { greeting: "hello".to_string() }.abi_encode();
270    /// let bytes = Bytes::from_iter(greeting.iter());
271    /// let gas = U256::from_str("200000").unwrap();
272    /// let value = U256::from_str("1").unwrap();
273    /// let nonce = U256::from_str("1").unwrap();
274    /// let tx = TransactionRequest::default().to(to).input(bytes.into()).from(from);
275    /// let tx = WithOtherFields::new(tx);
276    /// let cast = CastTxSender::new(provider);
277    /// let data = cast.send(tx).await?;
278    /// println!("{:#?}", data);
279    /// # Ok(())
280    /// # }
281    /// ```
282    pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
283        let res = self.provider.send_transaction(tx).await?;
284
285        Ok(res)
286    }
287
288    /// Sends a raw RLP-encoded transaction via `eth_sendRawTransaction`.
289    ///
290    /// Used for transaction types that the standard Alloy network stack doesn't understand
291    /// (e.g., Tempo transactions).
292    pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
293        let res = self.provider.send_raw_transaction(raw_tx).await?;
294        Ok(res)
295    }
296
297    /// Sends a raw RLP-encoded transaction and waits for its receipt synchronously.
298    pub async fn send_raw_sync(&self, raw_tx: &[u8]) -> Result<(B256, String)> {
299        let mut receipt = TransactionReceiptWithRevertReason::<N> {
300            receipt: self.provider.send_raw_transaction_sync(raw_tx).await?,
301            revert_reason: None,
302        };
303        let tx_hash = receipt.receipt.transaction_hash();
304        // Allow this to fail silently.
305        let _ = receipt.update_revert_reason(&self.provider).await;
306
307        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
308    }
309
310    /// Prints the transaction hash (if async) or waits for the receipt and prints it.
311    ///
312    /// This is the shared "output" path used by both the normal send flow and the browser wallet
313    /// flow (which sends the transaction out-of-band and only has a tx hash).
314    pub async fn print_tx_result(
315        &self,
316        tx_hash: B256,
317        cast_async: bool,
318        confs: u64,
319        timeout: u64,
320    ) -> Result<()> {
321        if cast_async {
322            sh_println!("{tx_hash:#x}")?;
323        } else {
324            let receipt =
325                self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
326            sh_println!("{receipt}")?;
327        }
328        Ok(())
329    }
330
331    /// # Example
332    ///
333    /// ```
334    /// use alloy_provider::{ProviderBuilder, RootProvider, network::AnyNetwork};
335    /// use cast::tx::CastTxSender;
336    ///
337    /// async fn foo() -> eyre::Result<()> {
338    /// let provider =
339    ///     ProviderBuilder::<_, _, AnyNetwork>::default().connect("http://localhost:8545").await?;
340    /// let cast = CastTxSender::new(provider);
341    /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc";
342    /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?;
343    /// println!("{}", receipt);
344    /// # Ok(())
345    /// # }
346    /// ```
347    pub async fn receipt(
348        &self,
349        tx_hash: String,
350        field: Option<String>,
351        confs: u64,
352        timeout: Option<u64>,
353        cast_async: bool,
354    ) -> Result<String> {
355        let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
356
357        let mut receipt = TransactionReceiptWithRevertReason::<N> {
358            receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
359                Some(r) => r,
360                None => {
361                    // if the async flag is provided, immediately exit if no tx is found, otherwise
362                    // try to poll for it
363                    if cast_async {
364                        eyre::bail!("tx not found: {:?}", tx_hash);
365                    }
366                    PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
367                        .with_required_confirmations(confs)
368                        .with_timeout(timeout.map(Duration::from_secs))
369                        .get_receipt()
370                        .await?
371                }
372            },
373            revert_reason: None,
374        };
375
376        // Allow to fail silently
377        let _ = receipt.update_revert_reason(&self.provider).await;
378
379        self.format_receipt(receipt, field)
380    }
381
382    /// Helper method to format transaction receipts consistently
383    fn format_receipt(
384        &self,
385        receipt: TransactionReceiptWithRevertReason<N>,
386        field: Option<String>,
387    ) -> Result<String> {
388        Ok(if let Some(ref field) = field {
389            get_pretty_receipt_w_reason_attr(&receipt, field)
390                .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
391        } else if shell::is_json() {
392            // to_value first to sort json object keys
393            serde_json::to_value(&receipt)?.to_string()
394        } else {
395            receipt.pretty()
396        })
397    }
398}
399
400/// Builder type constructing generic TransactionRequest from cast send/mktx inputs.
401///
402/// It is implemented as a stateful builder with expected state transition of [InitState] ->
403/// [ToState] -> [InputState].
404#[derive(Debug)]
405pub struct CastTxBuilder<N: Network, P, S> {
406    provider: P,
407    pub(crate) tx: N::TransactionRequest,
408    /// Whether the transaction should be sent as a legacy transaction.
409    legacy: bool,
410    blob: bool,
411    /// Whether the blob transaction should use EIP-4844 (legacy) format instead of EIP-7594.
412    eip4844: bool,
413    /// Whether to fill gas, fees and nonce. Set to `false` for read-only calls
414    /// (eth_call, eth_estimateGas, eth_createAccessList).
415    fill: bool,
416    /// Whether the filled transaction will be submitted through a browser wallet.
417    browser: bool,
418    /// The preset used when estimating EIP-1559 fees.
419    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
420    auth: Vec<CliAuthorizationList>,
421    chain: Chain,
422    etherscan_api_key: Option<String>,
423    etherscan_api_url: Option<String>,
424    access_list: Option<Option<AccessList>>,
425    state: S,
426}
427
428impl<N: Network, P, S> CastTxBuilder<N, P, S> {
429    /// Returns the resolved chain for this builder.
430    pub const fn chain(&self) -> Chain {
431        self.chain
432    }
433
434    /// Marks this transaction as destined for browser wallet submission.
435    pub const fn with_browser_wallet(mut self) -> Self {
436        self.browser = true;
437        self
438    }
439
440    /// Returns whether this builder contains any EIP-7702 authorizations.
441    pub(crate) const fn has_auth(&self) -> bool {
442        !self.auth.is_empty()
443    }
444
445    /// Validates that the configured sender can resolve all EIP-7702 authorizations.
446    pub(crate) fn validate_auth(&self, sender: &SenderKind<'_>) -> Result<()> {
447        let address_auth_count = self
448            .auth
449            .iter()
450            .filter(|auth| matches!(auth, CliAuthorizationList::Address(_)))
451            .count();
452        if address_auth_count > 1 {
453            eyre::bail!(
454                "Multiple address-based authorizations provided. Only one address can be specified; \
455                use pre-signed authorizations (hex-encoded) for multiple authorizations."
456            );
457        }
458        if address_auth_count == 1 && sender.as_signer().is_none() {
459            eyre::bail!(
460                "No signer available to sign authorization. \
461                Provide a pre-signed authorization (hex-encoded) instead."
462            );
463        }
464
465        Ok(())
466    }
467
468    /// Returns whether building this request will disclose an EIP-7702 authorization to an RPC
469    /// endpoint.
470    pub(crate) fn will_disclose_auth_during_build(&self) -> bool {
471        self.has_auth()
472            // Generating an access list sends the authorization-bearing request to the RPC.
473            && (matches!(self.access_list, Some(None))
474                // Gas estimation also sends the authorization-bearing request to the RPC.
475                || (self.fill && self.tx.gas_limit().is_none()))
476    }
477}
478
479impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
480where
481    N::TransactionRequest: FoundryTransactionBuilder<N>,
482{
483    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
484    /// provided [TransactionOpts].
485    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
486        let mut tx = N::TransactionRequest::default();
487
488        let chain = utils::get_chain(config.chain, &provider).await?;
489        let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
490        let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
491        let etherscan_api_url = etherscan_config.map(|c| c.api_url);
492        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
493        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
494
495        // Apply gas, value, fee, and network-specific options.
496        tx_opts.apply::<N>(&mut tx, legacy);
497
498        Ok(Self {
499            provider,
500            tx,
501            legacy,
502            blob: tx_opts.blob,
503            eip4844: tx_opts.eip4844,
504            fill: true,
505            browser: false,
506            eip1559_fee_estimate: config.eip1559_fee_estimate,
507            chain,
508            etherscan_api_key,
509            etherscan_api_url,
510            auth: tx_opts.auth,
511            access_list: tx_opts.access_list,
512            state: InitState,
513        })
514    }
515
516    /// Sets [TxKind] for this builder and changes state to [ToState].
517    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
518        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
519        Ok(CastTxBuilder {
520            provider: self.provider,
521            tx: self.tx,
522            legacy: self.legacy,
523            blob: self.blob,
524            eip4844: self.eip4844,
525            fill: self.fill,
526            browser: self.browser,
527            eip1559_fee_estimate: self.eip1559_fee_estimate,
528            chain: self.chain,
529            etherscan_api_key: self.etherscan_api_key,
530            etherscan_api_url: self.etherscan_api_url,
531            auth: self.auth,
532            access_list: self.access_list,
533            state: ToState { to },
534        })
535    }
536}
537
538impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
539where
540    N::TransactionRequest: FoundryTransactionBuilder<N>,
541{
542    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
543    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
544    /// present, input is set to just provided arguments.
545    pub async fn with_code_sig_and_args(
546        self,
547        code: Option<String>,
548        sig: Option<String>,
549        args: Vec<String>,
550    ) -> Result<CastTxBuilder<N, P, InputState>> {
551        let (mut args, func) = if let Some(sig) = sig {
552            parse_function_args(
553                &sig,
554                args,
555                self.state.to,
556                self.chain,
557                &self.provider,
558                self.etherscan_api_key.as_deref(),
559                self.etherscan_api_url.as_deref(),
560            )
561            .await?
562        } else {
563            (Vec::new(), None)
564        };
565
566        let input = if let Some(code) = &code {
567            let mut code = hex::decode(code)?;
568            code.append(&mut args);
569            code
570        } else {
571            args
572        };
573
574        if self.state.to.is_none() && code.is_none() {
575            let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
576            let has_auth = !self.auth.is_empty();
577            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
578            // without a value.
579            if !has_auth || has_value {
580                eyre::bail!("Must specify a recipient address or contract code to deploy");
581            }
582        }
583
584        Ok(CastTxBuilder {
585            provider: self.provider,
586            tx: self.tx,
587            legacy: self.legacy,
588            blob: self.blob,
589            eip4844: self.eip4844,
590            fill: self.fill,
591            browser: self.browser,
592            eip1559_fee_estimate: self.eip1559_fee_estimate,
593            chain: self.chain,
594            etherscan_api_key: self.etherscan_api_key,
595            etherscan_api_url: self.etherscan_api_url,
596            auth: self.auth,
597            access_list: self.access_list,
598            state: InputState { kind: self.state.to.into(), input, func },
599        })
600    }
601}
602
603impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
604where
605    N::TransactionRequest: FoundryTransactionBuilder<N>,
606{
607    /// Builds the TransactionRequest. Fills gas, fees and nonce unless [`raw`](Self::raw) was
608    /// called.
609    pub async fn build(
610        self,
611        sender: impl Into<SenderKind<'_>>,
612    ) -> Result<(N::TransactionRequest, Option<Function>)> {
613        let fill = self.fill;
614        self._build(sender, fill, None).await
615    }
616
617    /// Builds a transaction that will be signed by a Tempo access key.
618    ///
619    /// The access-key id is set before gas estimation. If the access key needs on-chain
620    /// provisioning, its authorization is embedded before access-list/gas estimation and before
621    /// any sponsor digest can be computed.
622    pub async fn build_with_tempo_wallet(
623        self,
624        wallet: &TempoAccountsWallet,
625    ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
626        let fill = self.fill;
627        let mut prepared = wallet.clone();
628        let (tx, func) = self._build(wallet.account(), fill, Some(&mut prepared)).await?;
629        Ok((tx, func, prepared))
630    }
631
632    async fn _build(
633        mut self,
634        sender: impl Into<SenderKind<'_>>,
635        fill: bool,
636        tempo_wallet: Option<&mut TempoAccountsWallet>,
637    ) -> Result<(N::TransactionRequest, Option<Function>)> {
638        // prepare
639        let sender = sender.into();
640        self.prepare(&sender);
641
642        // For batch transactions with calls, clear `to` and `value` so the node correctly
643        // identifies this as an AA batch transaction. The `calls` field determines the actual
644        // targets. If `to` is set, `build_aa()` would add a spurious extra call.
645        self.tx.clear_batch_to();
646
647        // resolve
648        // Read-only calls do not need a nonce unless it is required to sign an authorization.
649        // Avoid an otherwise unused `eth_getTransactionCount` request for raw transactions.
650        let resolve_in_parallel =
651            fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
652        let tx_nonce = if resolve_in_parallel {
653            let nonce = self.tx.nonce();
654            let fees_are_complete = if self.legacy {
655                self.tx.gas_price().is_some()
656            } else {
657                matches!(
658                    (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
659                    (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
660                )
661            } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
662            let gas_request =
663                (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
664                    .then(|| self.tx.clone());
665            let (tx_nonce, (), gas_limit) = tokio::try_join!(
666                Self::resolve_nonce(&self.provider, sender.address(), nonce),
667                Self::fill_fees(
668                    &self.provider,
669                    &mut self.tx,
670                    self.blob,
671                    self.legacy,
672                    self.browser,
673                    self.eip1559_fee_estimate,
674                ),
675                async {
676                    match gas_request {
677                        Some(request) => {
678                            Self::estimate_gas(&self.provider, request).await.map(Some)
679                        }
680                        None => Ok(None),
681                    }
682                },
683            )?;
684            if let Some(gas_limit) = gas_limit {
685                self.tx.set_gas_limit(gas_limit);
686            }
687            Some(tx_nonce)
688        } else if fill || !self.auth.is_empty() {
689            Some(Self::resolve_nonce(&self.provider, sender.address(), self.tx.nonce()).await?)
690        } else {
691            None
692        };
693        if let Some(tx_nonce) = tx_nonce {
694            if fill {
695                self.tx.set_nonce(tx_nonce);
696            }
697            self.resolve_auth(&sender, tx_nonce).await?;
698        }
699        if let Some(wallet) = tempo_wallet {
700            *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
701        }
702        if fill && !resolve_in_parallel {
703            Self::fill_fees(
704                &self.provider,
705                &mut self.tx,
706                self.blob,
707                self.legacy,
708                self.browser,
709                self.eip1559_fee_estimate,
710            )
711            .await?;
712        }
713        self.resolve_access_list().await?;
714        if fill {
715            self.fill_gas_limit().await?;
716        }
717
718        Ok((self.tx, self.state.func))
719    }
720
721    /// Sets the core transaction fields from the builder state: kind, input, optional from, and
722    /// chain id.
723    fn prepare(&mut self, sender: &SenderKind<'_>) {
724        self.tx.set_kind(self.state.kind);
725        // We set both fields to the same value because some nodes only accept the legacy
726        // `data` field: https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249
727        self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
728        let sender = sender.address();
729        if !sender.is_zero() {
730            self.tx.set_from(sender);
731        }
732        self.tx.set_chain_id(self.chain.id());
733    }
734
735    /// Resolves the transaction nonce. Returns the existing nonce or fetches one from the provider.
736    async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
737        if let Some(nonce) = nonce {
738            Ok(nonce)
739        } else {
740            Ok(provider.get_transaction_count(from).await?)
741        }
742    }
743
744    /// Resolves the access list. Fetches from the provider if `--access-list` was passed without
745    /// a value.
746    async fn resolve_access_list(&mut self) -> Result<()> {
747        if let Some(access_list) = match self.access_list.take() {
748            None => None,
749            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
750            Some(Some(access_list)) => Some(access_list),
751        } {
752            self.tx.set_access_list(access_list);
753        }
754        Ok(())
755    }
756
757    /// Parses the passed --auth values and sets the authorization list on the transaction.
758    ///
759    /// If a signer is available in `sender`, address-based auths will be signed.
760    /// If no signer is available, all auths must be pre-signed.
761    async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
762        if self.auth.is_empty() {
763            return Ok(());
764        }
765
766        self.validate_auth(sender)?;
767        let auths = std::mem::take(&mut self.auth);
768
769        let mut signed_auths = Vec::with_capacity(auths.len());
770
771        for auth in auths {
772            let signed_auth = match auth {
773                CliAuthorizationList::Address(address) => {
774                    let auth = Authorization {
775                        chain_id: U256::from(self.chain.id()),
776                        nonce: tx_nonce + 1,
777                        address,
778                    };
779
780                    let signer =
781                        sender.as_signer().expect("address-based authorization requires a signer");
782                    let signature = signer.sign_hash(&auth.signature_hash()).await?;
783
784                    auth.into_signed(signature)
785                }
786                CliAuthorizationList::Signed(auth) => auth,
787            };
788            signed_auths.push(signed_auth);
789        }
790
791        self.tx.set_authorization_list(signed_auths);
792
793        Ok(())
794    }
795
796    /// Fills gas price, EIP-1559 fees, and blob fees from the provider.
797    ///
798    /// Only fills values that haven't been explicitly set by the user.
799    async fn fill_fees(
800        provider: &P,
801        tx: &mut N::TransactionRequest,
802        blob: bool,
803        legacy: bool,
804        browser: bool,
805        eip1559_fee_estimate: Eip1559FeeEstimatePreset,
806    ) -> Result<()> {
807        if blob && tx.max_fee_per_blob_gas().is_none() {
808            tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
809        }
810
811        fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
812    }
813
814    /// Fills gas limit from the provider.
815    async fn fill_gas_limit(&mut self) -> Result<()> {
816        if self.tx.gas_limit().is_none() {
817            let request = if self.browser && self.chain.is_tempo() {
818                self.tx.browser_wallet_gas_estimation_request()
819            } else {
820                self.tx.clone()
821            };
822            let estimated = Self::estimate_gas(&self.provider, request).await?;
823            self.tx.set_gas_limit(estimated);
824        }
825
826        Ok(())
827    }
828
829    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
830    async fn estimate_gas(provider: &P, request: N::TransactionRequest) -> Result<u64> {
831        match provider.estimate_gas(request).await {
832            Ok(estimated) => Ok(estimated),
833            Err(err) => {
834                if let TransportError::ErrorResp(payload) = &err {
835                    // If execution reverted with code 3 during provider gas estimation then try
836                    // to decode custom errors and append it to the error message.
837                    if payload.code == 3
838                        && let Some(data) = &payload.data
839                        && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
840                    {
841                        eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error);
842                    }
843                }
844                eyre::bail!("Failed to estimate gas: {}", err);
845            }
846        }
847    }
848
849    /// Populates the blob sidecar for the transaction if any blob data was provided.
850    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
851        let Some(blob_data) = blob_data else { return Ok(self) };
852
853        let mut coder = SidecarBuilder::<SimpleCoder>::default();
854        coder.ingest(&blob_data);
855
856        if self.eip4844 {
857            let sidecar = coder.build_4844()?;
858            self.tx.set_blob_sidecar_4844(sidecar);
859        } else {
860            let sidecar = coder.build_7594()?;
861            self.tx.set_blob_sidecar_7594(sidecar);
862        }
863
864        Ok(self)
865    }
866
867    /// Skips gas, fee and nonce filling. Use for read-only calls
868    /// (eth_call, eth_estimateGas, eth_createAccessList).
869    pub const fn raw(mut self) -> Self {
870        self.fill = false;
871        self
872    }
873}
874
875/// Fills gas price or EIP-1559 fee fields from the provider and validates the final pair.
876pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
877    provider: &P,
878    tx: &mut N::TransactionRequest,
879    legacy: bool,
880    browser: bool,
881    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
882) -> Result<()>
883where
884    N::TransactionRequest: FoundryTransactionBuilder<N>,
885{
886    if legacy {
887        if tx.gas_price().is_none() {
888            tx.set_gas_price(provider.get_gas_price().await?);
889        }
890        return Ok(());
891    }
892
893    if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
894        let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
895
896        // Only honor the browser-suggested tip when the user has not pinned a
897        // priority fee; `resolve_broadcast_eip1559_fees` ignores a lower tip.
898        let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
899            provider.get_max_priority_fee_per_gas().await.ok()
900        } else {
901            None
902        };
903
904        // User `--gas-price`/`--priority-gas-price` overrides are already applied
905        // to `tx`; pass `None` so they are not double-applied here.
906        let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
907
908        if tx.max_fee_per_gas().is_none() {
909            tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
910        }
911
912        if tx.max_priority_fee_per_gas().is_none() {
913            tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
914        }
915    }
916
917    if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
918        eyre::ensure!(
919            priority <= max_fee,
920            "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
921        );
922    }
923
924    Ok(())
925}
926
927/// Helper function that tries to decode custom error name and inputs from error payload data.
928async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
929    let err_data = serde_json::from_str::<Bytes>(data.get())?;
930    let Some(selector) = err_data.get(..4) else { return Ok(None) };
931    if let Some(known_error) =
932        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
933    {
934        let mut decoded_error = known_error.name.clone();
935        if !known_error.inputs.is_empty()
936            && let Ok(error) = known_error.decode_error(&err_data)
937        {
938            write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
939        }
940        return Ok(Some(decoded_error));
941    }
942    Ok(None)
943}
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948    use alloy_json_rpc::{RequestPacket, ResponsePacket};
949    use alloy_network::Ethereum;
950    use alloy_provider::{ProviderBuilder, mock::Asserter};
951    use alloy_rpc_client::RpcClient;
952    use alloy_transport::{TransportFut, mock::MockTransport};
953    use clap::Parser;
954    use std::{
955        sync::{Arc, Mutex},
956        task::{Context, Poll},
957    };
958    use tokio::{sync::Barrier, time::timeout};
959    use tower::Service;
960
961    #[derive(Clone)]
962    struct BarrierTransport {
963        inner: MockTransport,
964        barrier: Arc<Barrier>,
965        fill_methods: Arc<Mutex<Vec<String>>>,
966    }
967
968    impl Service<RequestPacket> for BarrierTransport {
969        type Response = ResponsePacket;
970        type Error = TransportError;
971        type Future = TransportFut<'static>;
972
973        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
974            self.inner.poll_ready(cx)
975        }
976
977        fn call(&mut self, req: RequestPacket) -> Self::Future {
978            let fill_method = match &req {
979                RequestPacket::Single(req)
980                    if matches!(
981                        req.method(),
982                        "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
983                    ) =>
984                {
985                    Some(req.method().to_string())
986                }
987                _ => None,
988            };
989            let Some(fill_method) = fill_method else {
990                return self.inner.call(req);
991            };
992            self.fill_methods.lock().unwrap().push(fill_method);
993
994            let barrier = self.barrier.clone();
995            let mut inner = self.inner.clone();
996            Box::pin(async move {
997                barrier.wait().await;
998                inner.call(req).await
999            })
1000        }
1001    }
1002
1003    #[tokio::test]
1004    async fn filled_build_fetches_nonce_and_gas_concurrently_with_explicit_fees() {
1005        let asserter = Asserter::new();
1006        for _ in 0..2 {
1007            asserter.push_success(&U64::from(1));
1008        }
1009        let fill_methods = Arc::new(Mutex::new(Vec::new()));
1010        let transport = BarrierTransport {
1011            inner: MockTransport::new(asserter),
1012            barrier: Arc::new(Barrier::new(2)),
1013            fill_methods: fill_methods.clone(),
1014        };
1015        let provider = ProviderBuilder::new_with_network::<Ethereum>()
1016            .connect_client(RpcClient::new(transport, true));
1017        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1018
1019        let builder = CastTxBuilder::new(
1020            &provider,
1021            TransactionOpts::parse_from(["test", "--legacy", "--gas-price", "1"]),
1022            &config,
1023        )
1024        .await
1025        .unwrap()
1026        .with_to(Some(Address::repeat_byte(0x11).into()))
1027        .await
1028        .unwrap()
1029        .with_code_sig_and_args(None, None, Vec::new())
1030        .await
1031        .unwrap();
1032        let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
1033            .await
1034            .expect("nonce and gas requests were not in flight together")
1035            .unwrap();
1036
1037        assert_eq!(tx.nonce, Some(1));
1038        assert_eq!(tx.gas_price, Some(1));
1039        assert_eq!(tx.gas, Some(1));
1040        let mut fill_methods = fill_methods.lock().unwrap().clone();
1041        fill_methods.sort();
1042        assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
1043    }
1044
1045    #[tokio::test]
1046    async fn raw_build_skips_nonce_request() {
1047        // No responses are queued, so any RPC request would fail this test. In particular, this
1048        // guards against restoring the unused `eth_getTransactionCount` request.
1049        let provider =
1050            ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1051        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1052
1053        CastTxBuilder::new(&provider, TransactionOpts::parse_from(["test"]), &config)
1054            .await
1055            .unwrap()
1056            .with_to(Some(Address::repeat_byte(0x11).into()))
1057            .await
1058            .unwrap()
1059            .with_code_sig_and_args(None, None, Vec::new())
1060            .await
1061            .unwrap()
1062            .raw()
1063            .build(Address::repeat_byte(0x22))
1064            .await
1065            .unwrap();
1066    }
1067
1068    #[tokio::test]
1069    async fn detects_auth_rpc_disclosure() {
1070        let provider =
1071            ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1072        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
1073        let address = Address::repeat_byte(0x11);
1074
1075        let no_auth = CastTxBuilder::new(
1076            &provider,
1077            TransactionOpts::parse_from(["test", "--gas-limit", "21000"]),
1078            &config,
1079        )
1080        .await
1081        .unwrap()
1082        .with_to(Some(address.into()))
1083        .await
1084        .unwrap()
1085        .with_code_sig_and_args(None, None, Vec::new())
1086        .await
1087        .unwrap()
1088        .raw();
1089        assert!(!no_auth.has_auth());
1090        assert!(!no_auth.will_disclose_auth_during_build());
1091
1092        let rpc_call = CastTxBuilder::new(
1093            &provider,
1094            TransactionOpts::parse_from([
1095                "test",
1096                "--auth",
1097                &address.to_string(),
1098                "--gas-limit",
1099                "21000",
1100            ]),
1101            &config,
1102        )
1103        .await
1104        .unwrap()
1105        .with_to(Some(address.into()))
1106        .await
1107        .unwrap()
1108        .with_code_sig_and_args(None, None, Vec::new())
1109        .await
1110        .unwrap()
1111        .raw();
1112        assert!(rpc_call.has_auth());
1113        assert!(!rpc_call.will_disclose_auth_during_build());
1114
1115        let generated_access_list = CastTxBuilder::new(
1116            &provider,
1117            TransactionOpts::parse_from(["test", "--auth", &address.to_string(), "--access-list"]),
1118            &config,
1119        )
1120        .await
1121        .unwrap()
1122        .with_to(Some(address.into()))
1123        .await
1124        .unwrap()
1125        .with_code_sig_and_args(None, None, Vec::new())
1126        .await
1127        .unwrap()
1128        .raw();
1129        assert!(generated_access_list.will_disclose_auth_during_build());
1130
1131        let explicit_access_list = CastTxBuilder::new(
1132            &provider,
1133            TransactionOpts::parse_from([
1134                "test",
1135                "--auth",
1136                &address.to_string(),
1137                "--access-list",
1138                "[]",
1139            ]),
1140            &config,
1141        )
1142        .await
1143        .unwrap()
1144        .with_to(Some(address.into()))
1145        .await
1146        .unwrap()
1147        .with_code_sig_and_args(None, None, Vec::new())
1148        .await
1149        .unwrap()
1150        .raw();
1151        assert!(!explicit_access_list.will_disclose_auth_during_build());
1152
1153        let estimated_gas = CastTxBuilder::new(
1154            &provider,
1155            TransactionOpts::parse_from(["test", "--auth", &address.to_string()]),
1156            &config,
1157        )
1158        .await
1159        .unwrap();
1160        assert!(estimated_gas.will_disclose_auth_during_build());
1161    }
1162}