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 TransactionReceiptWithRevertReason, fmt::*, get_pretty_receipt_w_reason_attr, shell,
20};
21use foundry_config::{Chain, Config};
22use foundry_primitives::FoundryTransactionBuilder;
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(from) = opts.from {
90 Ok(from.into())
91 } else if let Ok(signer) = opts.signer().await {
92 Ok(Self::OwnedSigner(Box::new(signer)))
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 } else {
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 },
303 revert_reason: None,
304 };
305
306 let _ = receipt.update_revert_reason(&self.provider).await;
308
309 self.format_receipt(receipt, field)
310 }
311
312 fn format_receipt(
314 &self,
315 receipt: TransactionReceiptWithRevertReason<N>,
316 field: Option<String>,
317 ) -> Result<String> {
318 Ok(if let Some(ref field) = field {
319 get_pretty_receipt_w_reason_attr(&receipt, field)
320 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
321 } else if shell::is_json() {
322 serde_json::to_value(&receipt)?.to_string()
324 } else {
325 receipt.pretty()
326 })
327 }
328}
329
330#[derive(Debug)]
335pub struct CastTxBuilder<N: Network, P, S> {
336 provider: P,
337 tx: N::TransactionRequest,
338 legacy: bool,
340 blob: bool,
341 eip4844: bool,
343 fill: bool,
346 auth: Vec<CliAuthorizationList>,
347 chain: Chain,
348 etherscan_api_key: Option<String>,
349 access_list: Option<Option<AccessList>>,
350 state: S,
351}
352
353impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
354where
355 N::TransactionRequest: FoundryTransactionBuilder<N>,
356{
357 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
360 let mut tx = N::TransactionRequest::default();
361
362 let chain = utils::get_chain(config.chain, &provider).await?;
363 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
364 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
366
367 tx_opts.apply::<N>(&mut tx, legacy);
369
370 Ok(Self {
371 provider,
372 tx,
373 legacy,
374 blob: tx_opts.blob,
375 eip4844: tx_opts.eip4844,
376 fill: true,
377 chain,
378 etherscan_api_key,
379 auth: tx_opts.auth,
380 access_list: tx_opts.access_list,
381 state: InitState,
382 })
383 }
384
385 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
387 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
388 Ok(CastTxBuilder {
389 provider: self.provider,
390 tx: self.tx,
391 legacy: self.legacy,
392 blob: self.blob,
393 eip4844: self.eip4844,
394 fill: self.fill,
395 chain: self.chain,
396 etherscan_api_key: self.etherscan_api_key,
397 auth: self.auth,
398 access_list: self.access_list,
399 state: ToState { to },
400 })
401 }
402}
403
404impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
405where
406 N::TransactionRequest: FoundryTransactionBuilder<N>,
407{
408 pub async fn with_code_sig_and_args(
412 self,
413 code: Option<String>,
414 sig: Option<String>,
415 args: Vec<String>,
416 ) -> Result<CastTxBuilder<N, P, InputState>> {
417 let (mut args, func) = if let Some(sig) = sig {
418 parse_function_args(
419 &sig,
420 args,
421 self.state.to,
422 self.chain,
423 &self.provider,
424 self.etherscan_api_key.as_deref(),
425 )
426 .await?
427 } else {
428 (Vec::new(), None)
429 };
430
431 let input = if let Some(code) = &code {
432 let mut code = hex::decode(code)?;
433 code.append(&mut args);
434 code
435 } else {
436 args
437 };
438
439 if self.state.to.is_none() && code.is_none() {
440 let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
441 let has_auth = !self.auth.is_empty();
442 if !has_auth || has_value {
445 eyre::bail!("Must specify a recipient address or contract code to deploy");
446 }
447 }
448
449 Ok(CastTxBuilder {
450 provider: self.provider,
451 tx: self.tx,
452 legacy: self.legacy,
453 blob: self.blob,
454 eip4844: self.eip4844,
455 fill: self.fill,
456 chain: self.chain,
457 etherscan_api_key: self.etherscan_api_key,
458 auth: self.auth,
459 access_list: self.access_list,
460 state: InputState { kind: self.state.to.into(), input, func },
461 })
462 }
463}
464
465impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
466where
467 N::TransactionRequest: FoundryTransactionBuilder<N>,
468{
469 pub async fn build(
472 self,
473 sender: impl Into<SenderKind<'_>>,
474 ) -> Result<(N::TransactionRequest, Option<Function>)> {
475 let fill = self.fill;
476 self._build(sender, fill).await
477 }
478
479 async fn _build(
480 mut self,
481 sender: impl Into<SenderKind<'_>>,
482 fill: bool,
483 ) -> Result<(N::TransactionRequest, Option<Function>)> {
484 let sender = sender.into();
486 self.prepare(&sender);
487
488 let tx_nonce = self.resolve_nonce(sender.address(), fill).await?;
490 self.resolve_auth(&sender, tx_nonce).await?;
491 self.resolve_access_list().await?;
492
493 if fill {
495 self.fill_fees().await?;
496 }
497
498 Ok((self.tx, self.state.func))
499 }
500
501 fn prepare(&mut self, sender: &SenderKind<'_>) {
503 self.tx.set_kind(self.state.kind);
504 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
507 self.tx.set_from(sender.address());
508 self.tx.set_chain_id(self.chain.id());
509 }
510
511 async fn resolve_nonce(&mut self, from: Address, fill: bool) -> Result<u64> {
514 if let Some(nonce) = self.tx.nonce() {
515 Ok(nonce)
516 } else {
517 let nonce = self.provider.get_transaction_count(from).await?;
518 if fill {
519 self.tx.set_nonce(nonce);
520 }
521 Ok(nonce)
522 }
523 }
524
525 async fn resolve_access_list(&mut self) -> Result<()> {
528 if let Some(access_list) = match self.access_list.take() {
529 None => None,
530 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
531 Some(Some(access_list)) => Some(access_list),
532 } {
533 self.tx.set_access_list(access_list);
534 }
535 Ok(())
536 }
537
538 async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
543 if self.auth.is_empty() {
544 return Ok(());
545 }
546
547 let auths = std::mem::take(&mut self.auth);
548
549 let address_auth_count =
552 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
553 if address_auth_count > 1 {
554 eyre::bail!(
555 "Multiple address-based authorizations provided. Only one address can be specified; \
556 use pre-signed authorizations (hex-encoded) for multiple authorizations."
557 );
558 }
559
560 let mut signed_auths = Vec::with_capacity(auths.len());
561
562 for auth in auths {
563 let signed_auth = match auth {
564 CliAuthorizationList::Address(address) => {
565 let auth = Authorization {
566 chain_id: U256::from(self.chain.id()),
567 nonce: tx_nonce + 1,
568 address,
569 };
570
571 let Some(signer) = sender.as_signer() else {
572 eyre::bail!(
573 "No signer available to sign authorization. \
574 Provide a pre-signed authorization (hex-encoded) instead."
575 );
576 };
577 let signature = signer.sign_hash(&auth.signature_hash()).await?;
578
579 auth.into_signed(signature)
580 }
581 CliAuthorizationList::Signed(auth) => auth,
582 };
583 signed_auths.push(signed_auth);
584 }
585
586 self.tx.set_authorization_list(signed_auths);
587
588 Ok(())
589 }
590
591 async fn fill_fees(&mut self) -> Result<()> {
595 if self.legacy && self.tx.gas_price().is_none() {
596 self.tx.set_gas_price(self.provider.get_gas_price().await?);
597 }
598
599 if self.blob && self.tx.max_fee_per_blob_gas().is_none() {
600 self.tx.set_max_fee_per_blob_gas(self.provider.get_blob_base_fee().await?)
601 }
602
603 if !self.legacy
604 && (self.tx.max_fee_per_gas().is_none() || self.tx.max_priority_fee_per_gas().is_none())
605 {
606 let estimate = self.provider.estimate_eip1559_fees().await?;
607
608 if self.tx.max_fee_per_gas().is_none() {
609 self.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
610 }
611
612 if self.tx.max_priority_fee_per_gas().is_none() {
613 self.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
614 }
615 }
616
617 if self.tx.gas_limit().is_none() {
618 self.estimate_gas().await?;
619 }
620
621 Ok(())
622 }
623
624 async fn estimate_gas(&mut self) -> Result<()> {
626 match self.provider.estimate_gas(self.tx.clone()).await {
627 Ok(estimated) => {
628 self.tx.set_gas_limit(estimated);
629 Ok(())
630 }
631 Err(err) => {
632 if let TransportError::ErrorResp(payload) = &err {
633 if payload.code == 3
636 && let Some(data) = &payload.data
637 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
638 {
639 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
640 }
641 }
642 eyre::bail!("Failed to estimate gas: {}", err)
643 }
644 }
645 }
646
647 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
649 let Some(blob_data) = blob_data else { return Ok(self) };
650
651 let mut coder = SidecarBuilder::<SimpleCoder>::default();
652 coder.ingest(&blob_data);
653
654 if self.eip4844 {
655 let sidecar = coder.build_4844()?;
656 self.tx.set_blob_sidecar_4844(sidecar);
657 } else {
658 let sidecar = coder.build_7594()?;
659 self.tx.set_blob_sidecar_7594(sidecar);
660 }
661
662 Ok(self)
663 }
664
665 pub fn raw(mut self) -> Self {
668 self.fill = false;
669 self
670 }
671}
672
673async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
675 let err_data = serde_json::from_str::<Bytes>(data.get())?;
676 let Some(selector) = err_data.get(..4) else { return Ok(None) };
677 if let Some(known_error) =
678 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
679 {
680 let mut decoded_error = known_error.name.clone();
681 if !known_error.inputs.is_empty()
682 && let Ok(error) = known_error.decode_error(&err_data)
683 {
684 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
685 }
686 return Ok(Some(decoded_error));
687 }
688 Ok(None)
689}