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, TempoAccessKeyConfig, 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` which may not be supported by all clients.
39    #[arg(long, conflicts_with = "async")]
40    pub sync: bool,
41
42    /// The number of confirmations until the receipt is fetched.
43    #[arg(long, default_value = "1")]
44    pub confirmations: u64,
45
46    /// Timeout for sending the transaction.
47    #[arg(long, env = "ETH_TIMEOUT")]
48    pub timeout: Option<u64>,
49
50    /// Polling interval for transaction receipts (in seconds).
51    #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
52    pub poll_interval: Option<u64>,
53
54    /// Ethereum options
55    #[command(flatten)]
56    pub eth: EthereumOpts,
57
58    /// Browser wallet options
59    #[command(flatten)]
60    pub browser: BrowserWalletOpts,
61}
62
63/// Transaction options shared across cast commands that submit on-chain transactions.
64#[derive(Debug, Clone, Args)]
65#[command(next_help_heading = "Transaction options")]
66pub struct TxParams {
67    /// Gas limit for the transaction.
68    #[arg(long, env = "ETH_GAS_LIMIT")]
69    pub gas_limit: Option<U256>,
70
71    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.
72    #[arg(long, env = "ETH_GAS_PRICE")]
73    pub gas_price: Option<U256>,
74
75    /// Max priority fee per gas for EIP1559 transactions.
76    #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
77    pub priority_gas_price: Option<U256>,
78
79    /// Nonce for the transaction.
80    #[arg(long)]
81    pub nonce: Option<U64>,
82
83    #[command(flatten)]
84    pub tempo: TempoOpts,
85}
86
87impl TxParams {
88    pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
89    where
90        N::TransactionRequest: FoundryTransactionBuilder<N>,
91    {
92        if let Some(gas_limit) = self.gas_limit {
93            tx.set_gas_limit(gas_limit.to());
94        }
95
96        if let Some(gas_price) = self.gas_price {
97            if legacy {
98                tx.set_gas_price(gas_price.to());
99            } else {
100                tx.set_max_fee_per_gas(gas_price.to());
101            }
102        }
103
104        if !legacy && let Some(priority_fee) = self.priority_gas_price {
105            tx.set_max_priority_fee_per_gas(priority_fee.to());
106        }
107
108        self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
109    }
110}
111
112/// Different sender kinds used by [`CastTxBuilder`].
113pub enum SenderKind<'a> {
114    /// An address without signer. Used for read-only calls and transactions sent through unlocked
115    /// accounts.
116    Address(Address),
117    /// A reference to a signer.
118    Signer(&'a WalletSigner),
119    /// An owned signer.
120    OwnedSigner(Box<WalletSigner>),
121}
122
123impl SenderKind<'_> {
124    /// Resolves the name to an Ethereum Address.
125    pub fn address(&self) -> Address {
126        match self {
127            Self::Address(addr) => *addr,
128            Self::Signer(signer) => signer.address(),
129            Self::OwnedSigner(signer) => signer.address(),
130        }
131    }
132
133    /// Resolves the sender from the wallet options.
134    ///
135    /// This function prefers the `from` field and may return a different address from the
136    /// configured signer
137    /// If from is specified, returns it
138    /// If from is not specified, but there is a signer configured, returns the signer's address
139    /// If from is not specified and there is no signer configured, returns zero address
140    pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
141        if let (Some(signer), _) = opts.maybe_signer().await? {
142            Ok(Self::OwnedSigner(Box::new(signer)))
143        } else if let Some(from) = opts.from {
144            Ok(from.into())
145        } else {
146            Ok(Address::ZERO.into())
147        }
148    }
149
150    /// Returns the signer if available.
151    pub fn as_signer(&self) -> Option<&WalletSigner> {
152        match self {
153            Self::Signer(signer) => Some(signer),
154            Self::OwnedSigner(signer) => Some(signer.as_ref()),
155            _ => None,
156        }
157    }
158}
159
160impl From<Address> for SenderKind<'_> {
161    fn from(addr: Address) -> Self {
162        Self::Address(addr)
163    }
164}
165
166impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
167    fn from(signer: &'a WalletSigner) -> Self {
168        Self::Signer(signer)
169    }
170}
171
172impl From<WalletSigner> for SenderKind<'_> {
173    fn from(signer: WalletSigner) -> Self {
174        Self::OwnedSigner(Box::new(signer))
175    }
176}
177
178/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
179pub fn validate_from_address(
180    specified_from: Option<Address>,
181    signer_address: Address,
182) -> Result<()> {
183    if let Some(specified_from) = specified_from
184        && specified_from != signer_address
185    {
186        eyre::bail!(
187                "\
188The specified sender via CLI/env vars does not match the sender configured via
189the hardware wallet's HD Path.
190Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
191corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
192            );
193    }
194    Ok(())
195}
196
197/// Initial state.
198#[derive(Debug)]
199pub struct InitState;
200
201/// State with known [TxKind].
202#[derive(Debug)]
203pub struct ToState {
204    to: Option<Address>,
205}
206
207/// State with known input for the transaction.
208#[derive(Debug)]
209pub struct InputState {
210    kind: TxKind,
211    input: Vec<u8>,
212    func: Option<Function>,
213}
214
215pub struct CastTxSender<N, P> {
216    provider: P,
217    _phantom: PhantomData<N>,
218}
219
220impl<N: Network, P: Provider<N>> CastTxSender<N, P>
221where
222    N::TransactionRequest: FoundryTransactionBuilder<N>,
223    N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
224{
225    /// Creates a new Cast instance responsible for sending transactions.
226    pub const fn new(provider: P) -> Self {
227        Self { provider, _phantom: PhantomData }
228    }
229
230    /// Sends a transaction and waits for receipt synchronously
231    pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
232        let mut receipt = TransactionReceiptWithRevertReason::<N> {
233            receipt: self.provider.send_transaction_sync(tx).await?,
234            revert_reason: None,
235        };
236        let tx_hash = receipt.receipt.transaction_hash();
237        // Allow to fail silently
238        let _ = receipt.update_revert_reason(&self.provider).await;
239
240        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
241    }
242
243    /// Sends a transaction to the specified address
244    ///
245    /// # Example
246    ///
247    /// ```
248    /// use cast::tx::CastTxSender;
249    /// use alloy_primitives::{Address, U256, Bytes};
250    /// use alloy_serde::WithOtherFields;
251    /// use alloy_rpc_types::{TransactionRequest};
252    /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork};
253    /// use std::str::FromStr;
254    /// use alloy_sol_types::{sol, SolCall};    ///
255    ///
256    /// sol!(
257    ///     function greet(string greeting) public;
258    /// );
259    ///
260    /// # async fn foo() -> eyre::Result<()> {
261    /// let provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;;
262    /// let from = Address::from_str("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")?;
263    /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?;
264    /// let greeting = greetCall { greeting: "hello".to_string() }.abi_encode();
265    /// let bytes = Bytes::from_iter(greeting.iter());
266    /// let gas = U256::from_str("200000").unwrap();
267    /// let value = U256::from_str("1").unwrap();
268    /// let nonce = U256::from_str("1").unwrap();
269    /// let tx = TransactionRequest::default().to(to).input(bytes.into()).from(from);
270    /// let tx = WithOtherFields::new(tx);
271    /// let cast = CastTxSender::new(provider);
272    /// let data = cast.send(tx).await?;
273    /// println!("{:#?}", data);
274    /// # Ok(())
275    /// # }
276    /// ```
277    pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
278        let res = self.provider.send_transaction(tx).await?;
279
280        Ok(res)
281    }
282
283    /// Sends a raw RLP-encoded transaction via `eth_sendRawTransaction`.
284    ///
285    /// Used for transaction types that the standard Alloy network stack doesn't understand
286    /// (e.g., Tempo transactions).
287    pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
288        let res = self.provider.send_raw_transaction(raw_tx).await?;
289        Ok(res)
290    }
291
292    /// Prints the transaction hash (if async) or waits for the receipt and prints it.
293    ///
294    /// This is the shared "output" path used by both the normal send flow and the browser wallet
295    /// flow (which sends the transaction out-of-band and only has a tx hash).
296    pub async fn print_tx_result(
297        &self,
298        tx_hash: B256,
299        cast_async: bool,
300        confs: u64,
301        timeout: u64,
302    ) -> Result<()> {
303        if cast_async {
304            sh_println!("{tx_hash:#x}")?;
305        } else {
306            let receipt =
307                self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
308            sh_println!("{receipt}")?;
309        }
310        Ok(())
311    }
312
313    /// # Example
314    ///
315    /// ```
316    /// use alloy_provider::{ProviderBuilder, RootProvider, network::AnyNetwork};
317    /// use cast::tx::CastTxSender;
318    ///
319    /// async fn foo() -> eyre::Result<()> {
320    /// let provider =
321    ///     ProviderBuilder::<_, _, AnyNetwork>::default().connect("http://localhost:8545").await?;
322    /// let cast = CastTxSender::new(provider);
323    /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc";
324    /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?;
325    /// println!("{}", receipt);
326    /// # Ok(())
327    /// # }
328    /// ```
329    pub async fn receipt(
330        &self,
331        tx_hash: String,
332        field: Option<String>,
333        confs: u64,
334        timeout: Option<u64>,
335        cast_async: bool,
336    ) -> Result<String> {
337        let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
338
339        let mut receipt = TransactionReceiptWithRevertReason::<N> {
340            receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
341                Some(r) => r,
342                None => {
343                    // if the async flag is provided, immediately exit if no tx is found, otherwise
344                    // try to poll for it
345                    if cast_async {
346                        eyre::bail!("tx not found: {:?}", tx_hash);
347                    }
348                    PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
349                        .with_required_confirmations(confs)
350                        .with_timeout(timeout.map(Duration::from_secs))
351                        .get_receipt()
352                        .await?
353                }
354            },
355            revert_reason: None,
356        };
357
358        // Allow to fail silently
359        let _ = receipt.update_revert_reason(&self.provider).await;
360
361        self.format_receipt(receipt, field)
362    }
363
364    /// Helper method to format transaction receipts consistently
365    fn format_receipt(
366        &self,
367        receipt: TransactionReceiptWithRevertReason<N>,
368        field: Option<String>,
369    ) -> Result<String> {
370        Ok(if let Some(ref field) = field {
371            get_pretty_receipt_w_reason_attr(&receipt, field)
372                .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
373        } else if shell::is_json() {
374            // to_value first to sort json object keys
375            serde_json::to_value(&receipt)?.to_string()
376        } else {
377            receipt.pretty()
378        })
379    }
380}
381
382/// Builder type constructing generic TransactionRequest from cast send/mktx inputs.
383///
384/// It is implemented as a stateful builder with expected state transition of [InitState] ->
385/// [ToState] -> [InputState].
386#[derive(Debug)]
387pub struct CastTxBuilder<N: Network, P, S> {
388    provider: P,
389    pub(crate) tx: N::TransactionRequest,
390    /// Whether the transaction should be sent as a legacy transaction.
391    legacy: bool,
392    blob: bool,
393    /// Whether the blob transaction should use EIP-4844 (legacy) format instead of EIP-7594.
394    eip4844: bool,
395    /// Whether to fill gas, fees and nonce. Set to `false` for read-only calls
396    /// (eth_call, eth_estimateGas, eth_createAccessList).
397    fill: bool,
398    /// Whether the filled transaction will be submitted through a browser wallet.
399    browser: bool,
400    /// The preset used when estimating EIP-1559 fees.
401    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
402    auth: Vec<CliAuthorizationList>,
403    chain: Chain,
404    etherscan_api_key: Option<String>,
405    etherscan_api_url: Option<String>,
406    access_list: Option<Option<AccessList>>,
407    state: S,
408}
409
410impl<N: Network, P, S> CastTxBuilder<N, P, S> {
411    /// Returns the resolved chain for this builder.
412    pub const fn chain(&self) -> Chain {
413        self.chain
414    }
415
416    /// Marks this transaction as destined for browser wallet submission.
417    pub const fn with_browser_wallet(mut self) -> Self {
418        self.browser = true;
419        self
420    }
421}
422
423impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
424where
425    N::TransactionRequest: FoundryTransactionBuilder<N>,
426{
427    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
428    /// provided [TransactionOpts].
429    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
430        let mut tx = N::TransactionRequest::default();
431
432        let chain = utils::get_chain(config.chain, &provider).await?;
433        let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
434        let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
435        let etherscan_api_url = etherscan_config.map(|c| c.api_url);
436        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
437        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
438
439        // Apply gas, value, fee, and network-specific options.
440        tx_opts.apply::<N>(&mut tx, legacy);
441
442        Ok(Self {
443            provider,
444            tx,
445            legacy,
446            blob: tx_opts.blob,
447            eip4844: tx_opts.eip4844,
448            fill: true,
449            browser: false,
450            eip1559_fee_estimate: config.eip1559_fee_estimate,
451            chain,
452            etherscan_api_key,
453            etherscan_api_url,
454            auth: tx_opts.auth,
455            access_list: tx_opts.access_list,
456            state: InitState,
457        })
458    }
459
460    /// Sets [TxKind] for this builder and changes state to [ToState].
461    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
462        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
463        Ok(CastTxBuilder {
464            provider: self.provider,
465            tx: self.tx,
466            legacy: self.legacy,
467            blob: self.blob,
468            eip4844: self.eip4844,
469            fill: self.fill,
470            browser: self.browser,
471            eip1559_fee_estimate: self.eip1559_fee_estimate,
472            chain: self.chain,
473            etherscan_api_key: self.etherscan_api_key,
474            etherscan_api_url: self.etherscan_api_url,
475            auth: self.auth,
476            access_list: self.access_list,
477            state: ToState { to },
478        })
479    }
480}
481
482impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
483where
484    N::TransactionRequest: FoundryTransactionBuilder<N>,
485{
486    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
487    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
488    /// present, input is set to just provided arguments.
489    pub async fn with_code_sig_and_args(
490        self,
491        code: Option<String>,
492        sig: Option<String>,
493        args: Vec<String>,
494    ) -> Result<CastTxBuilder<N, P, InputState>> {
495        let (mut args, func) = if let Some(sig) = sig {
496            parse_function_args(
497                &sig,
498                args,
499                self.state.to,
500                self.chain,
501                &self.provider,
502                self.etherscan_api_key.as_deref(),
503                self.etherscan_api_url.as_deref(),
504            )
505            .await?
506        } else {
507            (Vec::new(), None)
508        };
509
510        let input = if let Some(code) = &code {
511            let mut code = hex::decode(code)?;
512            code.append(&mut args);
513            code
514        } else {
515            args
516        };
517
518        if self.state.to.is_none() && code.is_none() {
519            let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
520            let has_auth = !self.auth.is_empty();
521            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
522            // without a value.
523            if !has_auth || has_value {
524                eyre::bail!("Must specify a recipient address or contract code to deploy");
525            }
526        }
527
528        Ok(CastTxBuilder {
529            provider: self.provider,
530            tx: self.tx,
531            legacy: self.legacy,
532            blob: self.blob,
533            eip4844: self.eip4844,
534            fill: self.fill,
535            browser: self.browser,
536            eip1559_fee_estimate: self.eip1559_fee_estimate,
537            chain: self.chain,
538            etherscan_api_key: self.etherscan_api_key,
539            etherscan_api_url: self.etherscan_api_url,
540            auth: self.auth,
541            access_list: self.access_list,
542            state: InputState { kind: self.state.to.into(), input, func },
543        })
544    }
545}
546
547impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
548where
549    N::TransactionRequest: FoundryTransactionBuilder<N>,
550{
551    /// Builds the TransactionRequest. Fills gas, fees and nonce unless [`raw`](Self::raw) was
552    /// called.
553    pub async fn build(
554        self,
555        sender: impl Into<SenderKind<'_>>,
556    ) -> Result<(N::TransactionRequest, Option<Function>)> {
557        let fill = self.fill;
558        self._build(sender, fill, None).await
559    }
560
561    /// Builds a transaction that will be signed by a Tempo access key.
562    ///
563    /// The access-key id is set before gas estimation. If the access key needs on-chain
564    /// provisioning, its authorization is embedded before access-list/gas estimation and before
565    /// any sponsor digest can be computed.
566    pub async fn build_with_access_key(
567        mut self,
568        sender: impl Into<SenderKind<'_>>,
569        access_key: &TempoAccessKeyConfig,
570    ) -> Result<(N::TransactionRequest, Option<Function>)> {
571        self.tx.set_key_id(access_key.key_address);
572        let fill = self.fill;
573        self._build(sender, fill, Some(access_key)).await
574    }
575
576    async fn _build(
577        mut self,
578        sender: impl Into<SenderKind<'_>>,
579        fill: bool,
580        access_key: Option<&TempoAccessKeyConfig>,
581    ) -> Result<(N::TransactionRequest, Option<Function>)> {
582        // prepare
583        let sender = sender.into();
584        self.prepare(&sender);
585
586        // For batch transactions with calls, clear `to` and `value` so the node correctly
587        // identifies this as an AA batch transaction. The `calls` field determines the actual
588        // targets. If `to` is set, `build_aa()` would add a spurious extra call.
589        self.tx.clear_batch_to();
590
591        // resolve
592        // Read-only calls do not need a nonce unless it is required to sign an authorization.
593        // Avoid an otherwise unused `eth_getTransactionCount` request for raw transactions.
594        if fill || !self.auth.is_empty() {
595            let tx_nonce = self.resolve_nonce(sender.address(), fill).await?;
596            self.resolve_auth(&sender, tx_nonce).await?;
597        }
598        if let Some(access_key) = access_key {
599            self.tx
600                .prepare_access_key_authorization(
601                    &self.provider,
602                    access_key.wallet_address,
603                    access_key.key_address,
604                    access_key.key_authorization.as_ref(),
605                )
606                .await?;
607        }
608        if fill {
609            self.fill_fees().await?;
610        }
611        self.resolve_access_list().await?;
612        if fill {
613            self.fill_gas_limit().await?;
614        }
615
616        Ok((self.tx, self.state.func))
617    }
618
619    /// Sets the core transaction fields from the builder state: kind, input, optional from, and
620    /// chain id.
621    fn prepare(&mut self, sender: &SenderKind<'_>) {
622        self.tx.set_kind(self.state.kind);
623        // We set both fields to the same value because some nodes only accept the legacy
624        // `data` field: https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249
625        self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
626        let sender = sender.address();
627        if !sender.is_zero() {
628            self.tx.set_from(sender);
629        }
630        self.tx.set_chain_id(self.chain.id());
631    }
632
633    /// Resolves the transaction nonce. Returns the existing nonce or fetches one from the
634    /// provider. Only sets it on the transaction when `fill` is true.
635    async fn resolve_nonce(&mut self, from: Address, fill: bool) -> Result<u64> {
636        if let Some(nonce) = self.tx.nonce() {
637            Ok(nonce)
638        } else {
639            let nonce = self.provider.get_transaction_count(from).await?;
640            if fill {
641                self.tx.set_nonce(nonce);
642            }
643            Ok(nonce)
644        }
645    }
646
647    /// Resolves the access list. Fetches from the provider if `--access-list` was passed without
648    /// a value.
649    async fn resolve_access_list(&mut self) -> Result<()> {
650        if let Some(access_list) = match self.access_list.take() {
651            None => None,
652            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
653            Some(Some(access_list)) => Some(access_list),
654        } {
655            self.tx.set_access_list(access_list);
656        }
657        Ok(())
658    }
659
660    /// Parses the passed --auth values and sets the authorization list on the transaction.
661    ///
662    /// If a signer is available in `sender`, address-based auths will be signed.
663    /// If no signer is available, all auths must be pre-signed.
664    async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
665        if self.auth.is_empty() {
666            return Ok(());
667        }
668
669        let auths = std::mem::take(&mut self.auth);
670
671        // Validate that at most one address-based auth is provided (multiple addresses are
672        // almost always unintended).
673        let address_auth_count =
674            auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
675        if address_auth_count > 1 {
676            eyre::bail!(
677                "Multiple address-based authorizations provided. Only one address can be specified; \
678                use pre-signed authorizations (hex-encoded) for multiple authorizations."
679            );
680        }
681
682        let mut signed_auths = Vec::with_capacity(auths.len());
683
684        for auth in auths {
685            let signed_auth = match auth {
686                CliAuthorizationList::Address(address) => {
687                    let auth = Authorization {
688                        chain_id: U256::from(self.chain.id()),
689                        nonce: tx_nonce + 1,
690                        address,
691                    };
692
693                    let Some(signer) = sender.as_signer() else {
694                        eyre::bail!(
695                            "No signer available to sign authorization. \
696                            Provide a pre-signed authorization (hex-encoded) instead."
697                        );
698                    };
699                    let signature = signer.sign_hash(&auth.signature_hash()).await?;
700
701                    auth.into_signed(signature)
702                }
703                CliAuthorizationList::Signed(auth) => auth,
704            };
705            signed_auths.push(signed_auth);
706        }
707
708        self.tx.set_authorization_list(signed_auths);
709
710        Ok(())
711    }
712
713    /// Fills gas price, EIP-1559 fees, and blob fees from the provider.
714    ///
715    /// Only fills values that haven't been explicitly set by the user.
716    async fn fill_fees(&mut self) -> Result<()> {
717        if self.blob && self.tx.max_fee_per_blob_gas().is_none() {
718            self.tx.set_max_fee_per_blob_gas(self.provider.get_blob_base_fee().await?)
719        }
720
721        fill_transaction_gas_fees(
722            &self.provider,
723            &mut self.tx,
724            self.legacy,
725            self.browser,
726            self.eip1559_fee_estimate,
727        )
728        .await
729    }
730
731    /// Fills gas limit from the provider.
732    async fn fill_gas_limit(&mut self) -> Result<()> {
733        if self.tx.gas_limit().is_none() {
734            self.estimate_gas().await?;
735        }
736
737        Ok(())
738    }
739
740    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
741    async fn estimate_gas(&mut self) -> Result<()> {
742        match self.provider.estimate_gas(self.tx.clone()).await {
743            Ok(estimated) => {
744                self.tx.set_gas_limit(estimated);
745                Ok(())
746            }
747            Err(err) => {
748                if let TransportError::ErrorResp(payload) = &err {
749                    // If execution reverted with code 3 during provider gas estimation then try
750                    // to decode custom errors and append it to the error message.
751                    if payload.code == 3
752                        && let Some(data) = &payload.data
753                        && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
754                    {
755                        eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error);
756                    }
757                }
758                eyre::bail!("Failed to estimate gas: {}", err);
759            }
760        }
761    }
762
763    /// Populates the blob sidecar for the transaction if any blob data was provided.
764    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
765        let Some(blob_data) = blob_data else { return Ok(self) };
766
767        let mut coder = SidecarBuilder::<SimpleCoder>::default();
768        coder.ingest(&blob_data);
769
770        if self.eip4844 {
771            let sidecar = coder.build_4844()?;
772            self.tx.set_blob_sidecar_4844(sidecar);
773        } else {
774            let sidecar = coder.build_7594()?;
775            self.tx.set_blob_sidecar_7594(sidecar);
776        }
777
778        Ok(self)
779    }
780
781    /// Skips gas, fee and nonce filling. Use for read-only calls
782    /// (eth_call, eth_estimateGas, eth_createAccessList).
783    pub const fn raw(mut self) -> Self {
784        self.fill = false;
785        self
786    }
787}
788
789/// Fills gas price or EIP-1559 fee fields from the provider and validates the final pair.
790pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
791    provider: &P,
792    tx: &mut N::TransactionRequest,
793    legacy: bool,
794    browser: bool,
795    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
796) -> Result<()>
797where
798    N::TransactionRequest: FoundryTransactionBuilder<N>,
799{
800    if legacy {
801        if tx.gas_price().is_none() {
802            tx.set_gas_price(provider.get_gas_price().await?);
803        }
804        return Ok(());
805    }
806
807    if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
808        let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;
809
810        // Only honor the browser-suggested tip when the user has not pinned a
811        // priority fee; `resolve_broadcast_eip1559_fees` ignores a lower tip.
812        let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
813            provider.get_max_priority_fee_per_gas().await.ok()
814        } else {
815            None
816        };
817
818        // User `--gas-price`/`--priority-gas-price` overrides are already applied
819        // to `tx`; pass `None` so they are not double-applied here.
820        let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
821
822        if tx.max_fee_per_gas().is_none() {
823            tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
824        }
825
826        if tx.max_priority_fee_per_gas().is_none() {
827            tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
828        }
829    }
830
831    if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
832        eyre::ensure!(
833            priority <= max_fee,
834            "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
835        );
836    }
837
838    Ok(())
839}
840
841/// Helper function that tries to decode custom error name and inputs from error payload data.
842async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
843    let err_data = serde_json::from_str::<Bytes>(data.get())?;
844    let Some(selector) = err_data.get(..4) else { return Ok(None) };
845    if let Some(known_error) =
846        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
847    {
848        let mut decoded_error = known_error.name.clone();
849        if !known_error.inputs.is_empty()
850            && let Ok(error) = known_error.decode_error(&err_data)
851        {
852            write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
853        }
854        return Ok(Some(decoded_error));
855    }
856    Ok(None)
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use alloy_network::Ethereum;
863    use alloy_provider::{ProviderBuilder, mock::Asserter};
864    use clap::Parser;
865
866    #[tokio::test]
867    async fn raw_build_skips_nonce_request() {
868        // No responses are queued, so any RPC request would fail this test. In particular, this
869        // guards against restoring the unused `eth_getTransactionCount` request.
870        let provider =
871            ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
872        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };
873
874        CastTxBuilder::new(&provider, TransactionOpts::parse_from(["test"]), &config)
875            .await
876            .unwrap()
877            .with_to(Some(Address::repeat_byte(0x11).into()))
878            .await
879            .unwrap()
880            .with_code_sig_and_args(None, None, Vec::new())
881            .await
882            .unwrap()
883            .raw()
884            .build(Address::repeat_byte(0x22))
885            .await
886            .unwrap();
887    }
888}