cast/
tx.rs

1use crate::traces::identifier::SignaturesIdentifier;
2use alloy_consensus::{SidecarBuilder, SignableTransaction, SimpleCoder};
3use alloy_dyn_abi::ErrorExt;
4use alloy_ens::NameOrAddress;
5use alloy_json_abi::Function;
6use alloy_network::{
7    AnyNetwork, TransactionBuilder, TransactionBuilder7594, TransactionBuilder7702,
8};
9use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256, hex};
10use alloy_provider::{PendingTransactionBuilder, Provider};
11use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind, TransactionRequest};
12use alloy_serde::WithOtherFields;
13use alloy_signer::Signer;
14use alloy_transport::TransportError;
15use clap::Args;
16use eyre::{Result, WrapErr};
17use foundry_cli::{
18    opts::{CliAuthorizationList, EthereumOpts, TransactionOpts},
19    utils::{self, LoadConfig, get_provider_builder, parse_function_args},
20};
21use foundry_common::{
22    TransactionReceiptWithRevertReason, fmt::*, get_pretty_tx_receipt_attr,
23    provider::RetryProviderWithSigner, shell,
24};
25use foundry_config::{Chain, Config};
26use foundry_primitives::{FoundryTransactionRequest, FoundryTypedTx};
27use foundry_wallets::{WalletOpts, WalletSigner};
28use itertools::Itertools;
29use serde_json::value::RawValue;
30use std::{fmt::Write, str::FromStr, time::Duration};
31
32#[derive(Debug, Clone, Args)]
33pub struct SendTxOpts {
34    /// Only print the transaction hash and exit immediately.
35    #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
36    pub cast_async: bool,
37
38    /// Wait for transaction receipt synchronously instead of polling.
39    /// Note: uses `eth_sendTransactionSync` which may not be 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
60/// Different sender kinds used by [`CastTxBuilder`].
61pub enum SenderKind<'a> {
62    /// An address without signer. Used for read-only calls and transactions sent through unlocked
63    /// accounts.
64    Address(Address),
65    /// A reference to a signer.
66    Signer(&'a WalletSigner),
67    /// An owned signer.
68    OwnedSigner(Box<WalletSigner>),
69}
70
71impl SenderKind<'_> {
72    /// Resolves the name to an Ethereum Address.
73    pub fn address(&self) -> Address {
74        match self {
75            Self::Address(addr) => *addr,
76            Self::Signer(signer) => signer.address(),
77            Self::OwnedSigner(signer) => signer.address(),
78        }
79    }
80
81    /// Resolves the sender from the wallet options.
82    ///
83    /// This function prefers the `from` field and may return a different address from the
84    /// configured signer
85    /// If from is specified, returns it
86    /// If from is not specified, but there is a signer configured, returns the signer's address
87    /// If from is not specified and there is no signer configured, returns zero address
88    pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
89        if let Some(from) = opts.from {
90            Ok(from.into())
91        } else if let Ok(signer) = opts.signer().await {
92            Ok(Self::OwnedSigner(Box::new(signer)))
93        } else {
94            Ok(Address::ZERO.into())
95        }
96    }
97
98    /// Returns the signer if available.
99    pub fn as_signer(&self) -> Option<&WalletSigner> {
100        match self {
101            Self::Signer(signer) => Some(signer),
102            Self::OwnedSigner(signer) => Some(signer.as_ref()),
103            _ => None,
104        }
105    }
106}
107
108impl From<Address> for SenderKind<'_> {
109    fn from(addr: Address) -> Self {
110        Self::Address(addr)
111    }
112}
113
114impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
115    fn from(signer: &'a WalletSigner) -> Self {
116        Self::Signer(signer)
117    }
118}
119
120impl From<WalletSigner> for SenderKind<'_> {
121    fn from(signer: WalletSigner) -> Self {
122        Self::OwnedSigner(Box::new(signer))
123    }
124}
125
126/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
127pub fn validate_from_address(
128    specified_from: Option<Address>,
129    signer_address: Address,
130) -> Result<()> {
131    if let Some(specified_from) = specified_from
132        && specified_from != signer_address
133    {
134        eyre::bail!(
135                "\
136The specified sender via CLI/env vars does not match the sender configured via
137the hardware wallet's HD Path.
138Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
139corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
140            )
141    }
142    Ok(())
143}
144
145/// Initial state.
146#[derive(Debug)]
147pub struct InitState;
148
149/// State with known [TxKind].
150#[derive(Debug)]
151pub struct ToState {
152    to: Option<Address>,
153}
154
155/// State with known input for the transaction.
156#[derive(Debug)]
157pub struct InputState {
158    kind: TxKind,
159    input: Vec<u8>,
160    func: Option<Function>,
161}
162
163pub struct CastTxSender<P> {
164    provider: P,
165}
166
167impl<P: Provider<AnyNetwork>> CastTxSender<P> {
168    /// Creates a new Cast instance responsible for sending transactions.
169    pub fn new(provider: P) -> Self {
170        Self { provider }
171    }
172
173    /// Sends a transaction and waits for receipt synchronously
174    pub async fn send_sync(&self, tx: WithOtherFields<TransactionRequest>) -> Result<String> {
175        let mut receipt: TransactionReceiptWithRevertReason =
176            self.provider.send_transaction_sync(tx).await?.into();
177
178        // Allow to fail silently
179        let _ = receipt.update_revert_reason(&self.provider).await;
180
181        self.format_receipt(receipt, None)
182    }
183
184    /// Sends a transaction to the specified address
185    ///
186    /// # Example
187    ///
188    /// ```
189    /// use cast::tx::CastTxSender;
190    /// use alloy_primitives::{Address, U256, Bytes};
191    /// use alloy_serde::WithOtherFields;
192    /// use alloy_rpc_types::{TransactionRequest};
193    /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork};
194    /// use std::str::FromStr;
195    /// use alloy_sol_types::{sol, SolCall};    ///
196    ///
197    /// sol!(
198    ///     function greet(string greeting) public;
199    /// );
200    ///
201    /// # async fn foo() -> eyre::Result<()> {
202    /// let provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;;
203    /// let from = Address::from_str("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")?;
204    /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?;
205    /// let greeting = greetCall { greeting: "hello".to_string() }.abi_encode();
206    /// let bytes = Bytes::from_iter(greeting.iter());
207    /// let gas = U256::from_str("200000").unwrap();
208    /// let value = U256::from_str("1").unwrap();
209    /// let nonce = U256::from_str("1").unwrap();
210    /// let tx = TransactionRequest::default().to(to).input(bytes.into()).from(from);
211    /// let tx = WithOtherFields::new(tx);
212    /// let cast = CastTxSender::new(provider);
213    /// let data = cast.send(tx).await?;
214    /// println!("{:#?}", data);
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub async fn send(
219        &self,
220        tx: WithOtherFields<TransactionRequest>,
221    ) -> Result<PendingTransactionBuilder<AnyNetwork>> {
222        let res = self.provider.send_transaction(tx).await?;
223
224        Ok(res)
225    }
226
227    /// Sends a raw RLP-encoded transaction via `eth_sendRawTransaction`.
228    ///
229    /// Used for transaction types that the standard Alloy network stack doesn't understand
230    /// (e.g., Tempo transactions).
231    pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<AnyNetwork>> {
232        let res = self.provider.send_raw_transaction(raw_tx).await?;
233        Ok(res)
234    }
235
236    /// # Example
237    ///
238    /// ```
239    /// use alloy_provider::{ProviderBuilder, RootProvider, network::AnyNetwork};
240    /// use cast::tx::CastTxSender;
241    ///
242    /// async fn foo() -> eyre::Result<()> {
243    /// let provider =
244    ///     ProviderBuilder::<_, _, AnyNetwork>::default().connect("http://localhost:8545").await?;
245    /// let cast = CastTxSender::new(provider);
246    /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc";
247    /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?;
248    /// println!("{}", receipt);
249    /// # Ok(())
250    /// # }
251    /// ```
252    pub async fn receipt(
253        &self,
254        tx_hash: String,
255        field: Option<String>,
256        confs: u64,
257        timeout: Option<u64>,
258        cast_async: bool,
259    ) -> Result<String> {
260        let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
261
262        let mut receipt: TransactionReceiptWithRevertReason =
263            match self.provider.get_transaction_receipt(tx_hash).await? {
264                Some(r) => r,
265                None => {
266                    // if the async flag is provided, immediately exit if no tx is found, otherwise
267                    // try to poll for it
268                    if cast_async {
269                        eyre::bail!("tx not found: {:?}", tx_hash)
270                    } else {
271                        PendingTransactionBuilder::new(self.provider.root().clone(), tx_hash)
272                            .with_required_confirmations(confs)
273                            .with_timeout(timeout.map(Duration::from_secs))
274                            .get_receipt()
275                            .await?
276                    }
277                }
278            }
279            .into();
280
281        // Allow to fail silently
282        let _ = receipt.update_revert_reason(&self.provider).await;
283
284        self.format_receipt(receipt, field)
285    }
286
287    /// Helper method to format transaction receipts consistently
288    fn format_receipt(
289        &self,
290        receipt: TransactionReceiptWithRevertReason,
291        field: Option<String>,
292    ) -> Result<String> {
293        Ok(if let Some(ref field) = field {
294            get_pretty_tx_receipt_attr(&receipt, field)
295                .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
296        } else if shell::is_json() {
297            // to_value first to sort json object keys
298            serde_json::to_value(&receipt)?.to_string()
299        } else {
300            receipt.pretty()
301        })
302    }
303}
304
305/// Builder type constructing [TransactionRequest] from cast send/mktx inputs.
306///
307/// It is implemented as a stateful builder with expected state transition of [InitState] ->
308/// [ToState] -> [InputState].
309#[derive(Debug)]
310pub struct CastTxBuilder<P, S> {
311    provider: P,
312    tx: WithOtherFields<TransactionRequest>,
313    /// Whether the transaction should be sent as a legacy transaction.
314    legacy: bool,
315    blob: bool,
316    /// Whether the blob transaction should use EIP-4844 (legacy) format instead of EIP-7594.
317    eip4844: bool,
318    auth: Vec<CliAuthorizationList>,
319    chain: Chain,
320    etherscan_api_key: Option<String>,
321    access_list: Option<Option<AccessList>>,
322    state: S,
323}
324
325impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
326    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
327    /// provided [TransactionOpts].
328    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
329        let mut tx = WithOtherFields::<TransactionRequest>::default();
330
331        let chain = utils::get_chain(config.chain, &provider).await?;
332        let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
333        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
334        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
335
336        if let Some(gas_limit) = tx_opts.gas_limit {
337            tx.set_gas_limit(gas_limit.to());
338        }
339
340        if let Some(value) = tx_opts.value {
341            tx.set_value(value);
342        }
343
344        if let Some(gas_price) = tx_opts.gas_price {
345            if legacy {
346                tx.set_gas_price(gas_price.to());
347            } else {
348                tx.set_max_fee_per_gas(gas_price.to());
349            }
350        }
351
352        if !legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
353            tx.set_max_priority_fee_per_gas(priority_fee.to());
354        }
355
356        if let Some(max_blob_fee) = tx_opts.blob_gas_price {
357            tx.set_max_fee_per_blob_gas(max_blob_fee.to())
358        }
359
360        if let Some(nonce) = tx_opts.nonce {
361            tx.set_nonce(nonce.to());
362        }
363
364        // Set Tempo fee token if provided
365        if let Some(fee_token) = tx_opts.tempo.fee_token {
366            tx.other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
367        }
368
369        if let Some(nonce_key) = tx_opts.tempo.sequence_key {
370            tx.other.insert("nonceKey".to_string(), serde_json::to_value(nonce_key).unwrap());
371        }
372
373        Ok(Self {
374            provider,
375            tx,
376            legacy,
377            blob: tx_opts.blob,
378            eip4844: tx_opts.eip4844,
379            chain,
380            etherscan_api_key,
381            auth: tx_opts.auth,
382            access_list: tx_opts.access_list,
383            state: InitState,
384        })
385    }
386
387    /// Sets [TxKind] for this builder and changes state to [ToState].
388    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
389        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
390        Ok(CastTxBuilder {
391            provider: self.provider,
392            tx: self.tx,
393            legacy: self.legacy,
394            blob: self.blob,
395            eip4844: self.eip4844,
396            chain: self.chain,
397            etherscan_api_key: self.etherscan_api_key,
398            auth: self.auth,
399            access_list: self.access_list,
400            state: ToState { to },
401        })
402    }
403}
404
405impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
406    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
407    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
408    /// present, input is set to just provided arguments.
409    pub async fn with_code_sig_and_args(
410        self,
411        code: Option<String>,
412        sig: Option<String>,
413        args: Vec<String>,
414    ) -> Result<CastTxBuilder<P, InputState>> {
415        let (mut args, func) = if let Some(sig) = sig {
416            parse_function_args(
417                &sig,
418                args,
419                self.state.to,
420                self.chain,
421                &self.provider,
422                self.etherscan_api_key.as_deref(),
423            )
424            .await?
425        } else {
426            (Vec::new(), None)
427        };
428
429        let input = if let Some(code) = &code {
430            let mut code = hex::decode(code)?;
431            code.append(&mut args);
432            code
433        } else {
434            args
435        };
436
437        if self.state.to.is_none() && code.is_none() {
438            let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
439            let has_auth = !self.auth.is_empty();
440            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
441            // without a value.
442            if !has_auth || has_value {
443                eyre::bail!("Must specify a recipient address or contract code to deploy");
444            }
445        }
446
447        Ok(CastTxBuilder {
448            provider: self.provider,
449            tx: self.tx,
450            legacy: self.legacy,
451            blob: self.blob,
452            eip4844: self.eip4844,
453            chain: self.chain,
454            etherscan_api_key: self.etherscan_api_key,
455            auth: self.auth,
456            access_list: self.access_list,
457            state: InputState { kind: self.state.to.into(), input, func },
458        })
459    }
460}
461
462impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
463    /// Builds a [FoundryTransactionRequest] and fills missing fields. Returns a transaction which
464    /// is ready to be broadcasted.
465    pub async fn build(
466        self,
467        sender: impl Into<SenderKind<'_>>,
468    ) -> Result<(FoundryTransactionRequest, Option<Function>)> {
469        let (tx, func) = self._build(sender, true, false).await?;
470        Ok((FoundryTransactionRequest::new(tx), func))
471    }
472
473    /// Builds [TransactionRequest] without filling missing fields. Used for read-only calls such as
474    /// eth_call, eth_estimateGas, etc
475    pub async fn build_raw(
476        self,
477        sender: impl Into<SenderKind<'_>>,
478    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
479        self._build(sender, false, false).await
480    }
481
482    /// Builds an unsigned RLP-encoded raw transaction.
483    ///
484    /// Returns the hex encoded string representation of the transaction.
485    pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
486        let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
487        let ftx = FoundryTransactionRequest::new(tx);
488
489        let tx = ftx.build_unsigned()?;
490        Ok(hex::encode_prefixed(match tx {
491            FoundryTypedTx::Legacy(t) => t.encoded_for_signing(),
492            FoundryTypedTx::Eip1559(t) => t.encoded_for_signing(),
493            FoundryTypedTx::Eip2930(t) => t.encoded_for_signing(),
494            FoundryTypedTx::Eip4844(t) => t.encoded_for_signing(),
495            FoundryTypedTx::Eip7702(t) => t.encoded_for_signing(),
496            FoundryTypedTx::Tempo(t) => t.encoded_for_signing(),
497            _ => eyre::bail!(
498                "Cannot generate unsigned transaction for transaction: unknown transaction type"
499            ),
500        }))
501    }
502
503    /// Returns whether this builder will produce a Tempo transaction.
504    pub fn is_tempo(&self) -> bool {
505        // TODO: Replace this with `FoundryTransactionRequest::is_tempo`
506        self.tx.other.contains_key("feeToken") || self.tx.other.contains_key("nonceKey")
507    }
508
509    async fn _build(
510        mut self,
511        sender: impl Into<SenderKind<'_>>,
512        fill: bool,
513        unsigned: bool,
514    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
515        let sender = sender.into();
516        let from = sender.address();
517
518        self.tx.set_kind(self.state.kind);
519
520        // we set both fields to the same value because some nodes only accept the legacy `data` field: <https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249>
521        self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
522
523        self.tx.set_from(from);
524        self.tx.set_chain_id(self.chain.id());
525
526        let tx_nonce = if let Some(nonce) = self.tx.nonce {
527            nonce
528        } else {
529            let nonce = self.provider.get_transaction_count(from).await?;
530            if fill {
531                self.tx.nonce = Some(nonce);
532            }
533            nonce
534        };
535
536        if !unsigned {
537            self.resolve_auth(sender, tx_nonce).await?;
538        } else if !self.auth.is_empty() {
539            let mut signed_auths = Vec::with_capacity(self.auth.len());
540            for auth in std::mem::take(&mut self.auth) {
541                let CliAuthorizationList::Signed(signed_auth) = auth else {
542                    eyre::bail!(
543                        "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
544                    )
545                };
546                signed_auths.push(signed_auth);
547            }
548
549            self.tx.set_authorization_list(signed_auths);
550        }
551
552        if let Some(access_list) = match self.access_list.take() {
553            None => None,
554            // --access-list provided with no value, call the provider to create it
555            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
556            // Access list provided as a string, attempt to parse it
557            Some(Some(access_list)) => Some(access_list),
558        } {
559            self.tx.set_access_list(access_list);
560        }
561
562        if !fill {
563            return Ok((self.tx, self.state.func));
564        }
565
566        if self.legacy && self.tx.gas_price.is_none() {
567            self.tx.gas_price = Some(self.provider.get_gas_price().await?);
568        }
569
570        if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
571            self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
572        }
573
574        if !self.legacy
575            && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
576        {
577            let estimate = self.provider.estimate_eip1559_fees().await?;
578
579            if self.tx.max_fee_per_gas.is_none() {
580                self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
581            }
582
583            if self.tx.max_priority_fee_per_gas.is_none() {
584                self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
585            }
586        }
587
588        if self.tx.gas.is_none() {
589            self.estimate_gas().await?;
590        }
591
592        Ok((self.tx, self.state.func))
593    }
594
595    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
596    async fn estimate_gas(&mut self) -> Result<()> {
597        match self.provider.estimate_gas(self.tx.clone()).await {
598            Ok(estimated) => {
599                self.tx.gas = Some(estimated);
600                Ok(())
601            }
602            Err(err) => {
603                if let TransportError::ErrorResp(payload) = &err {
604                    // If execution reverted with code 3 during provider gas estimation then try
605                    // to decode custom errors and append it to the error message.
606                    if payload.code == 3
607                        && let Some(data) = &payload.data
608                        && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
609                    {
610                        eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
611                    }
612                }
613                eyre::bail!("Failed to estimate gas: {}", err)
614            }
615        }
616    }
617
618    /// Parses the passed --auth values and sets the authorization list on the transaction.
619    async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
620        if self.auth.is_empty() {
621            return Ok(());
622        }
623
624        let auths = std::mem::take(&mut self.auth);
625
626        // Validate that at most one address-based auth is provided (multiple addresses are
627        // almost always unintended).
628        let address_auth_count =
629            auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
630        if address_auth_count > 1 {
631            eyre::bail!(
632                "Multiple address-based authorizations provided. Only one address can be specified; \
633                use pre-signed authorizations (hex-encoded) for multiple authorizations."
634            );
635        }
636
637        let mut signed_auths = Vec::with_capacity(auths.len());
638
639        for auth in auths {
640            let signed_auth = match auth {
641                CliAuthorizationList::Address(address) => {
642                    let auth = Authorization {
643                        chain_id: U256::from(self.chain.id()),
644                        nonce: tx_nonce + 1,
645                        address,
646                    };
647
648                    let Some(signer) = sender.as_signer() else {
649                        eyre::bail!("No signer available to sign authorization");
650                    };
651                    let signature = signer.sign_hash(&auth.signature_hash()).await?;
652
653                    auth.into_signed(signature)
654                }
655                CliAuthorizationList::Signed(auth) => auth,
656            };
657            signed_auths.push(signed_auth);
658        }
659
660        self.tx.set_authorization_list(signed_auths);
661
662        Ok(())
663    }
664}
665
666impl<P, S> CastTxBuilder<P, S>
667where
668    P: Provider<AnyNetwork>,
669{
670    /// Populates the blob sidecar for the transaction if any blob data was provided.
671    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
672        let Some(blob_data) = blob_data else { return Ok(self) };
673
674        let mut coder = SidecarBuilder::<SimpleCoder>::default();
675        coder.ingest(&blob_data);
676
677        if self.eip4844 {
678            let sidecar = coder.build()?;
679            alloy_network::TransactionBuilder4844::set_blob_sidecar(&mut self.tx, sidecar);
680        } else {
681            let sidecar = coder.build_7594()?;
682            self.tx.set_blob_sidecar_7594(sidecar);
683        }
684
685        self.tx.populate_blob_hashes();
686
687        Ok(self)
688    }
689}
690
691/// Helper function that tries to decode custom error name and inputs from error payload data.
692async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
693    let err_data = serde_json::from_str::<Bytes>(data.get())?;
694    let Some(selector) = err_data.get(..4) else { return Ok(None) };
695    if let Some(known_error) =
696        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
697    {
698        let mut decoded_error = known_error.name.clone();
699        if !known_error.inputs.is_empty()
700            && let Ok(error) = known_error.decode_error(&err_data)
701        {
702            write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
703        }
704        return Ok(Some(decoded_error));
705    }
706    Ok(None)
707}
708
709/// Creates a provider with wallet for signing transactions locally.
710///
711/// If `curl_mode` is true, the provider will print equivalent curl commands to stdout
712/// instead of executing RPC requests.
713pub(crate) async fn signing_provider_with_curl(
714    tx_opts: &SendTxOpts,
715    curl_mode: bool,
716) -> eyre::Result<RetryProviderWithSigner> {
717    let config = tx_opts.eth.load_config()?;
718    let signer = tx_opts.eth.wallet.signer().await?;
719    let wallet = alloy_network::EthereumWallet::from(signer);
720    let provider = get_provider_builder(&config, curl_mode)?.build_with_wallet(wallet)?;
721    if let Some(interval) = tx_opts.poll_interval {
722        provider.client().set_poll_interval(Duration::from_secs(interval))
723    }
724    Ok(provider)
725}