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, ReceiptResponse, TransactionBuilder};
7use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U64, 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, TempoOpts, 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, TempoAccessKeyConfig, 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
60#[derive(Debug, Clone, Args)]
62#[command(next_help_heading = "Transaction options")]
63pub struct TxParams {
64 #[arg(long, env = "ETH_GAS_LIMIT")]
66 pub gas_limit: Option<U256>,
67
68 #[arg(long, env = "ETH_GAS_PRICE")]
70 pub gas_price: Option<U256>,
71
72 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
74 pub priority_gas_price: Option<U256>,
75
76 #[arg(long)]
78 pub nonce: Option<U64>,
79
80 #[command(flatten)]
81 pub tempo: TempoOpts,
82}
83
84impl TxParams {
85 pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
86 where
87 N::TransactionRequest: FoundryTransactionBuilder<N>,
88 {
89 if let Some(gas_limit) = self.gas_limit {
90 tx.set_gas_limit(gas_limit.to());
91 }
92
93 if let Some(gas_price) = self.gas_price {
94 if legacy {
95 tx.set_gas_price(gas_price.to());
96 } else {
97 tx.set_max_fee_per_gas(gas_price.to());
98 }
99 }
100
101 if !legacy && let Some(priority_fee) = self.priority_gas_price {
102 tx.set_max_priority_fee_per_gas(priority_fee.to());
103 }
104
105 self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
106 }
107}
108
109pub enum SenderKind<'a> {
111 Address(Address),
114 Signer(&'a WalletSigner),
116 OwnedSigner(Box<WalletSigner>),
118}
119
120impl SenderKind<'_> {
121 pub fn address(&self) -> Address {
123 match self {
124 Self::Address(addr) => *addr,
125 Self::Signer(signer) => signer.address(),
126 Self::OwnedSigner(signer) => signer.address(),
127 }
128 }
129
130 pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
138 if let (Some(signer), _) = opts.maybe_signer().await? {
139 Ok(Self::OwnedSigner(Box::new(signer)))
140 } else if let Some(from) = opts.from {
141 Ok(from.into())
142 } else {
143 Ok(Address::ZERO.into())
144 }
145 }
146
147 pub fn as_signer(&self) -> Option<&WalletSigner> {
149 match self {
150 Self::Signer(signer) => Some(signer),
151 Self::OwnedSigner(signer) => Some(signer.as_ref()),
152 _ => None,
153 }
154 }
155}
156
157impl From<Address> for SenderKind<'_> {
158 fn from(addr: Address) -> Self {
159 Self::Address(addr)
160 }
161}
162
163impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
164 fn from(signer: &'a WalletSigner) -> Self {
165 Self::Signer(signer)
166 }
167}
168
169impl From<WalletSigner> for SenderKind<'_> {
170 fn from(signer: WalletSigner) -> Self {
171 Self::OwnedSigner(Box::new(signer))
172 }
173}
174
175pub fn validate_from_address(
177 specified_from: Option<Address>,
178 signer_address: Address,
179) -> Result<()> {
180 if let Some(specified_from) = specified_from
181 && specified_from != signer_address
182 {
183 eyre::bail!(
184 "\
185The specified sender via CLI/env vars does not match the sender configured via
186the hardware wallet's HD Path.
187Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
188corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
189 )
190 }
191 Ok(())
192}
193
194#[derive(Debug)]
196pub struct InitState;
197
198#[derive(Debug)]
200pub struct ToState {
201 to: Option<Address>,
202}
203
204#[derive(Debug)]
206pub struct InputState {
207 kind: TxKind,
208 input: Vec<u8>,
209 func: Option<Function>,
210}
211
212pub struct CastTxSender<N, P> {
213 provider: P,
214 _phantom: PhantomData<N>,
215}
216
217impl<N: Network, P: Provider<N>> CastTxSender<N, P>
218where
219 N::TransactionRequest: FoundryTransactionBuilder<N>,
220 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
221{
222 pub const fn new(provider: P) -> Self {
224 Self { provider, _phantom: PhantomData }
225 }
226
227 pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
229 let mut receipt = TransactionReceiptWithRevertReason::<N> {
230 receipt: self.provider.send_transaction_sync(tx).await?,
231 revert_reason: None,
232 };
233 let tx_hash = receipt.receipt.transaction_hash();
234 let _ = receipt.update_revert_reason(&self.provider).await;
236
237 self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
238 }
239
240 pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
275 let res = self.provider.send_transaction(tx).await?;
276
277 Ok(res)
278 }
279
280 pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
285 let res = self.provider.send_raw_transaction(raw_tx).await?;
286 Ok(res)
287 }
288
289 pub async fn print_tx_result(
294 &self,
295 tx_hash: B256,
296 cast_async: bool,
297 confs: u64,
298 timeout: u64,
299 ) -> Result<()> {
300 if cast_async {
301 sh_println!("{tx_hash:#x}")?;
302 } else {
303 let receipt =
304 self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
305 sh_println!("{receipt}")?;
306 }
307 Ok(())
308 }
309
310 pub async fn receipt(
327 &self,
328 tx_hash: String,
329 field: Option<String>,
330 confs: u64,
331 timeout: Option<u64>,
332 cast_async: bool,
333 ) -> Result<String> {
334 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
335
336 let mut receipt = TransactionReceiptWithRevertReason::<N> {
337 receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
338 Some(r) => r,
339 None => {
340 if cast_async {
343 eyre::bail!("tx not found: {:?}", tx_hash)
344 }
345 PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
346 .with_required_confirmations(confs)
347 .with_timeout(timeout.map(Duration::from_secs))
348 .get_receipt()
349 .await?
350 }
351 },
352 revert_reason: None,
353 };
354
355 let _ = receipt.update_revert_reason(&self.provider).await;
357
358 self.format_receipt(receipt, field)
359 }
360
361 fn format_receipt(
363 &self,
364 receipt: TransactionReceiptWithRevertReason<N>,
365 field: Option<String>,
366 ) -> Result<String> {
367 Ok(if let Some(ref field) = field {
368 get_pretty_receipt_w_reason_attr(&receipt, field)
369 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
370 } else if shell::is_json() {
371 serde_json::to_value(&receipt)?.to_string()
373 } else {
374 receipt.pretty()
375 })
376 }
377}
378
379#[derive(Debug)]
384pub struct CastTxBuilder<N: Network, P, S> {
385 provider: P,
386 pub(crate) tx: N::TransactionRequest,
387 legacy: bool,
389 blob: bool,
390 eip4844: bool,
392 fill: bool,
395 browser: bool,
397 auth: Vec<CliAuthorizationList>,
398 chain: Chain,
399 etherscan_api_key: Option<String>,
400 etherscan_api_url: Option<String>,
401 access_list: Option<Option<AccessList>>,
402 state: S,
403}
404
405impl<N: Network, P, S> CastTxBuilder<N, P, S> {
406 pub const fn chain(&self) -> Chain {
408 self.chain
409 }
410
411 pub const fn with_browser_wallet(mut self) -> Self {
413 self.browser = true;
414 self
415 }
416}
417
418impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
419where
420 N::TransactionRequest: FoundryTransactionBuilder<N>,
421{
422 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
425 let mut tx = N::TransactionRequest::default();
426
427 let chain = utils::get_chain(config.chain, &provider).await?;
428 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
429 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
430 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
431 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
433
434 tx_opts.apply::<N>(&mut tx, legacy);
436
437 Ok(Self {
438 provider,
439 tx,
440 legacy,
441 blob: tx_opts.blob,
442 eip4844: tx_opts.eip4844,
443 fill: true,
444 browser: false,
445 chain,
446 etherscan_api_key,
447 etherscan_api_url,
448 auth: tx_opts.auth,
449 access_list: tx_opts.access_list,
450 state: InitState,
451 })
452 }
453
454 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
456 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
457 Ok(CastTxBuilder {
458 provider: self.provider,
459 tx: self.tx,
460 legacy: self.legacy,
461 blob: self.blob,
462 eip4844: self.eip4844,
463 fill: self.fill,
464 browser: self.browser,
465 chain: self.chain,
466 etherscan_api_key: self.etherscan_api_key,
467 etherscan_api_url: self.etherscan_api_url,
468 auth: self.auth,
469 access_list: self.access_list,
470 state: ToState { to },
471 })
472 }
473}
474
475impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
476where
477 N::TransactionRequest: FoundryTransactionBuilder<N>,
478{
479 pub async fn with_code_sig_and_args(
483 self,
484 code: Option<String>,
485 sig: Option<String>,
486 args: Vec<String>,
487 ) -> Result<CastTxBuilder<N, P, InputState>> {
488 let (mut args, func) = if let Some(sig) = sig {
489 parse_function_args(
490 &sig,
491 args,
492 self.state.to,
493 self.chain,
494 &self.provider,
495 self.etherscan_api_key.as_deref(),
496 self.etherscan_api_url.as_deref(),
497 )
498 .await?
499 } else {
500 (Vec::new(), None)
501 };
502
503 let input = if let Some(code) = &code {
504 let mut code = hex::decode(code)?;
505 code.append(&mut args);
506 code
507 } else {
508 args
509 };
510
511 if self.state.to.is_none() && code.is_none() {
512 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
513 let has_auth = !self.auth.is_empty();
514 if !has_auth || has_value {
517 eyre::bail!("Must specify a recipient address or contract code to deploy");
518 }
519 }
520
521 Ok(CastTxBuilder {
522 provider: self.provider,
523 tx: self.tx,
524 legacy: self.legacy,
525 blob: self.blob,
526 eip4844: self.eip4844,
527 fill: self.fill,
528 browser: self.browser,
529 chain: self.chain,
530 etherscan_api_key: self.etherscan_api_key,
531 etherscan_api_url: self.etherscan_api_url,
532 auth: self.auth,
533 access_list: self.access_list,
534 state: InputState { kind: self.state.to.into(), input, func },
535 })
536 }
537}
538
539impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
540where
541 N::TransactionRequest: FoundryTransactionBuilder<N>,
542{
543 pub async fn build(
546 self,
547 sender: impl Into<SenderKind<'_>>,
548 ) -> Result<(N::TransactionRequest, Option<Function>)> {
549 let fill = self.fill;
550 self._build(sender, fill, None).await
551 }
552
553 pub async fn build_with_access_key(
559 mut self,
560 sender: impl Into<SenderKind<'_>>,
561 access_key: &TempoAccessKeyConfig,
562 ) -> Result<(N::TransactionRequest, Option<Function>)> {
563 self.tx.set_key_id(access_key.key_address);
564 let fill = self.fill;
565 self._build(sender, fill, Some(access_key)).await
566 }
567
568 async fn _build(
569 mut self,
570 sender: impl Into<SenderKind<'_>>,
571 fill: bool,
572 access_key: Option<&TempoAccessKeyConfig>,
573 ) -> Result<(N::TransactionRequest, Option<Function>)> {
574 let sender = sender.into();
576 self.prepare(&sender);
577
578 self.tx.clear_batch_to();
582
583 let tx_nonce = self.resolve_nonce(sender.address(), fill).await?;
585 self.resolve_auth(&sender, tx_nonce).await?;
586 if let Some(access_key) = access_key {
587 self.tx
588 .prepare_access_key_authorization(
589 &self.provider,
590 access_key.wallet_address,
591 access_key.key_address,
592 access_key.key_authorization.as_ref(),
593 )
594 .await?;
595 }
596 if fill {
597 self.fill_fees().await?;
598 }
599 self.resolve_access_list().await?;
600 if fill {
601 self.fill_gas_limit().await?;
602 }
603
604 Ok((self.tx, self.state.func))
605 }
606
607 fn prepare(&mut self, sender: &SenderKind<'_>) {
610 self.tx.set_kind(self.state.kind);
611 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
614 let sender = sender.address();
615 if !sender.is_zero() {
616 self.tx.set_from(sender);
617 }
618 self.tx.set_chain_id(self.chain.id());
619 }
620
621 async fn resolve_nonce(&mut self, from: Address, fill: bool) -> Result<u64> {
624 if let Some(nonce) = self.tx.nonce() {
625 Ok(nonce)
626 } else {
627 let nonce = self.provider.get_transaction_count(from).await?;
628 if fill {
629 self.tx.set_nonce(nonce);
630 }
631 Ok(nonce)
632 }
633 }
634
635 async fn resolve_access_list(&mut self) -> Result<()> {
638 if let Some(access_list) = match self.access_list.take() {
639 None => None,
640 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
641 Some(Some(access_list)) => Some(access_list),
642 } {
643 self.tx.set_access_list(access_list);
644 }
645 Ok(())
646 }
647
648 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
653 if self.auth.is_empty() {
654 return Ok(());
655 }
656
657 let auths = std::mem::take(&mut self.auth);
658
659 let address_auth_count =
662 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
663 if address_auth_count > 1 {
664 eyre::bail!(
665 "Multiple address-based authorizations provided. Only one address can be specified; \
666 use pre-signed authorizations (hex-encoded) for multiple authorizations."
667 );
668 }
669
670 let mut signed_auths = Vec::with_capacity(auths.len());
671
672 for auth in auths {
673 let signed_auth = match auth {
674 CliAuthorizationList::Address(address) => {
675 let auth = Authorization {
676 chain_id: U256::from(self.chain.id()),
677 nonce: tx_nonce + 1,
678 address,
679 };
680
681 let Some(signer) = sender.as_signer() else {
682 eyre::bail!(
683 "No signer available to sign authorization. \
684 Provide a pre-signed authorization (hex-encoded) instead."
685 );
686 };
687 let signature = signer.sign_hash(&auth.signature_hash()).await?;
688
689 auth.into_signed(signature)
690 }
691 CliAuthorizationList::Signed(auth) => auth,
692 };
693 signed_auths.push(signed_auth);
694 }
695
696 self.tx.set_authorization_list(signed_auths);
697
698 Ok(())
699 }
700
701 async fn fill_fees(&mut self) -> Result<()> {
705 if self.blob && self.tx.max_fee_per_blob_gas().is_none() {
706 self.tx.set_max_fee_per_blob_gas(self.provider.get_blob_base_fee().await?)
707 }
708
709 fill_transaction_gas_fees(&self.provider, &mut self.tx, self.legacy, self.browser).await
710 }
711
712 async fn fill_gas_limit(&mut self) -> Result<()> {
714 if self.tx.gas_limit().is_none() {
715 self.estimate_gas().await?;
716 }
717
718 Ok(())
719 }
720
721 async fn estimate_gas(&mut self) -> Result<()> {
723 match self.provider.estimate_gas(self.tx.clone()).await {
724 Ok(estimated) => {
725 self.tx.set_gas_limit(estimated);
726 Ok(())
727 }
728 Err(err) => {
729 if let TransportError::ErrorResp(payload) = &err {
730 if payload.code == 3
733 && let Some(data) = &payload.data
734 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
735 {
736 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
737 }
738 }
739 eyre::bail!("Failed to estimate gas: {}", err)
740 }
741 }
742 }
743
744 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
746 let Some(blob_data) = blob_data else { return Ok(self) };
747
748 let mut coder = SidecarBuilder::<SimpleCoder>::default();
749 coder.ingest(&blob_data);
750
751 if self.eip4844 {
752 let sidecar = coder.build_4844()?;
753 self.tx.set_blob_sidecar_4844(sidecar);
754 } else {
755 let sidecar = coder.build_7594()?;
756 self.tx.set_blob_sidecar_7594(sidecar);
757 }
758
759 Ok(self)
760 }
761
762 pub const fn raw(mut self) -> Self {
765 self.fill = false;
766 self
767 }
768}
769
770pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
772 provider: &P,
773 tx: &mut N::TransactionRequest,
774 legacy: bool,
775 browser: bool,
776) -> Result<()>
777where
778 N::TransactionRequest: FoundryTransactionBuilder<N>,
779{
780 if legacy {
781 if tx.gas_price().is_none() {
782 tx.set_gas_price(provider.get_gas_price().await?);
783 }
784 return Ok(());
785 }
786
787 if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
788 let mut estimate = provider.estimate_eip1559_fees().await?;
789 if browser
790 && tx.max_priority_fee_per_gas().is_none()
791 && let Ok(suggested_tip) = provider.get_max_priority_fee_per_gas().await
792 && suggested_tip > estimate.max_priority_fee_per_gas
793 {
794 estimate.max_fee_per_gas += suggested_tip - estimate.max_priority_fee_per_gas;
795 estimate.max_priority_fee_per_gas = suggested_tip;
796 }
797
798 if tx.max_fee_per_gas().is_none() {
799 tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
800 }
801
802 if tx.max_priority_fee_per_gas().is_none() {
803 tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
804 }
805 }
806
807 if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
808 eyre::ensure!(
809 priority <= max_fee,
810 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
811 );
812 }
813
814 Ok(())
815}
816
817async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
819 let err_data = serde_json::from_str::<Bytes>(data.get())?;
820 let Some(selector) = err_data.get(..4) else { return Ok(None) };
821 if let Some(known_error) =
822 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
823 {
824 let mut decoded_error = known_error.name.clone();
825 if !known_error.inputs.is_empty()
826 && let Ok(error) = known_error.decode_error(&err_data)
827 {
828 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
829 }
830 return Ok(Some(decoded_error));
831 }
832 Ok(None)
833}