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#[expect(clippy::large_enum_variant)]
31pub enum SenderKind<'a> {
32 Address(Address),
35 Signer(&'a WalletSigner),
37 OwnedSigner(WalletSigner),
39}
40
41impl SenderKind<'_> {
42 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 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 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
96pub 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#[derive(Debug)]
117pub struct InitState;
118
119#[derive(Debug)]
121pub struct ToState {
122 to: Option<Address>,
123}
124
125#[derive(Debug)]
127pub struct InputState {
128 kind: TxKind,
129 input: Vec<u8>,
130 func: Option<Function>,
131}
132
133#[derive(Debug)]
138pub struct CastTxBuilder<P, S> {
139 provider: P,
140 tx: WithOtherFields<TransactionRequest>,
141 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 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 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 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 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 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 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 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 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 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 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
359 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 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 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 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
470async 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}