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, AnyTypedTransaction, TransactionBuilder, TransactionBuilder4844,
8    TransactionBuilder7702,
9};
10use alloy_primitives::{hex, Address, Bytes, TxKind, U256};
11use alloy_provider::Provider;
12use alloy_rpc_types::{AccessList, Authorization, TransactionInput, TransactionRequest};
13use alloy_serde::WithOtherFields;
14use alloy_signer::Signer;
15use alloy_transport::TransportError;
16use eyre::Result;
17use foundry_block_explorers::EtherscanApiVersion;
18use foundry_cli::{
19    opts::{CliAuthorizationList, TransactionOpts},
20    utils::{self, parse_function_args},
21};
22use foundry_common::fmt::format_tokens;
23use foundry_config::{Chain, Config};
24use foundry_wallets::{WalletOpts, WalletSigner};
25use itertools::Itertools;
26use serde_json::value::RawValue;
27use std::fmt::Write;
28
29/// Different sender kinds used by [`CastTxBuilder`].
30#[expect(clippy::large_enum_variant)]
31pub enum SenderKind<'a> {
32    /// An address without signer. Used for read-only calls and transactions sent through unlocked
33    /// accounts.
34    Address(Address),
35    /// A reference to a signer.
36    Signer(&'a WalletSigner),
37    /// An owned signer.
38    OwnedSigner(WalletSigner),
39}
40
41impl SenderKind<'_> {
42    /// Resolves the name to an Ethereum Address.
43    pub fn address(&self) -> Address {
44        match self {
45            Self::Address(addr) => *addr,
46            Self::Signer(signer) => signer.address(),
47            Self::OwnedSigner(signer) => signer.address(),
48        }
49    }
50
51    /// Resolves the sender from the wallet options.
52    ///
53    /// This function prefers the `from` field and may return a different address from the
54    /// configured signer
55    /// If from is specified, returns it
56    /// If from is not specified, but there is a signer configured, returns the signer's address
57    /// If from is not specified and there is no signer configured, returns zero address
58    pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
59        if let Some(from) = opts.from {
60            Ok(from.into())
61        } else if let Ok(signer) = opts.signer().await {
62            Ok(Self::OwnedSigner(signer))
63        } else {
64            Ok(Address::ZERO.into())
65        }
66    }
67
68    /// Returns the signer if available.
69    pub fn as_signer(&self) -> Option<&WalletSigner> {
70        match self {
71            Self::Signer(signer) => Some(signer),
72            Self::OwnedSigner(signer) => Some(signer),
73            _ => None,
74        }
75    }
76}
77
78impl From<Address> for SenderKind<'_> {
79    fn from(addr: Address) -> Self {
80        Self::Address(addr)
81    }
82}
83
84impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
85    fn from(signer: &'a WalletSigner) -> Self {
86        Self::Signer(signer)
87    }
88}
89
90impl From<WalletSigner> for SenderKind<'_> {
91    fn from(signer: WalletSigner) -> Self {
92        Self::OwnedSigner(signer)
93    }
94}
95
96/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
97pub fn validate_from_address(
98    specified_from: Option<Address>,
99    signer_address: Address,
100) -> Result<()> {
101    if let Some(specified_from) = specified_from {
102        if specified_from != signer_address {
103            eyre::bail!(
104                "\
105The specified sender via CLI/env vars does not match the sender configured via
106the hardware wallet's HD Path.
107Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
108corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
109            )
110        }
111    }
112    Ok(())
113}
114
115/// Initial state.
116#[derive(Debug)]
117pub struct InitState;
118
119/// State with known [TxKind].
120#[derive(Debug)]
121pub struct ToState {
122    to: Option<Address>,
123}
124
125/// State with known input for the transaction.
126#[derive(Debug)]
127pub struct InputState {
128    kind: TxKind,
129    input: Vec<u8>,
130    func: Option<Function>,
131}
132
133/// Builder type constructing [TransactionRequest] from cast send/mktx inputs.
134///
135/// It is implemented as a stateful builder with expected state transition of [InitState] ->
136/// [ToState] -> [InputState].
137#[derive(Debug)]
138pub struct CastTxBuilder<P, S> {
139    provider: P,
140    tx: WithOtherFields<TransactionRequest>,
141    /// Whether the transaction should be sent as a legacy transaction.
142    legacy: bool,
143    blob: bool,
144    auth: Option<CliAuthorizationList>,
145    chain: Chain,
146    etherscan_api_key: Option<String>,
147    etherscan_api_version: EtherscanApiVersion,
148    access_list: Option<Option<AccessList>>,
149    state: S,
150}
151
152impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
153    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
154    /// provided [TransactionOpts].
155    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
156        let mut tx = WithOtherFields::<TransactionRequest>::default();
157
158        let chain = utils::get_chain(config.chain, &provider).await?;
159        let etherscan_api_version = config.get_etherscan_api_version(Some(chain));
160        let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
161        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
162        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_none());
163
164        if let Some(gas_limit) = tx_opts.gas_limit {
165            tx.set_gas_limit(gas_limit.to());
166        }
167
168        if let Some(value) = tx_opts.value {
169            tx.set_value(value);
170        }
171
172        if let Some(gas_price) = tx_opts.gas_price {
173            if legacy {
174                tx.set_gas_price(gas_price.to());
175            } else {
176                tx.set_max_fee_per_gas(gas_price.to());
177            }
178        }
179
180        if !legacy {
181            if let Some(priority_fee) = tx_opts.priority_gas_price {
182                tx.set_max_priority_fee_per_gas(priority_fee.to());
183            }
184        }
185
186        if let Some(max_blob_fee) = tx_opts.blob_gas_price {
187            tx.set_max_fee_per_blob_gas(max_blob_fee.to())
188        }
189
190        if let Some(nonce) = tx_opts.nonce {
191            tx.set_nonce(nonce.to());
192        }
193
194        Ok(Self {
195            provider,
196            tx,
197            legacy,
198            blob: tx_opts.blob,
199            chain,
200            etherscan_api_key,
201            etherscan_api_version,
202            auth: tx_opts.auth,
203            access_list: tx_opts.access_list,
204            state: InitState,
205        })
206    }
207
208    /// Sets [TxKind] for this builder and changes state to [ToState].
209    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
210        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
211        Ok(CastTxBuilder {
212            provider: self.provider,
213            tx: self.tx,
214            legacy: self.legacy,
215            blob: self.blob,
216            chain: self.chain,
217            etherscan_api_key: self.etherscan_api_key,
218            etherscan_api_version: self.etherscan_api_version,
219            auth: self.auth,
220            access_list: self.access_list,
221            state: ToState { to },
222        })
223    }
224}
225
226impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
227    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
228    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
229    /// present, input is set to just provided arguments.
230    pub async fn with_code_sig_and_args(
231        self,
232        code: Option<String>,
233        sig: Option<String>,
234        args: Vec<String>,
235    ) -> Result<CastTxBuilder<P, InputState>> {
236        let (mut args, func) = if let Some(sig) = sig {
237            parse_function_args(
238                &sig,
239                args,
240                self.state.to,
241                self.chain,
242                &self.provider,
243                self.etherscan_api_key.as_deref(),
244                self.etherscan_api_version,
245            )
246            .await?
247        } else {
248            (Vec::new(), None)
249        };
250
251        let input = if let Some(code) = &code {
252            let mut code = hex::decode(code)?;
253            code.append(&mut args);
254            code
255        } else {
256            args
257        };
258
259        if self.state.to.is_none() && code.is_none() {
260            let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
261            let has_auth = self.auth.is_some();
262            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
263            // without a value.
264            if !has_auth || has_value {
265                eyre::bail!("Must specify a recipient address or contract code to deploy");
266            }
267        }
268
269        Ok(CastTxBuilder {
270            provider: self.provider,
271            tx: self.tx,
272            legacy: self.legacy,
273            blob: self.blob,
274            chain: self.chain,
275            etherscan_api_key: self.etherscan_api_key,
276            etherscan_api_version: self.etherscan_api_version,
277            auth: self.auth,
278            access_list: self.access_list,
279            state: InputState { kind: self.state.to.into(), input, func },
280        })
281    }
282}
283
284impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
285    /// Builds [TransactionRequest] and fiils missing fields. Returns a transaction which is ready
286    /// to be broadcasted.
287    pub async fn build(
288        self,
289        sender: impl Into<SenderKind<'_>>,
290    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
291        self._build(sender, true, false).await
292    }
293
294    /// Builds [TransactionRequest] without filling missing fields. Used for read-only calls such as
295    /// eth_call, eth_estimateGas, etc
296    pub async fn build_raw(
297        self,
298        sender: impl Into<SenderKind<'_>>,
299    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
300        self._build(sender, false, false).await
301    }
302
303    /// Builds an unsigned RLP-encoded raw transaction.
304    ///
305    /// Returns the hex encoded string representation of the transaction.
306    pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
307        let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
308        let tx = tx.build_unsigned()?;
309        match tx {
310            AnyTypedTransaction::Ethereum(t) => Ok(hex::encode_prefixed(t.encoded_for_signing())),
311            _ => eyre::bail!("Cannot generate unsigned transaction for non-Ethereum transactions"),
312        }
313    }
314
315    async fn _build(
316        mut self,
317        sender: impl Into<SenderKind<'_>>,
318        fill: bool,
319        unsigned: bool,
320    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
321        let sender = sender.into();
322        let from = sender.address();
323
324        self.tx.set_kind(self.state.kind);
325
326        // 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>
327        let input = Bytes::copy_from_slice(&self.state.input);
328        self.tx.input = TransactionInput { input: Some(input.clone()), data: Some(input) };
329
330        self.tx.set_from(from);
331        self.tx.set_chain_id(self.chain.id());
332
333        let tx_nonce = if let Some(nonce) = self.tx.nonce {
334            nonce
335        } else {
336            let nonce = self.provider.get_transaction_count(from).await?;
337            if fill {
338                self.tx.nonce = Some(nonce);
339            }
340            nonce
341        };
342
343        if !unsigned {
344            self.resolve_auth(sender, tx_nonce).await?;
345        } else if self.auth.is_some() {
346            let Some(CliAuthorizationList::Signed(signed_auth)) = self.auth.take() else {
347                eyre::bail!(
348                    "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
349                )
350            };
351
352            self.tx.set_authorization_list(vec![signed_auth]);
353        }
354
355        if let Some(access_list) = match self.access_list.take() {
356            None => None,
357            // --access-list provided with no value, call the provider to create it
358            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
359            // Access list provided as a string, attempt to parse it
360            Some(Some(access_list)) => Some(access_list),
361        } {
362            self.tx.set_access_list(access_list);
363        }
364
365        if !fill {
366            return Ok((self.tx, self.state.func));
367        }
368
369        if self.legacy && self.tx.gas_price.is_none() {
370            self.tx.gas_price = Some(self.provider.get_gas_price().await?);
371        }
372
373        if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
374            self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
375        }
376
377        if !self.legacy &&
378            (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
379        {
380            let estimate = self.provider.estimate_eip1559_fees().await?;
381
382            if !self.legacy {
383                if self.tx.max_fee_per_gas.is_none() {
384                    self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
385                }
386
387                if self.tx.max_priority_fee_per_gas.is_none() {
388                    self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
389                }
390            }
391        }
392
393        if self.tx.gas.is_none() {
394            self.estimate_gas().await?;
395        }
396
397        Ok((self.tx, self.state.func))
398    }
399
400    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
401    async fn estimate_gas(&mut self) -> Result<()> {
402        match self.provider.estimate_gas(self.tx.clone()).await {
403            Ok(estimated) => {
404                self.tx.gas = Some(estimated);
405                Ok(())
406            }
407            Err(err) => {
408                if let TransportError::ErrorResp(payload) = &err {
409                    // If execution reverted with code 3 during provider gas estimation then try
410                    // to decode custom errors and append it to the error message.
411                    if payload.code == 3 {
412                        if let Some(data) = &payload.data {
413                            if let Ok(Some(decoded_error)) = decode_execution_revert(data).await {
414                                eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
415                            }
416                        }
417                    }
418                }
419                eyre::bail!("Failed to estimate gas: {}", err)
420            }
421        }
422    }
423
424    /// Parses the passed --auth value and sets the authorization list on the transaction.
425    async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
426        let Some(auth) = self.auth.take() else { return Ok(()) };
427
428        let auth = match auth {
429            CliAuthorizationList::Address(address) => {
430                let auth = Authorization {
431                    chain_id: U256::from(self.chain.id()),
432                    nonce: tx_nonce + 1,
433                    address,
434                };
435
436                let Some(signer) = sender.as_signer() else {
437                    eyre::bail!("No signer available to sign authorization");
438                };
439                let signature = signer.sign_hash(&auth.signature_hash()).await?;
440
441                auth.into_signed(signature)
442            }
443            CliAuthorizationList::Signed(auth) => auth,
444        };
445
446        self.tx.set_authorization_list(vec![auth]);
447
448        Ok(())
449    }
450}
451
452impl<P, S> CastTxBuilder<P, S>
453where
454    P: Provider<AnyNetwork>,
455{
456    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
457        let Some(blob_data) = blob_data else { return Ok(self) };
458
459        let mut coder = SidecarBuilder::<SimpleCoder>::default();
460        coder.ingest(&blob_data);
461        let sidecar = coder.build()?;
462
463        self.tx.set_blob_sidecar(sidecar);
464        self.tx.populate_blob_hashes();
465
466        Ok(self)
467    }
468}
469
470/// Helper function that tries to decode custom error name and inputs from error payload data.
471async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
472    let err_data = serde_json::from_str::<Bytes>(data.get())?;
473    let Some(selector) = err_data.get(..4) else { return Ok(None) };
474    if let Some(known_error) =
475        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
476    {
477        let mut decoded_error = known_error.name.clone();
478        if !known_error.inputs.is_empty() {
479            if let Ok(error) = known_error.decode_error(&err_data) {
480                write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
481            }
482        }
483        return Ok(Some(decoded_error))
484    }
485    Ok(None)
486}