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
31pub enum SenderKind<'a> {
33 Address(Address),
36 Signer(&'a WalletSigner),
38 OwnedSigner(Box<WalletSigner>),
40}
41
42impl SenderKind<'_> {
43 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 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 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
97pub 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#[derive(Debug)]
118pub struct InitState;
119
120#[derive(Debug)]
122pub struct ToState {
123 to: Option<Address>,
124}
125
126#[derive(Debug)]
128pub struct InputState {
129 kind: TxKind,
130 input: Vec<u8>,
131 func: Option<Function>,
132}
133
134#[derive(Debug)]
139pub struct CastTxBuilder<P, S> {
140 provider: P,
141 tx: WithOtherFields<TransactionRequest>,
142 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 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 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 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 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 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 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 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 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 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 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
352 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 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}
477
478pub 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}