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_ether_value, parse_function_args},
17};
18use foundry_common::{
19    FoundryTransactionBuilder, TransactionReceiptWithRevertReason,
20    fmt::*,
21    get_pretty_receipt_w_reason_attr,
22    provider::fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
23    shell,
24    tempo::ensure_expiring_nonce_discriminator_active,
25};
26use foundry_config::{Chain, Config, Eip1559FeeEstimatePreset};
27use foundry_wallets::{BrowserWalletOpts, TempoAccountsWallet, WalletOpts, WalletSigner};
28use itertools::Itertools;
29use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration};
30
31#[derive(Debug, Clone, Args)]
32pub struct SendTxOpts {
33    /// 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/// Applies the `--poll-interval` (in seconds) to a provider's receipt polling when one was given.
65pub(crate) fn apply_poll_interval<N: Network>(provider: &impl Provider<N>, interval: Option<u64>) {
66    if let Some(interval) = interval {
67        provider.client().set_poll_interval(Duration::from_secs(interval));
68    }
69}
70
71/// Transaction options shared across cast commands that submit on-chain transactions.
72#[derive(Debug, Clone, Args)]
73#[command(next_help_heading = "Transaction options")]
74pub struct TxParams {
75    /// Gas limit for the transaction.
76    #[arg(long, env = "ETH_GAS_LIMIT")]
77    pub gas_limit: Option<U256>,
78
79    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either
80    /// specified in wei, or as a string with a unit type.
81    ///
82    /// Examples: 1ether, 10gwei, 0.01ether
83    #[arg(
84        long,
85        env = "ETH_GAS_PRICE",
86        value_parser = parse_ether_value,
87        value_name = "PRICE"
88    )]
89    pub gas_price: Option<U256>,
90
91    /// Max priority fee per gas for EIP1559 transactions.
92    #[arg(
93        long,
94        env = "ETH_PRIORITY_GAS_PRICE",
95        value_parser = parse_ether_value,
96        value_name = "PRICE"
97    )]
98    pub priority_gas_price: Option<U256>,
99
100    /// Nonce for the transaction.
101    #[arg(long)]
102    pub nonce: Option<U64>,
103
104    #[command(flatten)]
105    pub tempo: TempoOpts,
106}
107
108impl TxParams {
109    pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
110    where
111        N::TransactionRequest: FoundryTransactionBuilder<N>,
112    {
113        self.clone().into_transaction_opts().apply::<N>(tx, legacy);
114    }
115
116    /// Converts the shared compact transaction options into the full `cast send` options.
117    pub(crate) fn into_transaction_opts(self) -> TransactionOpts {
118        TransactionOpts {
119            gas_limit: self.gas_limit,
120            gas_price: self.gas_price,
121            priority_gas_price: self.priority_gas_price,
122            value: None,
123            nonce: self.nonce,
124            legacy: false,
125            blob: false,
126            eip4844: false,
127            blob_gas_price: None,
128            auth: Vec::new(),
129            access_list: None,
130            tempo: self.tempo,
131        }
132    }
133}
134
135/// Different sender kinds used by [`CastTxBuilder`].
136pub enum SenderKind<'a> {
137    /// An address without signer. Used for read-only calls and transactions sent through unlocked
138    /// accounts.
139    Address(Address),
140    /// A reference to a signer.
141    Signer(&'a WalletSigner),
142    /// An owned signer.
143    OwnedSigner(Box<WalletSigner>),
144}
145
146impl SenderKind<'_> {
147    /// Resolves the name to an Ethereum Address.
148    pub fn address(&self) -> Address {
149        match self {
150            Self::Address(addr) => *addr,
151            Self::Signer(signer) => signer.address(),
152            Self::OwnedSigner(signer) => signer.address(),
153        }
154    }
155
156    /// Resolves the sender from the wallet options.
157    ///
158    /// Returns `from` when set, unless a Tempo access key is set or an address-based authorization
159    /// needs a signer. Otherwise returns the configured signer (or Tempo wallet account), falling
160    /// back to the zero address.
161    pub async fn from_wallet_opts(
162        mut opts: WalletOpts,
163        authorizations: &[CliAuthorizationList],
164    ) -> Result<Self> {
165        let from = opts.from.take();
166        if let Some(from) = from
167            && opts.tempo_access_key.is_none()
168            && !authorizations.iter().any(|auth| matches!(auth, CliAuthorizationList::Address(_)))
169        {
170            return Ok(from.into());
171        }
172        let (signer, tempo_wallet) = opts.maybe_signer().await?;
173        Ok(if let Some(signer) = signer {
174            signer.into()
175        } else if let Some(tempo_wallet) = tempo_wallet {
176            tempo_wallet.account().into()
177        } else {
178            from.unwrap_or_default().into()
179        })
180    }
181
182    /// Returns the signer if available.
183    pub fn as_signer(&self) -> Option<&WalletSigner> {
184        match self {
185            Self::Signer(signer) => Some(signer),
186            Self::OwnedSigner(signer) => Some(signer.as_ref()),
187            Self::Address(_) => None,
188        }
189    }
190}
191
192impl From<Address> for SenderKind<'_> {
193    fn from(addr: Address) -> Self {
194        Self::Address(addr)
195    }
196}
197
198impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
199    fn from(signer: &'a WalletSigner) -> Self {
200        Self::Signer(signer)
201    }
202}
203
204impl From<WalletSigner> for SenderKind<'_> {
205    fn from(signer: WalletSigner) -> Self {
206        Self::OwnedSigner(Box::new(signer))
207    }
208}
209
210/// Resolves the sender of a read-only request: the browser wallet address when `--browser` is
211/// set, otherwise the wallet options. Also returns whether the browser wallet was used.
212pub(crate) async fn read_only_sender<N: Network>(
213    browser: &BrowserWalletOpts,
214    wallet: WalletOpts,
215    tx: &TransactionOpts,
216    chain_id: u64,
217) -> Result<(SenderKind<'static>, bool)> {
218    crate::tempo::ensure_session_not_browser(&tx.tempo, browser.browser)?;
219    if let Some(session) = tx.tempo.session_signer_for_wallet(&wallet, chain_id)? {
220        return Ok((session.access_key.account().into(), false));
221    }
222
223    Ok(match browser.run::<N>().await? {
224        Some(browser) => (browser.address().into(), true),
225        None => (SenderKind::from_wallet_opts(wallet, &tx.auth).await?, false),
226    })
227}
228
229/// Validates that `sender` can resolve every EIP-7702 authorization.
230pub(crate) fn validate_authorizations(
231    authorizations: &[CliAuthorizationList],
232    sender: &SenderKind<'_>,
233) -> Result<()> {
234    let address_auth_count = authorizations
235        .iter()
236        .filter(|auth| matches!(auth, CliAuthorizationList::Address(_)))
237        .count();
238    if address_auth_count > 1 {
239        eyre::bail!(
240            "Multiple address-based authorizations provided. Only one address can be specified; \
241             use pre-signed authorizations (hex-encoded) for multiple authorizations."
242        );
243    }
244    if address_auth_count == 1 && sender.as_signer().is_none() {
245        eyre::bail!(
246            "No signer available to sign authorization. \
247             Provide a pre-signed authorization (hex-encoded) instead."
248        );
249    }
250
251    Ok(())
252}
253
254/// Resolves the sending signer, falling back to the wallet options, and validates it against an
255/// explicit `--from`.
256pub(crate) async fn resolve_send_signer(
257    pre_resolved: Option<WalletSigner>,
258    eth: &EthereumOpts,
259) -> Result<(WalletSigner, Address)> {
260    let signer = match pre_resolved {
261        Some(signer) => signer,
262        None => eth.wallet.signer().await?,
263    };
264    let from = signer.address();
265    validate_from_address(eth.wallet.from, from)?;
266    Ok((signer, from))
267}
268
269/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
270pub fn validate_from_address(
271    specified_from: Option<Address>,
272    signer_address: Address,
273) -> Result<()> {
274    if let Some(specified_from) = specified_from
275        && specified_from != signer_address
276    {
277        eyre::bail!(
278                "\
279The specified sender via CLI/env vars does not match the sender configured via
280the hardware wallet's HD Path.
281Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
282corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
283            );
284    }
285    Ok(())
286}
287
288/// Initial state.
289#[derive(Debug)]
290pub struct InitState;
291
292/// State with known [TxKind].
293#[derive(Debug)]
294pub struct ToState {
295    to: Option<Address>,
296}
297
298/// State with known input for the transaction.
299#[derive(Debug)]
300pub struct InputState {
301    kind: TxKind,
302    input: Vec<u8>,
303    func: Option<Function>,
304}
305
306pub struct CastTxSender<N, P> {
307    provider: P,
308    _phantom: PhantomData<N>,
309}
310
311impl<N: Network, P: Provider<N>> CastTxSender<N, P>
312where
313    N::TransactionRequest: FoundryTransactionBuilder<N>,
314    N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
315{
316    /// Creates a new Cast instance responsible for sending transactions.
317    pub const fn new(provider: P) -> Self {
318        Self { provider, _phantom: PhantomData }
319    }
320
321    /// Sends a transaction and waits for receipt synchronously
322    pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
323        let receipt = self.provider.send_transaction_sync(tx).await?;
324        self.finish(receipt, None).await
325    }
326
327    /// Sends a transaction and returns the pending transaction handle.
328    pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
329        Ok(self.provider.send_transaction(tx).await?)
330    }
331
332    /// Sends a raw RLP-encoded transaction and waits for its receipt synchronously.
333    pub async fn send_raw_sync(&self, raw_tx: &[u8]) -> Result<(B256, String)> {
334        let receipt = self.provider.send_raw_transaction_sync(raw_tx).await?;
335        self.finish(receipt, None).await
336    }
337
338    /// Prints the transaction hash (if async) or waits for the receipt and prints it.
339    ///
340    /// This is the shared "output" path used by both the normal send flow and the browser wallet
341    /// flow (which sends the transaction out-of-band and only has a tx hash).
342    pub async fn print_tx_result(
343        &self,
344        tx_hash: B256,
345        cast_async: bool,
346        confs: u64,
347        timeout: u64,
348    ) -> Result<()> {
349        if cast_async {
350            sh_println!("{tx_hash:#x}")?;
351        } else {
352            let receipt =
353                self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
354            sh_println!("{receipt}")?;
355        }
356        Ok(())
357    }
358
359    /// Fetches the receipt of `tx_hash`, polling for it unless `cast_async` is set, and formats
360    /// it (or the requested `field`).
361    pub async fn receipt(
362        &self,
363        tx_hash: String,
364        field: Option<String>,
365        confs: u64,
366        timeout: Option<u64>,
367        cast_async: bool,
368    ) -> Result<String> {
369        let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
370        let receipt = match self.provider.get_transaction_receipt(tx_hash).await? {
371            Some(receipt) => receipt,
372            None if cast_async => eyre::bail!("tx not found: {tx_hash:?}"),
373            None => {
374                PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
375                    .with_required_confirmations(confs)
376                    .with_timeout(timeout.map(Duration::from_secs))
377                    .get_receipt()
378                    .await?
379            }
380        };
381        Ok(self.finish(receipt, field).await?.1)
382    }
383
384    /// Attaches the revert reason (best effort) and formats the receipt.
385    async fn finish(
386        &self,
387        receipt: N::ReceiptResponse,
388        field: Option<String>,
389    ) -> Result<(B256, String)> {
390        let mut receipt = TransactionReceiptWithRevertReason::<N> { receipt, revert_reason: None };
391        let tx_hash = receipt.receipt.transaction_hash();
392        let _ = receipt.update_revert_reason(&self.provider).await;
393        let formatted = if let Some(field) = field {
394            get_pretty_receipt_w_reason_attr(&receipt, &field)
395                .ok_or_else(|| eyre::eyre!("invalid receipt field: {field}"))?
396        } else if shell::is_json() {
397            // to_value first to sort json object keys
398            serde_json::to_value(&receipt)?.to_string()
399        } else {
400            receipt.pretty()
401        };
402        Ok((tx_hash, formatted))
403    }
404}
405
406/// Builder type constructing generic TransactionRequest from cast send/mktx inputs.
407///
408/// It is implemented as a stateful builder with expected state transition of [InitState] ->
409/// [ToState] -> [InputState].
410#[derive(Debug)]
411pub struct CastTxBuilder<N: Network, P, S> {
412    inner: TxBuilderInner<N, P>,
413    state: S,
414}
415
416/// The state-independent part of [`CastTxBuilder`].
417#[derive(Debug)]
418struct TxBuilderInner<N: Network, P> {
419    provider: P,
420    tx: N::TransactionRequest,
421    /// Whether the transaction should be sent as a legacy transaction.
422    legacy: bool,
423    blob: bool,
424    /// Whether the blob transaction should use EIP-4844 (legacy) format instead of EIP-7594.
425    eip4844: bool,
426    /// Whether to fill gas, fees and nonce. Set to `false` for read-only calls
427    /// (eth_call, eth_estimateGas, eth_createAccessList).
428    fill: bool,
429    /// Whether the filled transaction will be submitted through a browser wallet.
430    browser: bool,
431    /// The preset used when estimating EIP-1559 fees.
432    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
433    /// Optional percentage applied to provider gas estimates.
434    gas_estimate_multiplier: Option<u64>,
435    auth: Vec<CliAuthorizationList>,
436    chain: Chain,
437    etherscan_api_key: Option<String>,
438    etherscan_api_url: Option<String>,
439    access_list: Option<Option<AccessList>>,
440}
441
442impl<N: Network, P, S> CastTxBuilder<N, P, S> {
443    /// Returns the resolved chain for this builder.
444    pub const fn chain(&self) -> Chain {
445        self.inner.chain
446    }
447
448    /// Returns the Etherscan API key and URL resolved for this builder's chain.
449    pub(crate) fn etherscan_api(&self) -> (Option<&str>, Option<&str>) {
450        (self.inner.etherscan_api_key.as_deref(), self.inner.etherscan_api_url.as_deref())
451    }
452
453    /// Returns the transaction request being built.
454    pub(crate) const fn tx_mut(&mut self) -> &mut N::TransactionRequest {
455        &mut self.inner.tx
456    }
457
458    /// Marks this transaction as destined for browser wallet submission.
459    pub const fn with_browser_wallet(mut self) -> Self {
460        self.inner.browser = true;
461        self
462    }
463
464    /// Applies a percentage multiplier to provider gas estimates.
465    pub const fn with_gas_estimate_multiplier(mut self, multiplier: Option<u64>) -> Self {
466        self.inner.gas_estimate_multiplier = multiplier;
467        self
468    }
469
470    /// Returns whether this builder contains any EIP-7702 authorizations.
471    pub(crate) const fn has_auth(&self) -> bool {
472        !self.inner.auth.is_empty()
473    }
474
475    /// Validates that the configured sender can resolve all EIP-7702 authorizations.
476    pub(crate) fn validate_auth(&self, sender: &SenderKind<'_>) -> Result<()> {
477        validate_authorizations(&self.inner.auth, sender)
478    }
479
480    /// Returns whether building this request will disclose an EIP-7702 authorization to an RPC
481    /// endpoint.
482    pub(crate) fn will_disclose_auth_during_build(&self) -> bool {
483        // Generating an access list or estimating gas sends the authorization-bearing request
484        // to the RPC.
485        self.has_auth()
486            && (matches!(self.inner.access_list, Some(None))
487                || (self.inner.fill && self.inner.tx.gas_limit().is_none()))
488    }
489
490    fn with_state<S2>(self, state: S2) -> CastTxBuilder<N, P, S2> {
491        CastTxBuilder { inner: self.inner, state }
492    }
493}
494
495impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
496where
497    N::TransactionRequest: FoundryTransactionBuilder<N>,
498{
499    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
500    /// provided [TransactionOpts].
501    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
502        let mut tx = N::TransactionRequest::default();
503
504        let chain = utils::get_chain(config.chain, &provider).await?;
505        let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
506        let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
507        let etherscan_api_url = etherscan_config.map(|c| c.api_url);
508        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
509        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
510
511        tx_opts.apply::<N>(&mut tx, legacy);
512        // The curl transport answers every request with a placeholder, so there is nothing to
513        // check against.
514        if !config.eth_rpc_curl {
515            ensure_expiring_nonce_discriminator_active(&provider, &tx).await?;
516        }
517
518        Ok(Self {
519            inner: TxBuilderInner {
520                provider,
521                tx,
522                legacy,
523                blob: tx_opts.blob,
524                eip4844: tx_opts.eip4844,
525                fill: true,
526                browser: false,
527                eip1559_fee_estimate: config.eip1559_fee_estimate,
528                gas_estimate_multiplier: None,
529                chain,
530                etherscan_api_key,
531                etherscan_api_url,
532                auth: tx_opts.auth,
533                access_list: tx_opts.access_list,
534            },
535            state: InitState,
536        })
537    }
538
539    /// Sets [TxKind] for this builder and changes state to [ToState].
540    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
541        let to = match to {
542            Some(to) => Some(to.resolve(&self.inner.provider).await?),
543            None => None,
544        };
545        Ok(self.with_state(ToState { to }))
546    }
547}
548
549impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
550where
551    N::TransactionRequest: FoundryTransactionBuilder<N>,
552{
553    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
554    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
555    /// present, input is set to just provided arguments.
556    pub async fn with_code_sig_and_args(
557        self,
558        code: Option<String>,
559        sig: Option<String>,
560        args: Vec<String>,
561    ) -> Result<CastTxBuilder<N, P, InputState>> {
562        let to = self.state.to;
563        let (mut args, func) = if let Some(sig) = sig {
564            let (key, url) = self.etherscan_api();
565            parse_function_args(&sig, args, to, self.chain(), &self.inner.provider, key, url)
566                .await?
567        } else {
568            (Vec::new(), None)
569        };
570
571        let input = if let Some(code) = &code {
572            let mut code = hex::decode(code)?;
573            code.append(&mut args);
574            code
575        } else {
576            args
577        };
578
579        // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
580        // without a value. The sender is used as the destination in that case, see `build`.
581        if to.is_none()
582            && code.is_none()
583            && (!self.has_auth() || self.inner.tx.value().is_some_and(|v| !v.is_zero()))
584        {
585            eyre::bail!("Must specify a recipient address or contract code to deploy");
586        }
587        if to.is_none() && code.is_some() && self.has_auth() {
588            eyre::bail!(
589                "EIP-7702 transactions can't be CREATE transactions and require a destination address"
590            );
591        }
592
593        Ok(self.with_state(InputState { kind: to.into(), input, func }))
594    }
595}
596
597impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
598where
599    N::TransactionRequest: FoundryTransactionBuilder<N>,
600{
601    /// Builds the TransactionRequest. Fills gas, fees and nonce unless [`raw`](Self::raw) was
602    /// called.
603    pub async fn build(
604        self,
605        sender: impl Into<SenderKind<'_>>,
606    ) -> Result<(N::TransactionRequest, Option<Function>)> {
607        self.inner.build(self.state, &sender.into(), None).await
608    }
609
610    /// Builds a transaction that will be signed by a Tempo access key.
611    ///
612    /// The access-key id is set before gas estimation. If the access key needs on-chain
613    /// provisioning, its authorization is embedded before access-list/gas estimation and before
614    /// any sponsor digest can be computed.
615    pub async fn build_with_tempo_wallet(
616        self,
617        wallet: &TempoAccountsWallet,
618    ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
619        let mut prepared = wallet.clone();
620        let (tx, func) =
621            self.inner.build(self.state, &wallet.account().into(), Some(&mut prepared)).await?;
622        Ok((tx, func, prepared))
623    }
624
625    /// Populates the blob sidecar for the transaction if any blob data was provided.
626    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
627        let Some(blob_data) = blob_data else { return Ok(self) };
628
629        let mut coder = SidecarBuilder::<SimpleCoder>::default();
630        coder.ingest(&blob_data);
631        if self.inner.eip4844 {
632            self.inner.tx.set_blob_sidecar_4844(coder.build_4844()?);
633        } else {
634            self.inner.tx.set_blob_sidecar_7594(coder.build_7594()?);
635        }
636        Ok(self)
637    }
638
639    /// Skips gas, fee and nonce filling. Use for read-only calls
640    /// (eth_call, eth_estimateGas, eth_createAccessList).
641    pub const fn raw(mut self) -> Self {
642        self.inner.fill = false;
643        self
644    }
645}
646
647impl<N: Network, P: Provider<N>> TxBuilderInner<N, P>
648where
649    N::TransactionRequest: FoundryTransactionBuilder<N>,
650{
651    async fn build(
652        mut self,
653        state: InputState,
654        sender: &SenderKind<'_>,
655        tempo_wallet: Option<&mut TempoAccountsWallet>,
656    ) -> Result<(N::TransactionRequest, Option<Function>)> {
657        let fill = self.fill;
658        let from = sender.address();
659
660        // An EIP-7702 transaction can't be a CREATE. If the recipient was omitted, the sender
661        // itself is the destination, which is the common case for self-delegation.
662        let kind = match state.kind {
663            TxKind::Create if !self.auth.is_empty() => TxKind::Call(from),
664            kind => kind,
665        };
666        self.tx.set_kind(kind);
667        // We set both fields to the same value because some nodes only accept the legacy
668        // `data` field: https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249
669        self.tx.set_input_kind(state.input, TransactionInputKind::Both);
670        if !from.is_zero() {
671            self.tx.set_from(from);
672        }
673        self.tx.set_chain_id(self.chain.id());
674        // For batch transactions with calls, clear `to` and `value` so the node correctly
675        // identifies this as an AA batch transaction. The `calls` field determines the actual
676        // targets. If `to` is set, `build_aa()` would add a spurious extra call.
677        self.tx.clear_batch_to();
678
679        // Read-only calls do not need a nonce unless it is required to sign an authorization.
680        // Avoid an otherwise unused `eth_getTransactionCount` request for raw transactions.
681        let resolve_in_parallel =
682            fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
683        let tx_nonce = if resolve_in_parallel {
684            let fees_are_complete = if self.legacy {
685                self.tx.gas_price().is_some()
686            } else {
687                matches!(
688                    (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
689                    (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
690                )
691            } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
692            let gas_request =
693                (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
694                    .then(|| self.tx.clone());
695            let (blob, legacy, browser, eip1559_fee_estimate, gas_estimate_multiplier) = (
696                self.blob,
697                self.legacy,
698                self.browser,
699                self.eip1559_fee_estimate,
700                self.gas_estimate_multiplier,
701            );
702            let Self { provider, tx, .. } = &mut self;
703            let provider = &*provider;
704            let (tx_nonce, (), gas_limit) = tokio::try_join!(
705                Self::resolve_nonce(provider, from, tx.nonce()),
706                Self::fill_fees(provider, tx, blob, legacy, browser, eip1559_fee_estimate),
707                async {
708                    match gas_request {
709                        Some(request) => {
710                            Self::estimate_gas(provider, request, gas_estimate_multiplier)
711                                .await
712                                .map(Some)
713                        }
714                        None => Ok(None),
715                    }
716                },
717            )?;
718            if let Some(gas_limit) = gas_limit {
719                self.tx.set_gas_limit(gas_limit);
720            }
721            Some(tx_nonce)
722        } else if fill || !self.auth.is_empty() {
723            Some(Self::resolve_nonce(&self.provider, from, self.tx.nonce()).await?)
724        } else {
725            None
726        };
727        if let Some(tx_nonce) = tx_nonce {
728            if fill {
729                self.tx.set_nonce(tx_nonce);
730            }
731            self.resolve_auth(sender, tx_nonce).await?;
732        }
733        if let Some(wallet) = tempo_wallet {
734            *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
735        }
736        if fill && !resolve_in_parallel {
737            Self::fill_fees(
738                &self.provider,
739                &mut self.tx,
740                self.blob,
741                self.legacy,
742                self.browser,
743                self.eip1559_fee_estimate,
744            )
745            .await?;
746        }
747        // Fetch the access list from the provider if `--access-list` was passed without a value.
748        let access_list = match self.access_list.take() {
749            None => None,
750            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
751            Some(Some(access_list)) => Some(access_list),
752        };
753        if let Some(access_list) = access_list {
754            self.tx.set_access_list(access_list);
755        }
756        if fill && self.tx.gas_limit().is_none() {
757            let request = if self.browser {
758                self.tx.browser_wallet_gas_estimation_request()
759            } else {
760                self.tx.clone()
761            };
762            let estimated =
763                Self::estimate_gas(&self.provider, request, self.gas_estimate_multiplier).await?;
764            self.tx.set_gas_limit(estimated);
765        }
766
767        Ok((self.tx, state.func))
768    }
769
770    /// Resolves the transaction nonce. Returns the existing nonce or fetches one from the provider.
771    async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
772        match nonce {
773            Some(nonce) => Ok(nonce),
774            None => Ok(provider.get_transaction_count(from).await?),
775        }
776    }
777
778    /// 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        validate_authorizations(&self.auth, sender)?;
787
788        let mut signed_auths = Vec::with_capacity(self.auth.len());
789        for auth in std::mem::take(&mut self.auth) {
790            signed_auths.push(match auth {
791                CliAuthorizationList::Address(address) => {
792                    let auth = Authorization {
793                        chain_id: U256::from(self.chain.id()),
794                        nonce: tx_nonce + 1,
795                        address,
796                    };
797                    let signer =
798                        sender.as_signer().expect("address-based authorization requires a signer");
799                    let signature = signer.sign_hash(&auth.signature_hash()).await?;
800                    auth.into_signed(signature)
801                }
802                CliAuthorizationList::Signed(auth) => auth,
803            });
804        }
805        self.tx.set_authorization_list(signed_auths);
806        Ok(())
807    }
808
809    /// Fills gas price, EIP-1559 fees, and blob fees from the provider.
810    ///
811    /// Only fills values that haven't been explicitly set by the user.
812    async fn fill_fees(
813        provider: &P,
814        tx: &mut N::TransactionRequest,
815        blob: bool,
816        legacy: bool,
817        browser: bool,
818        eip1559_fee_estimate: Eip1559FeeEstimatePreset,
819    ) -> Result<()> {
820        if blob && tx.max_fee_per_blob_gas().is_none() {
821            tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
822        }
823        fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
824    }
825
826    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
827    async fn estimate_gas(
828        provider: &P,
829        request: N::TransactionRequest,
830        multiplier: Option<u64>,
831    ) -> Result<u64> {
832        let err = match provider.estimate_gas(request).await {
833            Ok(estimated) => return apply_gas_estimate_multiplier(estimated, multiplier),
834            Err(err) => err,
835        };
836        // If execution reverted with code 3 during provider gas estimation then try to decode
837        // custom errors and append it to the error message.
838        if let TransportError::ErrorResp(payload) = &err
839            && payload.code == 3
840            && let Some(data) = &payload.data
841            && let Ok(data) = serde_json::from_str::<Bytes>(data.get())
842            && let Ok(Some(decoded_error)) = decode_custom_error(&data).await
843        {
844            eyre::bail!("Failed to estimate gas: {err}: {decoded_error}");
845        }
846        eyre::bail!("Failed to estimate gas: {err}");
847    }
848}
849
850/// Fills gas price or EIP-1559 fee fields from the provider and validates the final pair.
851pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
852    provider: &P,
853    tx: &mut N::TransactionRequest,
854    legacy: bool,
855    browser: bool,
856    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
857) -> Result<()>
858where
859    N::TransactionRequest: FoundryTransactionBuilder<N>,
860{
861    if legacy {
862        if tx.gas_price().is_none() {
863            tx.set_gas_price(provider.get_gas_price().await?);
864        }
865        return Ok(());
866    }
867
868    if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
869        let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
870
871        // Only honor the browser-suggested tip when the user has not pinned a
872        // priority fee; `resolve_broadcast_eip1559_fees` ignores a lower tip.
873        let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
874            provider.get_max_priority_fee_per_gas().await.ok()
875        } else {
876            None
877        };
878
879        // User `--gas-price`/`--priority-gas-price` overrides are already applied
880        // to `tx`; pass `None` so they are not double-applied here.
881        let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
882
883        if tx.max_fee_per_gas().is_none() {
884            tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
885        }
886
887        if tx.max_priority_fee_per_gas().is_none() {
888            tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
889        }
890    }
891
892    if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
893        eyre::ensure!(
894            priority <= max_fee,
895            "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
896        );
897    }
898
899    Ok(())
900}
901
902/// Tries to decode a custom error name and inputs from revert data.
903pub(crate) async fn decode_custom_error(data: &[u8]) -> Result<Option<String>> {
904    let Some(selector) = data.get(..4) else { return Ok(None) };
905    let Some(known_error) =
906        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
907    else {
908        return Ok(None);
909    };
910    let mut decoded_error = known_error.name.clone();
911    if !known_error.inputs.is_empty()
912        && let Ok(error) = known_error.decode_error(data)
913    {
914        write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
915    }
916    Ok(Some(decoded_error))
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922    use alloy_json_rpc::{RequestPacket, ResponsePacket};
923    use alloy_network::{Ethereum, NetworkTransactionBuilder};
924    use alloy_provider::{ProviderBuilder, mock::Asserter};
925    use alloy_rpc_client::RpcClient;
926    use alloy_sol_types::SolValue;
927    use alloy_transport::{TransportFut, mock::MockTransport};
928    use clap::Parser;
929    use foundry_common::tempo::resolve_and_set_fee_token;
930    use std::{
931        sync::{Arc, Mutex},
932        task::{Context, Poll},
933    };
934    use tempo_alloy::TempoNetwork;
935    use tokio::{sync::Barrier, time::timeout};
936    use tower::Service;
937
938    #[derive(Clone)]
939    struct BarrierTransport {
940        inner: MockTransport,
941        barrier: Arc<Barrier>,
942        fill_methods: Arc<Mutex<Vec<String>>>,
943    }
944
945    impl Service<RequestPacket> for BarrierTransport {
946        type Response = ResponsePacket;
947        type Error = TransportError;
948        type Future = TransportFut<'static>;
949
950        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
951            self.inner.poll_ready(cx)
952        }
953
954        fn call(&mut self, req: RequestPacket) -> Self::Future {
955            let fill_method = match &req {
956                RequestPacket::Single(req)
957                    if matches!(
958                        req.method(),
959                        "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
960                    ) =>
961                {
962                    Some(req.method().to_string())
963                }
964                _ => None,
965            };
966            let Some(fill_method) = fill_method else {
967                return self.inner.call(req);
968            };
969            self.fill_methods.lock().unwrap().push(fill_method);
970
971            let barrier = self.barrier.clone();
972            let mut inner = self.inner.clone();
973            Box::pin(async move {
974                barrier.wait().await;
975                inner.call(req).await
976            })
977        }
978    }
979
980    const TO: Address = Address::repeat_byte(0x11);
981
982    /// Builds a transaction to [`TO`] from the given `cast send` style arguments.
983    async fn builder<P: Provider<Ethereum>>(
984        provider: P,
985        args: &[&str],
986    ) -> CastTxBuilder<Ethereum, P, InputState> {
987        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
988        let opts = TransactionOpts::parse_from([&["test"], args].concat());
989        CastTxBuilder::new(provider, opts, &config)
990            .await
991            .unwrap()
992            .with_to(Some(TO.into()))
993            .await
994            .unwrap()
995            .with_code_sig_and_args(None, None, Vec::new())
996            .await
997            .unwrap()
998    }
999
1000    #[tokio::test]
1001    async fn filled_build_applies_multiplier_to_concurrent_gas_estimate() {
1002        let asserter = Asserter::new();
1003        for _ in 0..2 {
1004            asserter.push_success(&U64::from(100));
1005        }
1006        let fill_methods = Arc::new(Mutex::new(Vec::new()));
1007        let transport = BarrierTransport {
1008            inner: MockTransport::new(asserter),
1009            barrier: Arc::new(Barrier::new(2)),
1010            fill_methods: fill_methods.clone(),
1011        };
1012        let provider = ProviderBuilder::new_with_network::<Ethereum>()
1013            .connect_client(RpcClient::new(transport, true));
1014
1015        let builder = builder(&provider, &["--legacy", "--gas-price", "1"])
1016            .await
1017            .with_gas_estimate_multiplier(Some(150));
1018        let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
1019            .await
1020            .expect("nonce and gas requests were not in flight together")
1021            .unwrap();
1022
1023        assert_eq!(tx.nonce, Some(100));
1024        assert_eq!(tx.gas_price, Some(1));
1025        assert_eq!(tx.gas, Some(150));
1026        let mut fill_methods = fill_methods.lock().unwrap().clone();
1027        fill_methods.sort();
1028        assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
1029    }
1030
1031    #[tokio::test]
1032    async fn browser_submission_resolves_type_after_tempo_fee_token() {
1033        let asserter = Asserter::new();
1034        let token = Address::repeat_byte(0x42);
1035        asserter.push_success(&token.abi_encode());
1036        let provider =
1037            ProviderBuilder::new_with_network::<TempoNetwork>().connect_mocked_client(asserter);
1038        let chain = Chain::from_id(4217);
1039        let config = Config { chain: Some(chain), ..Default::default() };
1040        let opts = TransactionOpts::parse_from([
1041            "test",
1042            "--gas-limit",
1043            "21000",
1044            "--nonce",
1045            "0",
1046            "--gas-price",
1047            "1",
1048            "--priority-gas-price",
1049            "1",
1050        ]);
1051        let (mut tx, _) = CastTxBuilder::new(&provider, opts, &config)
1052            .await
1053            .unwrap()
1054            .with_browser_wallet()
1055            .with_to(Some(TO.into()))
1056            .await
1057            .unwrap()
1058            .with_code_sig_and_args(None, None, Vec::new())
1059            .await
1060            .unwrap()
1061            .build(Address::repeat_byte(0x22))
1062            .await
1063            .unwrap();
1064        assert_eq!(tx.inner.transaction_type, None);
1065        assert_eq!(tx.fee_token, None);
1066
1067        // Fee resolution happens after building and can change the submission type to Tempo AA.
1068        resolve_and_set_fee_token(Some(&provider), &mut tx, None).await.unwrap();
1069        tx.prep_for_submission();
1070        assert_eq!(tx.inner.transaction_type, Some(0x76));
1071        assert_eq!(tx.fee_token, Some(token));
1072    }
1073
1074    #[tokio::test]
1075    async fn raw_build_skips_nonce_request() {
1076        // No responses are queued, so any RPC request would fail this test. In particular, this
1077        // guards against restoring the unused `eth_getTransactionCount` request.
1078        let provider =
1079            ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1080        builder(&provider, &[]).await.raw().build(Address::repeat_byte(0x22)).await.unwrap();
1081    }
1082
1083    #[tokio::test]
1084    async fn detects_auth_rpc_disclosure() {
1085        let provider =
1086            ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
1087        let auth = TO.to_string();
1088
1089        let no_auth = builder(&provider, &["--gas-limit", "21000"]).await.raw();
1090        assert!(!no_auth.has_auth());
1091        assert!(!no_auth.will_disclose_auth_during_build());
1092
1093        let rpc_call = builder(&provider, &["--auth", &auth, "--gas-limit", "21000"]).await.raw();
1094        assert!(rpc_call.has_auth());
1095        assert!(!rpc_call.will_disclose_auth_during_build());
1096
1097        let generated_access_list = builder(&provider, &["--auth", &auth, "--access-list"]).await;
1098        assert!(generated_access_list.raw().will_disclose_auth_during_build());
1099
1100        let explicit_access_list =
1101            builder(&provider, &["--auth", &auth, "--access-list", "[]"]).await;
1102        assert!(!explicit_access_list.raw().will_disclose_auth_during_build());
1103
1104        let estimated_gas = builder(&provider, &["--auth", &auth]).await;
1105        assert!(estimated_gas.will_disclose_auth_during_build());
1106    }
1107
1108    #[test]
1109    fn tx_params_parses_gas_price_units() {
1110        #[derive(Parser)]
1111        struct Cli {
1112            #[command(flatten)]
1113            tx: TxParams,
1114        }
1115
1116        let cli =
1117            Cli::try_parse_from(["test", "--gas-price", "10gwei", "--priority-gas-price", "2gwei"])
1118                .unwrap();
1119        assert_eq!(cli.tx.gas_price, Some(U256::from(10_000_000_000u64)));
1120        assert_eq!(cli.tx.priority_gas_price, Some(U256::from(2_000_000_000u64)));
1121
1122        let cli = Cli::try_parse_from(["test", "--gas-price", "100", "--priority-gas-price", "2"])
1123            .unwrap();
1124        assert_eq!(cli.tx.gas_price, Some(U256::from(100)));
1125        assert_eq!(cli.tx.priority_gas_price, Some(U256::from(2)));
1126    }
1127}