1use crate::traces::identifier::SignaturesIdentifier;
2use alloy_consensus::{SidecarBuilder, SignableTransaction, SimpleCoder};
3use alloy_dyn_abi::ErrorExt;
4use alloy_json_abi::Function;
5use alloy_network::{
6 AnyNetwork, AnyTypedTransaction, TransactionBuilder, TransactionBuilder4844,
7 TransactionBuilder7702,
8};
9use alloy_primitives::{hex, Address, Bytes, TxKind, U256};
10use alloy_provider::Provider;
11use alloy_rpc_types::{AccessList, Authorization, TransactionInput, TransactionRequest};
12use alloy_serde::WithOtherFields;
13use alloy_signer::Signer;
14use alloy_transport::TransportError;
15use eyre::Result;
16use foundry_cli::{
17 opts::{CliAuthorizationList, TransactionOpts},
18 utils::{self, parse_function_args},
19};
20use foundry_common::{ens::NameOrAddress, fmt::format_tokens};
21use foundry_config::{Chain, Config};
22use foundry_wallets::{WalletOpts, WalletSigner};
23use itertools::Itertools;
24use serde_json::value::RawValue;
25use std::fmt::Write;
26
27#[expect(clippy::large_enum_variant)]
29pub enum SenderKind<'a> {
30 Address(Address),
33 Signer(&'a WalletSigner),
35 OwnedSigner(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(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),
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(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 if specified_from != signer_address {
101 eyre::bail!(
102 "\
103The specified sender via CLI/env vars does not match the sender configured via
104the hardware wallet's HD Path.
105Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
106corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
107 )
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,
140 blob: bool,
141 auth: Option<CliAuthorizationList>,
142 chain: Chain,
143 etherscan_api_key: Option<String>,
144 access_list: Option<Option<AccessList>>,
145 state: S,
146}
147
148impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
149 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
152 let mut tx = WithOtherFields::<TransactionRequest>::default();
153
154 let chain = utils::get_chain(config.chain, &provider).await?;
155 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
156 let legacy = tx_opts.legacy || chain.is_legacy();
157
158 if let Some(gas_limit) = tx_opts.gas_limit {
159 tx.set_gas_limit(gas_limit.to());
160 }
161
162 if let Some(value) = tx_opts.value {
163 tx.set_value(value);
164 }
165
166 if let Some(gas_price) = tx_opts.gas_price {
167 if legacy {
168 tx.set_gas_price(gas_price.to());
169 } else {
170 tx.set_max_fee_per_gas(gas_price.to());
171 }
172 }
173
174 if !legacy {
175 if let Some(priority_fee) = tx_opts.priority_gas_price {
176 tx.set_max_priority_fee_per_gas(priority_fee.to());
177 }
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.legacy {
373 if self.tx.max_fee_per_gas.is_none() {
374 self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
375 }
376
377 if self.tx.max_priority_fee_per_gas.is_none() {
378 self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
379 }
380 }
381 }
382
383 if self.tx.gas.is_none() {
384 self.estimate_gas().await?;
385 }
386
387 Ok((self.tx, self.state.func))
388 }
389
390 async fn estimate_gas(&mut self) -> Result<()> {
392 match self.provider.estimate_gas(self.tx.clone()).await {
393 Ok(estimated) => {
394 self.tx.gas = Some(estimated);
395 Ok(())
396 }
397 Err(err) => {
398 if let TransportError::ErrorResp(payload) = &err {
399 if payload.code == 3 {
402 if let Some(data) = &payload.data {
403 if let Ok(Some(decoded_error)) = decode_execution_revert(data).await {
404 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
405 }
406 }
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 if let Some(err_data) = serde_json::from_str::<String>(data.get())?.strip_prefix("0x") {
463 let Some(selector) = err_data.get(..8) else { return Ok(None) };
464
465 if let Some(known_error) = SignaturesIdentifier::new(Config::foundry_cache_dir(), false)?
466 .write()
467 .await
468 .identify_error(&hex::decode(selector)?)
469 .await
470 {
471 let mut decoded_error = known_error.name.clone();
472 if !known_error.inputs.is_empty() {
473 if let Ok(error) = known_error.decode_error(&hex::decode(err_data)?) {
474 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
475 }
476 }
477 return Ok(Some(decoded_error))
478 }
479 }
480 Ok(None)
481}