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, TransactionBuilder, TransactionBuilder7594, TransactionBuilder7702,
8};
9use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256, hex};
10use alloy_provider::{PendingTransactionBuilder, Provider};
11use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind, TransactionRequest};
12use alloy_serde::WithOtherFields;
13use alloy_signer::Signer;
14use alloy_transport::TransportError;
15use clap::Args;
16use eyre::{Result, WrapErr};
17use foundry_cli::{
18 opts::{CliAuthorizationList, EthereumOpts, TransactionOpts},
19 utils::{self, LoadConfig, get_provider_builder, parse_function_args},
20};
21use foundry_common::{
22 TransactionReceiptWithRevertReason, fmt::*, get_pretty_tx_receipt_attr,
23 provider::RetryProviderWithSigner, shell,
24};
25use foundry_config::{Chain, Config};
26use foundry_primitives::{FoundryTransactionRequest, FoundryTypedTx};
27use foundry_wallets::{WalletOpts, WalletSigner};
28use itertools::Itertools;
29use serde_json::value::RawValue;
30use std::{fmt::Write, str::FromStr, time::Duration};
31
32#[derive(Debug, Clone, Args)]
33pub struct SendTxOpts {
34 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
36 pub cast_async: bool,
37
38 #[arg(long, conflicts_with = "async")]
41 pub sync: bool,
42
43 #[arg(long, default_value = "1")]
45 pub confirmations: u64,
46
47 #[arg(long, env = "ETH_TIMEOUT")]
49 pub timeout: Option<u64>,
50
51 #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
53 pub poll_interval: Option<u64>,
54
55 #[command(flatten)]
57 pub eth: EthereumOpts,
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<P> {
164 provider: P,
165}
166
167impl<P: Provider<AnyNetwork>> CastTxSender<P> {
168 pub fn new(provider: P) -> Self {
170 Self { provider }
171 }
172
173 pub async fn send_sync(&self, tx: WithOtherFields<TransactionRequest>) -> Result<String> {
175 let mut receipt: TransactionReceiptWithRevertReason =
176 self.provider.send_transaction_sync(tx).await?.into();
177
178 let _ = receipt.update_revert_reason(&self.provider).await;
180
181 self.format_receipt(receipt, None)
182 }
183
184 pub async fn send(
219 &self,
220 tx: WithOtherFields<TransactionRequest>,
221 ) -> Result<PendingTransactionBuilder<AnyNetwork>> {
222 let res = self.provider.send_transaction(tx).await?;
223
224 Ok(res)
225 }
226
227 pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<AnyNetwork>> {
232 let res = self.provider.send_raw_transaction(raw_tx).await?;
233 Ok(res)
234 }
235
236 pub async fn receipt(
253 &self,
254 tx_hash: String,
255 field: Option<String>,
256 confs: u64,
257 timeout: Option<u64>,
258 cast_async: bool,
259 ) -> Result<String> {
260 let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
261
262 let mut receipt: TransactionReceiptWithRevertReason =
263 match self.provider.get_transaction_receipt(tx_hash).await? {
264 Some(r) => r,
265 None => {
266 if cast_async {
269 eyre::bail!("tx not found: {:?}", tx_hash)
270 } else {
271 PendingTransactionBuilder::new(self.provider.root().clone(), tx_hash)
272 .with_required_confirmations(confs)
273 .with_timeout(timeout.map(Duration::from_secs))
274 .get_receipt()
275 .await?
276 }
277 }
278 }
279 .into();
280
281 let _ = receipt.update_revert_reason(&self.provider).await;
283
284 self.format_receipt(receipt, field)
285 }
286
287 fn format_receipt(
289 &self,
290 receipt: TransactionReceiptWithRevertReason,
291 field: Option<String>,
292 ) -> Result<String> {
293 Ok(if let Some(ref field) = field {
294 get_pretty_tx_receipt_attr(&receipt, field)
295 .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
296 } else if shell::is_json() {
297 serde_json::to_value(&receipt)?.to_string()
299 } else {
300 receipt.pretty()
301 })
302 }
303}
304
305#[derive(Debug)]
310pub struct CastTxBuilder<P, S> {
311 provider: P,
312 tx: WithOtherFields<TransactionRequest>,
313 legacy: bool,
315 blob: bool,
316 eip4844: bool,
318 auth: Vec<CliAuthorizationList>,
319 chain: Chain,
320 etherscan_api_key: Option<String>,
321 access_list: Option<Option<AccessList>>,
322 state: S,
323}
324
325impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
326 pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
329 let mut tx = WithOtherFields::<TransactionRequest>::default();
330
331 let chain = utils::get_chain(config.chain, &provider).await?;
332 let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
333 let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
335
336 if let Some(gas_limit) = tx_opts.gas_limit {
337 tx.set_gas_limit(gas_limit.to());
338 }
339
340 if let Some(value) = tx_opts.value {
341 tx.set_value(value);
342 }
343
344 if let Some(gas_price) = tx_opts.gas_price {
345 if legacy {
346 tx.set_gas_price(gas_price.to());
347 } else {
348 tx.set_max_fee_per_gas(gas_price.to());
349 }
350 }
351
352 if !legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
353 tx.set_max_priority_fee_per_gas(priority_fee.to());
354 }
355
356 if let Some(max_blob_fee) = tx_opts.blob_gas_price {
357 tx.set_max_fee_per_blob_gas(max_blob_fee.to())
358 }
359
360 if let Some(nonce) = tx_opts.nonce {
361 tx.set_nonce(nonce.to());
362 }
363
364 if let Some(fee_token) = tx_opts.tempo.fee_token {
366 tx.other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
367 }
368
369 if let Some(nonce_key) = tx_opts.tempo.sequence_key {
370 tx.other.insert("nonceKey".to_string(), serde_json::to_value(nonce_key).unwrap());
371 }
372
373 Ok(Self {
374 provider,
375 tx,
376 legacy,
377 blob: tx_opts.blob,
378 eip4844: tx_opts.eip4844,
379 chain,
380 etherscan_api_key,
381 auth: tx_opts.auth,
382 access_list: tx_opts.access_list,
383 state: InitState,
384 })
385 }
386
387 pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
389 let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
390 Ok(CastTxBuilder {
391 provider: self.provider,
392 tx: self.tx,
393 legacy: self.legacy,
394 blob: self.blob,
395 eip4844: self.eip4844,
396 chain: self.chain,
397 etherscan_api_key: self.etherscan_api_key,
398 auth: self.auth,
399 access_list: self.access_list,
400 state: ToState { to },
401 })
402 }
403}
404
405impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
406 pub async fn with_code_sig_and_args(
410 self,
411 code: Option<String>,
412 sig: Option<String>,
413 args: Vec<String>,
414 ) -> Result<CastTxBuilder<P, InputState>> {
415 let (mut args, func) = if let Some(sig) = sig {
416 parse_function_args(
417 &sig,
418 args,
419 self.state.to,
420 self.chain,
421 &self.provider,
422 self.etherscan_api_key.as_deref(),
423 )
424 .await?
425 } else {
426 (Vec::new(), None)
427 };
428
429 let input = if let Some(code) = &code {
430 let mut code = hex::decode(code)?;
431 code.append(&mut args);
432 code
433 } else {
434 args
435 };
436
437 if self.state.to.is_none() && code.is_none() {
438 let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
439 let has_auth = !self.auth.is_empty();
440 if !has_auth || has_value {
443 eyre::bail!("Must specify a recipient address or contract code to deploy");
444 }
445 }
446
447 Ok(CastTxBuilder {
448 provider: self.provider,
449 tx: self.tx,
450 legacy: self.legacy,
451 blob: self.blob,
452 eip4844: self.eip4844,
453 chain: self.chain,
454 etherscan_api_key: self.etherscan_api_key,
455 auth: self.auth,
456 access_list: self.access_list,
457 state: InputState { kind: self.state.to.into(), input, func },
458 })
459 }
460}
461
462impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
463 pub async fn build(
466 self,
467 sender: impl Into<SenderKind<'_>>,
468 ) -> Result<(FoundryTransactionRequest, Option<Function>)> {
469 let (tx, func) = self._build(sender, true, false).await?;
470 Ok((FoundryTransactionRequest::new(tx), func))
471 }
472
473 pub async fn build_raw(
476 self,
477 sender: impl Into<SenderKind<'_>>,
478 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
479 self._build(sender, false, false).await
480 }
481
482 pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
486 let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
487 let ftx = FoundryTransactionRequest::new(tx);
488
489 let tx = ftx.build_unsigned()?;
490 Ok(hex::encode_prefixed(match tx {
491 FoundryTypedTx::Legacy(t) => t.encoded_for_signing(),
492 FoundryTypedTx::Eip1559(t) => t.encoded_for_signing(),
493 FoundryTypedTx::Eip2930(t) => t.encoded_for_signing(),
494 FoundryTypedTx::Eip4844(t) => t.encoded_for_signing(),
495 FoundryTypedTx::Eip7702(t) => t.encoded_for_signing(),
496 FoundryTypedTx::Tempo(t) => t.encoded_for_signing(),
497 _ => eyre::bail!(
498 "Cannot generate unsigned transaction for transaction: unknown transaction type"
499 ),
500 }))
501 }
502
503 pub fn is_tempo(&self) -> bool {
505 self.tx.other.contains_key("feeToken") || self.tx.other.contains_key("nonceKey")
507 }
508
509 async fn _build(
510 mut self,
511 sender: impl Into<SenderKind<'_>>,
512 fill: bool,
513 unsigned: bool,
514 ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
515 let sender = sender.into();
516 let from = sender.address();
517
518 self.tx.set_kind(self.state.kind);
519
520 self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
522
523 self.tx.set_from(from);
524 self.tx.set_chain_id(self.chain.id());
525
526 let tx_nonce = if let Some(nonce) = self.tx.nonce {
527 nonce
528 } else {
529 let nonce = self.provider.get_transaction_count(from).await?;
530 if fill {
531 self.tx.nonce = Some(nonce);
532 }
533 nonce
534 };
535
536 if !unsigned {
537 self.resolve_auth(sender, tx_nonce).await?;
538 } else if !self.auth.is_empty() {
539 let mut signed_auths = Vec::with_capacity(self.auth.len());
540 for auth in std::mem::take(&mut self.auth) {
541 let CliAuthorizationList::Signed(signed_auth) = auth else {
542 eyre::bail!(
543 "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
544 )
545 };
546 signed_auths.push(signed_auth);
547 }
548
549 self.tx.set_authorization_list(signed_auths);
550 }
551
552 if let Some(access_list) = match self.access_list.take() {
553 None => None,
554 Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
556 Some(Some(access_list)) => Some(access_list),
558 } {
559 self.tx.set_access_list(access_list);
560 }
561
562 if !fill {
563 return Ok((self.tx, self.state.func));
564 }
565
566 if self.legacy && self.tx.gas_price.is_none() {
567 self.tx.gas_price = Some(self.provider.get_gas_price().await?);
568 }
569
570 if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
571 self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
572 }
573
574 if !self.legacy
575 && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
576 {
577 let estimate = self.provider.estimate_eip1559_fees().await?;
578
579 if self.tx.max_fee_per_gas.is_none() {
580 self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
581 }
582
583 if self.tx.max_priority_fee_per_gas.is_none() {
584 self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
585 }
586 }
587
588 if self.tx.gas.is_none() {
589 self.estimate_gas().await?;
590 }
591
592 Ok((self.tx, self.state.func))
593 }
594
595 async fn estimate_gas(&mut self) -> Result<()> {
597 match self.provider.estimate_gas(self.tx.clone()).await {
598 Ok(estimated) => {
599 self.tx.gas = Some(estimated);
600 Ok(())
601 }
602 Err(err) => {
603 if let TransportError::ErrorResp(payload) = &err {
604 if payload.code == 3
607 && let Some(data) = &payload.data
608 && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
609 {
610 eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
611 }
612 }
613 eyre::bail!("Failed to estimate gas: {}", err)
614 }
615 }
616 }
617
618 async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
620 if self.auth.is_empty() {
621 return Ok(());
622 }
623
624 let auths = std::mem::take(&mut self.auth);
625
626 let address_auth_count =
629 auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
630 if address_auth_count > 1 {
631 eyre::bail!(
632 "Multiple address-based authorizations provided. Only one address can be specified; \
633 use pre-signed authorizations (hex-encoded) for multiple authorizations."
634 );
635 }
636
637 let mut signed_auths = Vec::with_capacity(auths.len());
638
639 for auth in auths {
640 let signed_auth = match auth {
641 CliAuthorizationList::Address(address) => {
642 let auth = Authorization {
643 chain_id: U256::from(self.chain.id()),
644 nonce: tx_nonce + 1,
645 address,
646 };
647
648 let Some(signer) = sender.as_signer() else {
649 eyre::bail!("No signer available to sign authorization");
650 };
651 let signature = signer.sign_hash(&auth.signature_hash()).await?;
652
653 auth.into_signed(signature)
654 }
655 CliAuthorizationList::Signed(auth) => auth,
656 };
657 signed_auths.push(signed_auth);
658 }
659
660 self.tx.set_authorization_list(signed_auths);
661
662 Ok(())
663 }
664}
665
666impl<P, S> CastTxBuilder<P, S>
667where
668 P: Provider<AnyNetwork>,
669{
670 pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
672 let Some(blob_data) = blob_data else { return Ok(self) };
673
674 let mut coder = SidecarBuilder::<SimpleCoder>::default();
675 coder.ingest(&blob_data);
676
677 if self.eip4844 {
678 let sidecar = coder.build()?;
679 alloy_network::TransactionBuilder4844::set_blob_sidecar(&mut self.tx, sidecar);
680 } else {
681 let sidecar = coder.build_7594()?;
682 self.tx.set_blob_sidecar_7594(sidecar);
683 }
684
685 self.tx.populate_blob_hashes();
686
687 Ok(self)
688 }
689}
690
691async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
693 let err_data = serde_json::from_str::<Bytes>(data.get())?;
694 let Some(selector) = err_data.get(..4) else { return Ok(None) };
695 if let Some(known_error) =
696 SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
697 {
698 let mut decoded_error = known_error.name.clone();
699 if !known_error.inputs.is_empty()
700 && let Ok(error) = known_error.decode_error(&err_data)
701 {
702 write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
703 }
704 return Ok(Some(decoded_error));
705 }
706 Ok(None)
707}
708
709pub(crate) async fn signing_provider_with_curl(
714 tx_opts: &SendTxOpts,
715 curl_mode: bool,
716) -> eyre::Result<RetryProviderWithSigner> {
717 let config = tx_opts.eth.load_config()?;
718 let signer = tx_opts.eth.wallet.signer().await?;
719 let wallet = alloy_network::EthereumWallet::from(signer);
720 let provider = get_provider_builder(&config, curl_mode)?.build_with_wallet(wallet)?;
721 if let Some(interval) = tx_opts.poll_interval {
722 provider.client().set_poll_interval(Duration::from_secs(interval))
723 }
724 Ok(provider)
725}