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