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