1use crate::{
2 tx::{self, CastTxBuilder},
3 Cast,
4};
5use alloy_ens::NameOrAddress;
6use alloy_network::{AnyNetwork, EthereumWallet};
7use alloy_provider::{Provider, ProviderBuilder};
8use alloy_rpc_types::TransactionRequest;
9use alloy_serde::WithOtherFields;
10use alloy_signer::Signer;
11use clap::Parser;
12use eyre::{eyre, Result};
13use foundry_cli::{
14 opts::{EthereumOpts, TransactionOpts},
15 utils,
16 utils::LoadConfig,
17};
18use std::{path::PathBuf, str::FromStr};
19
20#[derive(Debug, Parser)]
22pub struct SendTxArgs {
23 #[arg(value_parser = NameOrAddress::from_str)]
27 to: Option<NameOrAddress>,
28
29 sig: Option<String>,
31
32 args: Vec<String>,
34
35 #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
37 cast_async: bool,
38
39 #[arg(long, default_value = "1")]
41 confirmations: u64,
42
43 #[command(subcommand)]
44 command: Option<SendTxSubcommands>,
45
46 #[arg(long, requires = "from")]
48 unlocked: bool,
49
50 #[arg(long, env = "ETH_TIMEOUT")]
52 pub timeout: Option<u64>,
53
54 #[command(flatten)]
55 tx: TransactionOpts,
56
57 #[command(flatten)]
58 eth: EthereumOpts,
59
60 #[arg(
62 long,
63 value_name = "BLOB_DATA_PATH",
64 conflicts_with = "legacy",
65 requires = "blob",
66 help_heading = "Transaction options"
67 )]
68 path: Option<PathBuf>,
69}
70
71#[derive(Debug, Parser)]
72pub enum SendTxSubcommands {
73 #[command(name = "--create")]
75 Create {
76 code: String,
78
79 sig: Option<String>,
81
82 args: Vec<String>,
84 },
85}
86
87impl SendTxArgs {
88 #[expect(dependency_on_unit_never_type_fallback)]
89 pub async fn run(self) -> eyre::Result<()> {
90 let Self {
91 eth,
92 to,
93 mut sig,
94 cast_async,
95 mut args,
96 tx,
97 confirmations,
98 command,
99 unlocked,
100 path,
101 timeout,
102 } = self;
103
104 let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
105
106 let code = if let Some(SendTxSubcommands::Create {
107 code,
108 sig: constructor_sig,
109 args: constructor_args,
110 }) = command
111 {
112 if to.is_none() && tx.auth.is_some() {
115 return Err(eyre!("EIP-7702 transactions can't be CREATE transactions and require a destination address"));
116 }
117 if to.is_none() && blob_data.is_some() {
120 return Err(eyre!("EIP-4844 transactions can't be CREATE transactions and require a destination address"));
121 }
122
123 sig = constructor_sig;
124 args = constructor_args;
125 Some(code)
126 } else {
127 None
128 };
129
130 let config = eth.load_config()?;
131 let provider = utils::get_provider(&config)?;
132
133 let builder = CastTxBuilder::new(&provider, tx, &config)
134 .await?
135 .with_to(to)
136 .await?
137 .with_code_sig_and_args(code, sig, args)
138 .await?
139 .with_blob_data(blob_data)?;
140
141 let timeout = timeout.unwrap_or(config.transaction_timeout);
142
143 if unlocked {
148 if let Some(config_chain) = config.chain {
150 let current_chain_id = provider.get_chain_id().await?;
151 let config_chain_id = config_chain.id();
152 if config_chain_id != current_chain_id {
155 sh_warn!("Switching to chain {}", config_chain)?;
156 provider
157 .raw_request(
158 "wallet_switchEthereumChain".into(),
159 [serde_json::json!({
160 "chainId": format!("0x{:x}", config_chain_id),
161 })],
162 )
163 .await?;
164 }
165 }
166
167 let (tx, _) = builder.build(config.sender).await?;
168
169 cast_send(provider, tx, cast_async, confirmations, timeout).await
170 } else {
175 let signer = eth.wallet.signer().await?;
177 let from = signer.address();
178
179 tx::validate_from_address(eth.wallet.from, from)?;
180
181 let (tx, _) = builder.build(&signer).await?;
182
183 let wallet = EthereumWallet::from(signer);
184 let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
185 .wallet(wallet)
186 .connect_provider(&provider);
187
188 cast_send(provider, tx, cast_async, confirmations, timeout).await
189 }
190 }
191}
192
193async fn cast_send<P: Provider<AnyNetwork>>(
194 provider: P,
195 tx: WithOtherFields<TransactionRequest>,
196 cast_async: bool,
197 confs: u64,
198 timeout: u64,
199) -> Result<()> {
200 let cast = Cast::new(provider);
201 let pending_tx = cast.send(tx).await?;
202
203 let tx_hash = pending_tx.inner().tx_hash();
204
205 if cast_async {
206 sh_println!("{tx_hash:#x}")?;
207 } else {
208 let receipt =
209 cast.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
210 sh_println!("{receipt}")?;
211 }
212
213 Ok(())
214}