Skip to main content

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_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#[allow(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        && specified_from != signer_address
103    {
104        eyre::bail!(
105                "\
106The specified sender via CLI/env vars does not match the sender configured via
107the hardware wallet's HD Path.
108Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
109corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
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 && let Some(priority_fee) = tx_opts.priority_gas_price {
181            tx.set_max_priority_fee_per_gas(priority_fee.to());
182        }
183
184        if let Some(max_blob_fee) = tx_opts.blob_gas_price {
185            tx.set_max_fee_per_blob_gas(max_blob_fee.to())
186        }
187
188        if let Some(nonce) = tx_opts.nonce {
189            tx.set_nonce(nonce.to());
190        }
191
192        Ok(Self {
193            provider,
194            tx,
195            legacy,
196            blob: tx_opts.blob,
197            chain,
198            etherscan_api_key,
199            etherscan_api_version,
200            auth: tx_opts.auth,
201            access_list: tx_opts.access_list,
202            state: InitState,
203        })
204    }
205
206    /// Sets [TxKind] for this builder and changes state to [ToState].
207    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
208        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
209        Ok(CastTxBuilder {
210            provider: self.provider,
211            tx: self.tx,
212            legacy: self.legacy,
213            blob: self.blob,
214            chain: self.chain,
215            etherscan_api_key: self.etherscan_api_key,
216            etherscan_api_version: self.etherscan_api_version,
217            auth: self.auth,
218            access_list: self.access_list,
219            state: ToState { to },
220        })
221    }
222}
223
224impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
225    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
226    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
227    /// present, input is set to just provided arguments.
228    pub async fn with_code_sig_and_args(
229        self,
230        code: Option<String>,
231        sig: Option<String>,
232        args: Vec<String>,
233    ) -> Result<CastTxBuilder<P, InputState>> {
234        let (mut args, func) = if let Some(sig) = sig {
235            parse_function_args(
236                &sig,
237                args,
238                self.state.to,
239                self.chain,
240                &self.provider,
241                self.etherscan_api_key.as_deref(),
242                self.etherscan_api_version,
243            )
244            .await?
245        } else {
246            (Vec::new(), None)
247        };
248
249        let input = if let Some(code) = &code {
250            let mut code = hex::decode(code)?;
251            code.append(&mut args);
252            code
253        } else {
254            args
255        };
256
257        if self.state.to.is_none() && code.is_none() {
258            let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
259            let has_auth = self.auth.is_some();
260            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
261            // without a value.
262            if !has_auth || has_value {
263                eyre::bail!("Must specify a recipient address or contract code to deploy");
264            }
265        }
266
267        Ok(CastTxBuilder {
268            provider: self.provider,
269            tx: self.tx,
270            legacy: self.legacy,
271            blob: self.blob,
272            chain: self.chain,
273            etherscan_api_key: self.etherscan_api_key,
274            etherscan_api_version: self.etherscan_api_version,
275            auth: self.auth,
276            access_list: self.access_list,
277            state: InputState { kind: self.state.to.into(), input, func },
278        })
279    }
280}
281
282impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
283    /// Builds [TransactionRequest] and fiils missing fields. Returns a transaction which is ready
284    /// to be broadcasted.
285    pub async fn build(
286        self,
287        sender: impl Into<SenderKind<'_>>,
288    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
289        self._build(sender, true, false).await
290    }
291
292    /// Builds [TransactionRequest] without filling missing fields. Used for read-only calls such as
293    /// eth_call, eth_estimateGas, etc
294    pub async fn build_raw(
295        self,
296        sender: impl Into<SenderKind<'_>>,
297    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
298        self._build(sender, false, false).await
299    }
300
301    /// Builds an unsigned RLP-encoded raw transaction.
302    ///
303    /// Returns the hex encoded string representation of the transaction.
304    pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
305        let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
306        let tx = tx.build_unsigned()?;
307        match tx {
308            AnyTypedTransaction::Ethereum(t) => Ok(hex::encode_prefixed(t.encoded_for_signing())),
309            _ => eyre::bail!("Cannot generate unsigned transaction for non-Ethereum transactions"),
310        }
311    }
312
313    async fn _build(
314        mut self,
315        sender: impl Into<SenderKind<'_>>,
316        fill: bool,
317        unsigned: bool,
318    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
319        let sender = sender.into();
320        let from = sender.address();
321
322        self.tx.set_kind(self.state.kind);
323
324        // 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>
325        let input = Bytes::copy_from_slice(&self.state.input);
326        self.tx.input = TransactionInput { input: Some(input.clone()), data: Some(input) };
327
328        self.tx.set_from(from);
329        self.tx.set_chain_id(self.chain.id());
330
331        let tx_nonce = if let Some(nonce) = self.tx.nonce {
332            nonce
333        } else {
334            let nonce = self.provider.get_transaction_count(from).await?;
335            if fill {
336                self.tx.nonce = Some(nonce);
337            }
338            nonce
339        };
340
341        if !unsigned {
342            self.resolve_auth(sender, tx_nonce).await?;
343        } else if self.auth.is_some() {
344            let Some(CliAuthorizationList::Signed(signed_auth)) = self.auth.take() else {
345                eyre::bail!(
346                    "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
347                )
348            };
349
350            self.tx.set_authorization_list(vec![signed_auth]);
351        }
352
353        if let Some(access_list) = match self.access_list.take() {
354            None => None,
355            // --access-list provided with no value, call the provider to create it
356            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
357            // Access list provided as a string, attempt to parse it
358            Some(Some(access_list)) => Some(access_list),
359        } {
360            self.tx.set_access_list(access_list);
361        }
362
363        if !fill {
364            return Ok((self.tx, self.state.func));
365        }
366
367        if self.legacy && self.tx.gas_price.is_none() {
368            self.tx.gas_price = Some(self.provider.get_gas_price().await?);
369        }
370
371        if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
372            self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
373        }
374
375        if !self.legacy
376            && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
377        {
378            let estimate = self.provider.estimate_eip1559_fees().await?;
379
380            if !self.legacy {
381                if self.tx.max_fee_per_gas.is_none() {
382                    self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
383                }
384
385                if self.tx.max_priority_fee_per_gas.is_none() {
386                    self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
387                }
388            }
389        }
390
391        if self.tx.gas.is_none() {
392            self.estimate_gas().await?;
393        }
394
395        Ok((self.tx, self.state.func))
396    }
397
398    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
399    async fn estimate_gas(&mut self) -> Result<()> {
400        match self.provider.estimate_gas(self.tx.clone()).await {
401            Ok(estimated) => {
402                self.tx.gas = Some(estimated);
403                Ok(())
404            }
405            Err(err) => {
406                if let TransportError::ErrorResp(payload) = &err {
407                    // If execution reverted with code 3 during provider gas estimation then try
408                    // to decode custom errors and append it to the error message.
409                    if payload.code == 3
410                        && let Some(data) = &payload.data
411                        && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
412                    {
413                        eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
414                    }
415                }
416                eyre::bail!("Failed to estimate gas: {}", err)
417            }
418        }
419    }
420
421    /// Parses the passed --auth value and sets the authorization list on the transaction.
422    async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
423        let Some(auth) = self.auth.take() else { return Ok(()) };
424
425        let auth = match auth {
426            CliAuthorizationList::Address(address) => {
427                let auth = Authorization {
428                    chain_id: U256::from(self.chain.id()),
429                    nonce: tx_nonce + 1,
430                    address,
431                };
432
433                let Some(signer) = sender.as_signer() else {
434                    eyre::bail!("No signer available to sign authorization");
435                };
436                let signature = signer.sign_hash(&auth.signature_hash()).await?;
437
438                auth.into_signed(signature)
439            }
440            CliAuthorizationList::Signed(auth) => auth,
441        };
442
443        self.tx.set_authorization_list(vec![auth]);
444
445        Ok(())
446    }
447}
448
449impl<P, S> CastTxBuilder<P, S>
450where
451    P: Provider<AnyNetwork>,
452{
453    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
454        let Some(blob_data) = blob_data else { return Ok(self) };
455
456        let mut coder = SidecarBuilder::<SimpleCoder>::default();
457        coder.ingest(&blob_data);
458        let sidecar = coder.build()?;
459
460        self.tx.set_blob_sidecar(sidecar);
461        self.tx.populate_blob_hashes();
462
463        Ok(self)
464    }
465}
466
467/// Helper function that tries to decode custom error name and inputs from error payload data.
468async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
469    let err_data = serde_json::from_str::<Bytes>(data.get())?;
470    let Some(selector) = err_data.get(..4) else { return Ok(None) };
471    if let Some(known_error) =
472        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
473    {
474        let mut decoded_error = known_error.name.clone();
475        if !known_error.inputs.is_empty()
476            && let Ok(error) = known_error.decode_error(&err_data)
477        {
478            write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
479        }
480        return Ok(Some(decoded_error));
481    }
482    Ok(None)
483}