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
28pub enum SenderKind<'a> {
30 Address(Address),
33 Signer(&'a WalletSigner),
35 OwnedSigner(Box<WalletSigner>),
37}
38
39impl SenderKind<'_> {
40 pub fn address(&self) -> Address {
42 match self {
43 Self::Address(addr) => *addr,
44 Self::Signer(signer) => signer.address(),
45 Self::OwnedSigner(signer) => signer.address(),
46 }
47 }
48
49 pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
57 if let Some(from) = opts.from {
58 Ok(from.into())
59 } else if let Ok(signer) = opts.signer().await {
60 Ok(Self::OwnedSigner(Box::new(signer)))
61 } else {
62 Ok(Address::ZERO.into())
63 }
64 }
65
66 pub fn as_signer(&self) -> Option<&WalletSigner> {
68 match self {
69 Self::Signer(signer) => Some(signer),
70 Self::OwnedSigner(signer) => Some(signer.as_ref()),
71 _ => None,
72 }
73 }
74}
75
76impl From<Address> for SenderKind<'_> {
77 fn from(addr: Address) -> Self {
78 Self::Address(addr)
79 }
80}
81
82impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
83 fn from(signer: &'a WalletSigner) -> Self {
84 Self::Signer(signer)
85 }
86}
87
88impl From<WalletSigner> for SenderKind<'_> {
89 fn from(signer: WalletSigner) -> Self {
90 Self::OwnedSigner(Box::new(signer))
91 }
92}
93
94pub fn validate_from_address(
96 specified_from: Option<Address>,
97 signer_address: Address,
98) -> Result<()> {
99 if let Some(specified_from) = specified_from
100 && specified_from != signer_address
101 {
102 eyre::bail!(
103 "\
104The specified sender via CLI/env vars does not match the sender configured via
105the hardware wallet's HD Path.
106Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
107corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
108 )
109 }
110 Ok(())
111}
112
113#[derive(Debug)]
115pub struct InitState;
116
117#[derive(Debug)]
119pub struct ToState {
120 to: Option<Address>,
121}
122
123#[derive(Debug)]
125pub struct InputState {
126 kind: TxKind,
127 input: Vec<u8>,
128 func: Option<Function>,
129}
130
131#[derive(Debug)]
136pub struct CastTxBuilder<P, S> {
137 provider: P,
138 tx: WithOtherFields<TransactionRequest>,
139 legacy: bool,
141 blob: bool,
142 auth: Option<CliAuthorizationList>,
143 chain: Chain,
144 etherscan_api_key: Option<String>,
145 access_list: Option<Option<AccessList>>,
146 state: S,
147}
148
149impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
150 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
153 let mut tx = WithOtherFields::<TransactionRequest>::default();
154
155 let chain = utils::get_chain(config.chain, &provider).await?;
156 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
157 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_none());
159
160 if let Some(gas_limit) = tx_opts.gas_limit {
161 tx.set_gas_limit(gas_limit.to());
162 }
163
164 if let Some(value) = tx_opts.value {
165 tx.set_value(value);
166 }
167
168 if let Some(gas_price) = tx_opts.gas_price {
169 if legacy {
170 tx.set_gas_price(gas_price.to());
171 } else {
172 tx.set_max_fee_per_gas(gas_price.to());
173 }
174 }
175
176 if !legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
177 tx.set_max_priority_fee_per_gas(priority_fee.to());
178 }
179
180 if let Some(max_blob_fee) = tx_opts.blob_gas_price {
181 tx.set_max_fee_per_blob_gas(max_blob_fee.to())
182 }
183
184 if let Some(nonce) = tx_opts.nonce {
185 tx.set_nonce(nonce.to());
186 }
187
188 Ok(Self {
189 provider,
190 tx,
191 legacy,
192 blob: tx_opts.blob,
193 chain,
194 etherscan_api_key,
195 auth: tx_opts.auth,
196 access_list: tx_opts.access_list,
197 state: InitState,
198 })
199 }
200
201 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
203 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
204 Ok(CastTxBuilder {
205 provider: self.provider,
206 tx: self.tx,
207 legacy: self.legacy,
208 blob: self.blob,
209 chain: self.chain,
210 etherscan_api_key: self.etherscan_api_key,
211 auth: self.auth,
212 access_list: self.access_list,
213 state: ToState { to },
214 })
215 }
216}
217
218impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
219 pub async fn with_code_sig_and_args(
223 self,
224 code: Option<String>,
225 sig: Option<String>,
226 args: Vec<String>,
227 ) -> Result<CastTxBuilder<P, InputState>> {
228 let (mut args, func) = if let Some(sig) = sig {
229 parse_function_args(
230 &sig,
231 args,
232 self.state.to,
233 self.chain,
234 &self.provider,
235 self.etherscan_api_key.as_deref(),
236 )
237 .await?
238 } else {
239 (Vec::new(), None)
240 };
241
242 let input = if let Some(code) = &code {
243 let mut code = hex::decode(code)?;
244 code.append(&mut args);
245 code
246 } else {
247 args
248 };
249
250 if self.state.to.is_none() && code.is_none() {
251 let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
252 let has_auth = self.auth.is_some();
253 if !has_auth || has_value {
256 eyre::bail!("Must specify a recipient address or contract code to deploy");
257 }
258 }
259
260 Ok(CastTxBuilder {
261 provider: self.provider,
262 tx: self.tx,
263 legacy: self.legacy,
264 blob: self.blob,
265 chain: self.chain,
266 etherscan_api_key: self.etherscan_api_key,
267 auth: self.auth,
268 access_list: self.access_list,
269 state: InputState { kind: self.state.to.into(), input, func },
270 })
271 }
272}
273
274impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
275 pub async fn build(
278 self,
279 sender: impl Into<SenderKind<'_>>,
280 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
281 self._build(sender, true, false).await
282 }
283
284 pub async fn build_raw(
287 self,
288 sender: impl Into<SenderKind<'_>>,
289 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
290 self._build(sender, false, false).await
291 }
292
293 pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
297 let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
298 let tx = tx.build_unsigned()?;
299 match tx {
300 AnyTypedTransaction::Ethereum(t) => Ok(hex::encode_prefixed(t.encoded_for_signing())),
301 _ => eyre::bail!("Cannot generate unsigned transaction for non-Ethereum transactions"),
302 }
303 }
304
305 async fn _build(
306 mut self,
307 sender: impl Into<SenderKind<'_>>,
308 fill: bool,
309 unsigned: bool,
310 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
311 let sender = sender.into();
312 let from = sender.address();
313
314 self.tx.set_kind(self.state.kind);
315
316 let input = Bytes::copy_from_slice(&self.state.input);
318 self.tx.input = TransactionInput { input: Some(input.clone()), data: Some(input) };
319
320 self.tx.set_from(from);
321 self.tx.set_chain_id(self.chain.id());
322
323 let tx_nonce = if let Some(nonce) = self.tx.nonce {
324 nonce
325 } else {
326 let nonce = self.provider.get_transaction_count(from).await?;
327 if fill {
328 self.tx.nonce = Some(nonce);
329 }
330 nonce
331 };
332
333 if !unsigned {
334 self.resolve_auth(sender, tx_nonce).await?;
335 } else if self.auth.is_some() {
336 let Some(CliAuthorizationList::Signed(signed_auth)) = self.auth.take() else {
337 eyre::bail!(
338 "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
339 )
340 };
341
342 self.tx.set_authorization_list(vec![signed_auth]);
343 }
344
345 if let Some(access_list) = match self.access_list.take() {
346 None => None,
347 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
349 Some(Some(access_list)) => Some(access_list),
351 } {
352 self.tx.set_access_list(access_list);
353 }
354
355 if !fill {
356 return Ok((self.tx, self.state.func));
357 }
358
359 if self.legacy && self.tx.gas_price.is_none() {
360 self.tx.gas_price = Some(self.provider.get_gas_price().await?);
361 }
362
363 if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
364 self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
365 }
366
367 if !self.legacy
368 && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
369 {
370 let estimate = self.provider.estimate_eip1559_fees().await?;
371
372 if self.tx.max_fee_per_gas.is_none() {
373 self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
374 }
375
376 if self.tx.max_priority_fee_per_gas.is_none() {
377 self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
378 }
379 }
380
381 if self.tx.gas.is_none() {
382 self.estimate_gas().await?;
383 }
384
385 Ok((self.tx, self.state.func))
386 }
387
388 async fn estimate_gas(&mut self) -> Result<()> {
390 match self.provider.estimate_gas(self.tx.clone()).await {
391 Ok(estimated) => {
392 self.tx.gas = Some(estimated);
393 Ok(())
394 }
395 Err(err) => {
396 if let TransportError::ErrorResp(payload) = &err {
397 if payload.code == 3
400 && let Some(data) = &payload.data
401 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
402 {
403 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
404 }
405 }
406 eyre::bail!("Failed to estimate gas: {}", err)
407 }
408 }
409 }
410
411 async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
413 let Some(auth) = self.auth.take() else { return Ok(()) };
414
415 let auth = match auth {
416 CliAuthorizationList::Address(address) => {
417 let auth = Authorization {
418 chain_id: U256::from(self.chain.id()),
419 nonce: tx_nonce + 1,
420 address,
421 };
422
423 let Some(signer) = sender.as_signer() else {
424 eyre::bail!("No signer available to sign authorization");
425 };
426 let signature = signer.sign_hash(&auth.signature_hash()).await?;
427
428 auth.into_signed(signature)
429 }
430 CliAuthorizationList::Signed(auth) => auth,
431 };
432
433 self.tx.set_authorization_list(vec![auth]);
434
435 Ok(())
436 }
437}
438
439impl<P, S> CastTxBuilder<P, S>
440where
441 P: Provider<AnyNetwork>,
442{
443 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
444 let Some(blob_data) = blob_data else { return Ok(self) };
445
446 let mut coder = SidecarBuilder::<SimpleCoder>::default();
447 coder.ingest(&blob_data);
448 let sidecar = coder.build()?;
449
450 self.tx.set_blob_sidecar(sidecar);
451 self.tx.populate_blob_hashes();
452
453 Ok(self)
454 }
455}
456
457async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
459 let err_data = serde_json::from_str::<Bytes>(data.get())?;
460 let Some(selector) = err_data.get(..4) else { return Ok(None) };
461 if let Some(known_error) =
462 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
463 {
464 let mut decoded_error = known_error.name.clone();
465 if !known_error.inputs.is_empty()
466 && let Ok(error) = known_error.decode_error(&err_data)
467 {
468 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
469 }
470 return Ok(Some(decoded_error));
471 }
472 Ok(None)
473}