1use std::{path::PathBuf, str::FromStr, time::Duration};
2
3use alloy_consensus::{SignableTransaction, Signed};
4use alloy_ens::NameOrAddress;
5use alloy_network::{Ethereum, EthereumWallet, Network};
6use alloy_primitives::Address;
7use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
8use alloy_signer::{Signature, Signer};
9use clap::Parser;
10use eyre::{Result, eyre};
11use foundry_cli::{opts::TransactionOpts, utils::LoadConfig};
12use foundry_common::{
13 FoundryTransactionBuilder,
14 fmt::{UIfmt, UIfmtReceiptExt},
15 provider::ProviderBuilder,
16};
17use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
18use tempo_alloy::TempoNetwork;
19
20use crate::{
21 cmd::tip20::iso4217_warning_message,
22 tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
23};
24use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
25
26#[derive(Debug, Parser)]
28pub struct SendTxArgs {
29 #[arg(value_parser = NameOrAddress::from_str)]
33 to: Option<NameOrAddress>,
34
35 sig: Option<String>,
37
38 #[arg(allow_negative_numbers = true)]
40 args: Vec<String>,
41
42 #[arg(
44 long,
45 conflicts_with_all = &["sig", "args"]
46 )]
47 data: Option<String>,
48
49 #[command(flatten)]
50 send_tx: SendTxOpts,
51
52 #[command(subcommand)]
53 command: Option<SendTxSubcommands>,
54
55 #[arg(long, requires = "from")]
57 unlocked: bool,
58
59 #[arg(long)]
61 force: bool,
62
63 #[command(flatten)]
64 tx: TransactionOpts,
65
66 #[arg(
68 long,
69 value_name = "BLOB_DATA_PATH",
70 conflicts_with = "legacy",
71 requires = "blob",
72 help_heading = "Transaction options"
73 )]
74 path: Option<PathBuf>,
75}
76
77#[derive(Debug, Parser)]
78pub enum SendTxSubcommands {
79 #[command(name = "--create")]
81 Create {
82 code: String,
84
85 sig: Option<String>,
87
88 #[arg(allow_negative_numbers = true)]
90 args: Vec<String>,
91 },
92}
93
94impl SendTxArgs {
95 pub async fn run(self) -> Result<()> {
96 let (signer, tempo_access_key) = self.send_tx.eth.wallet.maybe_signer().await?;
98
99 if tempo_access_key.is_some() || self.tx.tempo.is_tempo() {
100 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
101 } else {
102 self.run_generic::<Ethereum>(signer, None).await
103 }
104 }
105
106 pub async fn run_generic<N: Network>(
107 self,
108 pre_resolved_signer: Option<WalletSigner>,
109 access_key: Option<TempoAccessKeyConfig>,
110 ) -> Result<()>
111 where
112 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
113 N::UnsignedTx: SignableTransaction<Signature>,
114 N::TransactionRequest: FoundryTransactionBuilder<N>,
115 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
116 {
117 let Self { to, mut sig, mut args, data, send_tx, mut tx, command, unlocked, force, path } =
118 self;
119
120 let print_sponsor_hash = tx.tempo.print_sponsor_hash;
121 let sponsor_signature = tx.tempo.sponsor_signature;
122
123 let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
124
125 if let Some(data) = data {
126 sig = Some(data);
127 }
128
129 let code = if let Some(SendTxSubcommands::Create {
130 code,
131 sig: constructor_sig,
132 args: constructor_args,
133 }) = command
134 {
135 if to.is_none() && !tx.auth.is_empty() {
138 return Err(eyre!(
139 "EIP-7702 transactions can't be CREATE transactions and require a destination address"
140 ));
141 }
142 if to.is_none() && blob_data.is_some() {
145 return Err(eyre!(
146 "EIP-4844 transactions can't be CREATE transactions and require a destination address"
147 ));
148 }
149
150 sig = constructor_sig;
151 args = constructor_args;
152 Some(code)
153 } else {
154 None
155 };
156
157 if let Some(ref to_addr) = to {
159 let is_factory = match to_addr {
160 NameOrAddress::Address(addr) => *addr == TIP20_FACTORY_ADDRESS,
161 NameOrAddress::Name(name) => {
162 Address::from_str(name).ok() == Some(TIP20_FACTORY_ADDRESS)
163 }
164 };
165
166 if !force
167 && is_factory
168 && let Some(ref sig_str) = sig
169 && sig_str.starts_with("createToken")
170 && let Some(currency) = args.get(2)
171 && !is_iso4217_currency(currency)
172 {
173 sh_warn!("{}", iso4217_warning_message(currency))?;
174 let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
175 if !matches!(response.trim(), "y" | "Y") {
176 sh_println!("Aborted.")?;
177 return Ok(());
178 }
179 }
180 }
181
182 let config = send_tx.eth.load_config()?;
183 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
184
185 if let Some(interval) = send_tx.poll_interval {
186 provider.client().set_poll_interval(Duration::from_secs(interval))
187 }
188
189 if let Some(ref ak) = access_key {
191 tx.tempo.key_id = Some(ak.key_address);
192 }
193
194 let builder = CastTxBuilder::new(&provider, tx, &config)
195 .await?
196 .with_to(to)
197 .await?
198 .with_code_sig_and_args(code, sig, args)
199 .await?
200 .with_blob_data(blob_data)?;
201
202 if print_sponsor_hash {
204 let signer = pre_resolved_signer.as_ref().ok_or_else(|| {
207 eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)")
208 })?;
209 let from = signer.address();
210 let (tx, _) = builder.build(from).await?;
211 let hash = tx
212 .compute_sponsor_hash(from)
213 .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?;
214 sh_println!("{hash:?}")?;
215 return Ok(());
216 }
217
218 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
219
220 let browser = send_tx.browser.run::<N>().await?;
222
223 if unlocked && browser.is_none() {
228 if let Some(config_chain) = config.chain {
230 let current_chain_id = provider.get_chain_id().await?;
231 let config_chain_id = config_chain.id();
232 if config_chain_id != current_chain_id {
235 sh_warn!("Switching to chain {}", config_chain)?;
236 provider
237 .raw_request::<_, ()>(
238 "wallet_switchEthereumChain".into(),
239 [serde_json::json!({
240 "chainId": format!("0x{:x}", config_chain_id),
241 })],
242 )
243 .await?;
244 }
245 }
246
247 let (tx, _) = builder.build(config.sender).await?;
248
249 cast_send(
250 provider,
251 tx,
252 send_tx.cast_async,
253 send_tx.sync,
254 send_tx.confirmations,
255 timeout,
256 )
257 .await
258 } else if let Some(browser) = browser {
261 let (tx_request, _) = builder.build(browser.address()).await?;
262 let tx_hash = browser.send_transaction_via_browser(tx_request).await?;
263
264 let cast = CastTxSender::new(&provider);
265 cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await
266 } else if let Some(ak) = access_key {
270 let signer = match pre_resolved_signer {
271 Some(s) => s,
272 None => send_tx.eth.wallet.signer().await?,
273 };
274 let from = ak.wallet_address;
275
276 let (tx_request, _) = builder.build(from).await?;
277
278 let raw_tx = tx_request
279 .sign_with_access_key(
280 &provider,
281 &signer,
282 ak.wallet_address,
283 ak.key_address,
284 ak.key_authorization.as_ref(),
285 )
286 .await?;
287
288 let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
289
290 let cast = CastTxSender::new(&provider);
291 cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await
292 } else {
297 let signer = match pre_resolved_signer {
298 Some(s) => s,
299 None => send_tx.eth.wallet.signer().await?,
300 };
301 let from = signer.address();
302
303 tx::validate_from_address(send_tx.eth.wallet.from, from)?;
304
305 let (mut tx_request, _) = builder.build(&signer).await?;
306
307 if let Some(sig) = sponsor_signature {
310 tx_request.set_fee_payer_signature(sig);
311 }
312
313 let wallet = EthereumWallet::from(signer);
314 let provider = AlloyProviderBuilder::<_, _, N>::default()
315 .wallet(wallet)
316 .connect_provider(&provider);
317
318 cast_send(
319 provider,
320 tx_request,
321 send_tx.cast_async,
322 send_tx.sync,
323 send_tx.confirmations,
324 timeout,
325 )
326 .await
327 }
328 }
329}
330
331pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
332 provider: P,
333 tx: N::TransactionRequest,
334 cast_async: bool,
335 sync: bool,
336 confs: u64,
337 timeout: u64,
338) -> Result<()>
339where
340 N::TransactionRequest: FoundryTransactionBuilder<N>,
341 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
342{
343 let cast = CastTxSender::new(provider);
344
345 if sync {
346 let receipt = cast.send_sync(tx).await?;
348 sh_println!("{receipt}")?;
349 } else {
350 let pending_tx = cast.send(tx).await?;
351 let tx_hash = *pending_tx.inner().tx_hash();
352 cast.print_tx_result(tx_hash, cast_async, confs, timeout).await?;
353 }
354
355 Ok(())
356}