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