cast/
tx.rs

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