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#[expect(clippy::large_enum_variant)]
30pub enum SenderKind<'a> {
31 Address(Address),
34 Signer(&'a WalletSigner),
36 OwnedSigner(WalletSigner),
38}
39
40impl SenderKind<'_> {
41 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 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 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
95pub 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#[derive(Debug)]
116pub struct InitState;
117
118#[derive(Debug)]
120pub struct ToState {
121 to: Option<Address>,
122}
123
124#[derive(Debug)]
126pub struct InputState {
127 kind: TxKind,
128 input: Vec<u8>,
129 func: Option<Function>,
130}
131
132#[derive(Debug)]
137pub struct CastTxBuilder<P, S> {
138 provider: P,
139 tx: WithOtherFields<TransactionRequest>,
140 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 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 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 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 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 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 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 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 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 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 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
350 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 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 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 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
460async 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}