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#[allow(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 && 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#[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 && 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 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 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 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 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 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 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 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 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
357 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 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 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 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
467async 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}