1use crate::traces::identifier::SignaturesIdentifier;
2use alloy_consensus::{SidecarBuilder, SimpleCoder};
3use alloy_dyn_abi::ErrorExt;
4use alloy_ens::NameOrAddress;
5use alloy_json_abi::Function;
6use alloy_network::{Network, TransactionBuilder};
7use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U256, hex};
8use alloy_provider::{PendingTransactionBuilder, Provider};
9use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind};
10use alloy_signer::Signer;
11use alloy_transport::TransportError;
12use clap::Args;
13use eyre::{Result, WrapErr};
14use foundry_cli::{
15 opts::{CliAuthorizationList, EthereumOpts, TransactionOpts},
16 utils::{self, parse_function_args},
17};
18use foundry_common::{
19 FoundryTransactionBuilder, TransactionReceiptWithRevertReason, fmt::*,
20 get_pretty_receipt_w_reason_attr, shell,
21};
22use foundry_config::{Chain, Config};
23use foundry_wallets::{BrowserWalletOpts, WalletOpts, WalletSigner};
24use itertools::Itertools;
25use serde_json::value::RawValue;
26use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration};
27
28#[derive(Debug, Clone, Args)]
29pub struct SendTxOpts {
30 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
32 pub cast_async: bool,
33
34 #[arg(long, conflicts_with = "async")]
37 pub sync: bool,
38
39 #[arg(long, default_value = "1")]
41 pub confirmations: u64,
42
43 #[arg(long, env = "ETH_TIMEOUT")]
45 pub timeout: Option<u64>,
46
47 #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
49 pub poll_interval: Option<u64>,
50
51 #[command(flatten)]
53 pub eth: EthereumOpts,
54
55 #[command(flatten)]
57 pub browser: BrowserWalletOpts,
58}
59
60pub enum SenderKind<'a> {
62 Address(Address),
65 Signer(&'a WalletSigner),
67 OwnedSigner(Box<WalletSigner>),
69}
70
71impl SenderKind<'_> {
72 pub fn address(&self) -> Address {
74 match self {
75 Self::Address(addr) => *addr,
76 Self::Signer(signer) => signer.address(),
77 Self::OwnedSigner(signer) => signer.address(),
78 }
79 }
80
81 pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
89 if let (Some(signer), _) = opts.maybe_signer().await? {
90 Ok(Self::OwnedSigner(Box::new(signer)))
91 } else if let Some(from) = opts.from {
92 Ok(from.into())
93 } else {
94 Ok(Address::ZERO.into())
95 }
96 }
97
98 pub fn as_signer(&self) -> Option<&WalletSigner> {
100 match self {
101 Self::Signer(signer) => Some(signer),
102 Self::OwnedSigner(signer) => Some(signer.as_ref()),
103 _ => None,
104 }
105 }
106}
107
108impl From<Address> for SenderKind<'_> {
109 fn from(addr: Address) -> Self {
110 Self::Address(addr)
111 }
112}
113
114impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
115 fn from(signer: &'a WalletSigner) -> Self {
116 Self::Signer(signer)
117 }
118}
119
120impl From<WalletSigner> for SenderKind<'_> {
121 fn from(signer: WalletSigner) -> Self {
122 Self::OwnedSigner(Box::new(signer))
123 }
124}
125
126pub fn validate_from_address(
128 specified_from: Option<Address>,
129 signer_address: Address,
130) -> Result<()> {
131 if let Some(specified_from) = specified_from
132 && specified_from != signer_address
133 {
134 eyre::bail!(
135 "\
136The specified sender via CLI/env vars does not match the sender configured via
137the hardware wallet's HD Path.
138Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
139corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
140 )
141 }
142 Ok(())
143}
144
145#[derive(Debug)]
147pub struct InitState;
148
149#[derive(Debug)]
151pub struct ToState {
152 to: Option<Address>,
153}
154
155#[derive(Debug)]
157pub struct InputState {
158 kind: TxKind,
159 input: Vec<u8>,
160 func: Option<Function>,
161}
162
163pub struct CastTxSender<N, P> {
164 provider: P,
165 _phantom: PhantomData<N>,
166}
167
168impl<N: Network, P: Provider<N>> CastTxSender<N, P>
169where
170 N::TransactionRequest: FoundryTransactionBuilder<N>,
171 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
172{
173 pub fn new(provider: P) -> Self {
175 Self { provider, _phantom: PhantomData }
176 }
177
178 pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<String> {
180 let mut receipt = TransactionReceiptWithRevertReason::<N> {
181 receipt: self.provider.send_transaction_sync(tx).await?,
182 revert_reason: None,
183 };
184 let _ = receipt.update_revert_reason(&self.provider).await;
186
187 self.format_receipt(receipt, None)
188 }
189
190 pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
225 let res = self.provider.send_transaction(tx).await?;
226
227 Ok(res)
228 }
229
230 pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
235 let res = self.provider.send_raw_transaction(raw_tx).await?;
236 Ok(res)
237 }
238
239 pub async fn print_tx_result(
244 &self,
245 tx_hash: B256,
246 cast_async: bool,
247 confs: u64,
248 timeout: u64,
249 ) -> Result<()> {
250 if cast_async {
251 sh_println!("{tx_hash:#x}")?;
252 } else {
253 let receipt =
254 self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
255 sh_println!("{receipt}")?;
256 }
257 Ok(())
258 }
259
260 pub async fn receipt(
277 &self,
278 tx_hash: String,
279 field: Option<String>,
280 confs: u64,
281 timeout: Option<u64>,
282 cast_async: bool,
283 ) -> Result<String> {
284 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
285
286 let mut receipt = TransactionReceiptWithRevertReason::<N> {
287 receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
288 Some(r) => r,
289 None => {
290 if cast_async {
293 eyre::bail!("tx not found: {:?}", tx_hash)
294 }
295 PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
296 .with_required_confirmations(confs)
297 .with_timeout(timeout.map(Duration::from_secs))
298 .get_receipt()
299 .await?
300 }
301 },
302 revert_reason: None,
303 };
304
305 let _ = receipt.update_revert_reason(&self.provider).await;
307
308 self.format_receipt(receipt, field)
309 }
310
311 fn format_receipt(
313 &self,
314 receipt: TransactionReceiptWithRevertReason<N>,
315 field: Option<String>,
316 ) -> Result<String> {
317 Ok(if let Some(ref field) = field {
318 get_pretty_receipt_w_reason_attr(&receipt, field)
319 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
320 } else if shell::is_json() {
321 serde_json::to_value(&receipt)?.to_string()
323 } else {
324 receipt.pretty()
325 })
326 }
327}
328
329#[derive(Debug)]
334pub struct CastTxBuilder<N: Network, P, S> {
335 provider: P,
336 pub(crate) tx: N::TransactionRequest,
337 legacy: bool,
339 blob: bool,
340 eip4844: bool,
342 fill: bool,
345 auth: Vec<CliAuthorizationList>,
346 chain: Chain,
347 etherscan_api_key: Option<String>,
348 access_list: Option<Option<AccessList>>,
349 state: S,
350}
351
352impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
353where
354 N::TransactionRequest: FoundryTransactionBuilder<N>,
355{
356 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
359 let mut tx = N::TransactionRequest::default();
360
361 let chain = utils::get_chain(config.chain, &provider).await?;
362 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
363 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
365
366 tx_opts.apply::<N>(&mut tx, legacy);
368
369 Ok(Self {
370 provider,
371 tx,
372 legacy,
373 blob: tx_opts.blob,
374 eip4844: tx_opts.eip4844,
375 fill: true,
376 chain,
377 etherscan_api_key,
378 auth: tx_opts.auth,
379 access_list: tx_opts.access_list,
380 state: InitState,
381 })
382 }
383
384 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
386 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
387 Ok(CastTxBuilder {
388 provider: self.provider,
389 tx: self.tx,
390 legacy: self.legacy,
391 blob: self.blob,
392 eip4844: self.eip4844,
393 fill: self.fill,
394 chain: self.chain,
395 etherscan_api_key: self.etherscan_api_key,
396 auth: self.auth,
397 access_list: self.access_list,
398 state: ToState { to },
399 })
400 }
401}
402
403impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
404where
405 N::TransactionRequest: FoundryTransactionBuilder<N>,
406{
407 pub async fn with_code_sig_and_args(
411 self,
412 code: Option<String>,
413 sig: Option<String>,
414 args: Vec<String>,
415 ) -> Result<CastTxBuilder<N, P, InputState>> {
416 let (mut args, func) = if let Some(sig) = sig {
417 parse_function_args(
418 &sig,
419 args,
420 self.state.to,
421 self.chain,
422 &self.provider,
423 self.etherscan_api_key.as_deref(),
424 )
425 .await?
426 } else {
427 (Vec::new(), None)
428 };
429
430 let input = if let Some(code) = &code {
431 let mut code = hex::decode(code)?;
432 code.append(&mut args);
433 code
434 } else {
435 args
436 };
437
438 if self.state.to.is_none() && code.is_none() {
439 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
440 let has_auth = !self.auth.is_empty();
441 if !has_auth || has_value {
444 eyre::bail!("Must specify a recipient address or contract code to deploy");
445 }
446 }
447
448 Ok(CastTxBuilder {
449 provider: self.provider,
450 tx: self.tx,
451 legacy: self.legacy,
452 blob: self.blob,
453 eip4844: self.eip4844,
454 fill: self.fill,
455 chain: self.chain,
456 etherscan_api_key: self.etherscan_api_key,
457 auth: self.auth,
458 access_list: self.access_list,
459 state: InputState { kind: self.state.to.into(), input, func },
460 })
461 }
462}
463
464impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
465where
466 N::TransactionRequest: FoundryTransactionBuilder<N>,
467{
468 pub async fn build(
471 self,
472 sender: impl Into<SenderKind<'_>>,
473 ) -> Result<(N::TransactionRequest, Option<Function>)> {
474 let fill = self.fill;
475 self._build(sender, fill).await
476 }
477
478 async fn _build(
479 mut self,
480 sender: impl Into<SenderKind<'_>>,
481 fill: bool,
482 ) -> Result<(N::TransactionRequest, Option<Function>)> {
483 let sender = sender.into();
485 self.prepare(&sender);
486
487 self.tx.clear_batch_to();
491
492 let tx_nonce = self.resolve_nonce(sender.address(), fill).await?;
494 self.resolve_auth(&sender, tx_nonce).await?;
495 self.resolve_access_list().await?;
496
497 if fill {
499 self.fill_fees().await?;
500 }
501
502 Ok((self.tx, self.state.func))
503 }
504
505 fn prepare(&mut self, sender: &SenderKind<'_>) {
507 self.tx.set_kind(self.state.kind);
508 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
511 self.tx.set_from(sender.address());
512 self.tx.set_chain_id(self.chain.id());
513 }
514
515 async fn resolve_nonce(&mut self, from: Address, fill: bool) -> Result<u64> {
518 if let Some(nonce) = self.tx.nonce() {
519 Ok(nonce)
520 } else {
521 let nonce = self.provider.get_transaction_count(from).await?;
522 if fill {
523 self.tx.set_nonce(nonce);
524 }
525 Ok(nonce)
526 }
527 }
528
529 async fn resolve_access_list(&mut self) -> Result<()> {
532 if let Some(access_list) = match self.access_list.take() {
533 None => None,
534 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
535 Some(Some(access_list)) => Some(access_list),
536 } {
537 self.tx.set_access_list(access_list);
538 }
539 Ok(())
540 }
541
542 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
547 if self.auth.is_empty() {
548 return Ok(());
549 }
550
551 let auths = std::mem::take(&mut self.auth);
552
553 let address_auth_count =
556 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
557 if address_auth_count > 1 {
558 eyre::bail!(
559 "Multiple address-based authorizations provided. Only one address can be specified; \
560 use pre-signed authorizations (hex-encoded) for multiple authorizations."
561 );
562 }
563
564 let mut signed_auths = Vec::with_capacity(auths.len());
565
566 for auth in auths {
567 let signed_auth = match auth {
568 CliAuthorizationList::Address(address) => {
569 let auth = Authorization {
570 chain_id: U256::from(self.chain.id()),
571 nonce: tx_nonce + 1,
572 address,
573 };
574
575 let Some(signer) = sender.as_signer() else {
576 eyre::bail!(
577 "No signer available to sign authorization. \
578 Provide a pre-signed authorization (hex-encoded) instead."
579 );
580 };
581 let signature = signer.sign_hash(&auth.signature_hash()).await?;
582
583 auth.into_signed(signature)
584 }
585 CliAuthorizationList::Signed(auth) => auth,
586 };
587 signed_auths.push(signed_auth);
588 }
589
590 self.tx.set_authorization_list(signed_auths);
591
592 Ok(())
593 }
594
595 async fn fill_fees(&mut self) -> Result<()> {
599 if self.legacy && self.tx.gas_price().is_none() {
600 self.tx.set_gas_price(self.provider.get_gas_price().await?);
601 }
602
603 if self.blob && self.tx.max_fee_per_blob_gas().is_none() {
604 self.tx.set_max_fee_per_blob_gas(self.provider.get_blob_base_fee().await?)
605 }
606
607 if !self.legacy
608 && (self.tx.max_fee_per_gas().is_none() || self.tx.max_priority_fee_per_gas().is_none())
609 {
610 let estimate = self.provider.estimate_eip1559_fees().await?;
611
612 if self.tx.max_fee_per_gas().is_none() {
613 self.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
614 }
615
616 if self.tx.max_priority_fee_per_gas().is_none() {
617 self.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
618 }
619 }
620
621 if self.tx.gas_limit().is_none() {
622 self.estimate_gas().await?;
623 }
624
625 Ok(())
626 }
627
628 async fn estimate_gas(&mut self) -> Result<()> {
630 match self.provider.estimate_gas(self.tx.clone()).await {
631 Ok(estimated) => {
632 self.tx.set_gas_limit(estimated);
633 Ok(())
634 }
635 Err(err) => {
636 if let TransportError::ErrorResp(payload) = &err {
637 if payload.code == 3
640 && let Some(data) = &payload.data
641 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
642 {
643 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
644 }
645 }
646 eyre::bail!("Failed to estimate gas: {}", err)
647 }
648 }
649 }
650
651 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
653 let Some(blob_data) = blob_data else { return Ok(self) };
654
655 let mut coder = SidecarBuilder::<SimpleCoder>::default();
656 coder.ingest(&blob_data);
657
658 if self.eip4844 {
659 let sidecar = coder.build_4844()?;
660 self.tx.set_blob_sidecar_4844(sidecar);
661 } else {
662 let sidecar = coder.build_7594()?;
663 self.tx.set_blob_sidecar_7594(sidecar);
664 }
665
666 Ok(self)
667 }
668
669 pub fn raw(mut self) -> Self {
672 self.fill = false;
673 self
674 }
675}
676
677async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
679 let err_data = serde_json::from_str::<Bytes>(data.get())?;
680 let Some(selector) = err_data.get(..4) else { return Ok(None) };
681 if let Some(known_error) =
682 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
683 {
684 let mut decoded_error = known_error.name.clone();
685 if !known_error.inputs.is_empty()
686 && let Ok(error) = known_error.decode_error(&err_data)
687 {
688 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
689 }
690 return Ok(Some(decoded_error));
691 }
692 Ok(None)
693}