1use std::{path::PathBuf, str::FromStr, time::Duration};
2
3use alloy_consensus::{SignableTransaction, Signed};
4use alloy_ens::NameOrAddress;
5use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
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::{
12 opts::TransactionOpts,
13 utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane},
14};
15use foundry_common::{
16 FoundryTransactionBuilder,
17 fmt::{UIfmt, UIfmtReceiptExt},
18 provider::ProviderBuilder,
19 tempo::TEMPO_BROWSER_GAS_BUFFER,
20};
21use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
22use tempo_alloy::TempoNetwork;
23
24use crate::{
25 cmd::tip20::iso4217_warning_message,
26 tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
27};
28use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
29
30#[derive(Debug, Parser)]
32pub struct SendTxArgs {
33 #[arg(value_parser = NameOrAddress::from_str)]
37 to: Option<NameOrAddress>,
38
39 sig: Option<String>,
41
42 #[arg(allow_negative_numbers = true)]
44 args: Vec<String>,
45
46 #[arg(
48 long,
49 conflicts_with_all = &["sig", "args"]
50 )]
51 data: Option<String>,
52
53 #[command(flatten)]
54 send_tx: SendTxOpts,
55
56 #[command(subcommand)]
57 command: Option<SendTxSubcommands>,
58
59 #[arg(long, requires = "from")]
61 unlocked: bool,
62
63 #[arg(long)]
65 force: bool,
66
67 #[command(flatten)]
68 tx: TransactionOpts,
69
70 #[arg(
72 long,
73 value_name = "BLOB_DATA_PATH",
74 conflicts_with = "legacy",
75 requires = "blob",
76 help_heading = "Transaction options"
77 )]
78 path: Option<PathBuf>,
79}
80
81#[derive(Debug, Parser)]
82pub enum SendTxSubcommands {
83 #[command(name = "--create")]
85 Create {
86 code: String,
88
89 sig: Option<String>,
91
92 #[arg(allow_negative_numbers = true)]
94 args: Vec<String>,
95 },
96}
97
98impl SendTxArgs {
99 pub async fn run(self) -> Result<()> {
100 let (signer, tempo_access_key) = self.send_tx.eth.wallet.maybe_signer().await?;
102
103 if tempo_access_key.is_some() || self.tx.tempo.is_tempo() {
104 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
105 } else {
106 self.run_generic::<Ethereum>(signer, None).await
107 }
108 }
109
110 pub async fn run_generic<N: Network>(
111 self,
112 pre_resolved_signer: Option<WalletSigner>,
113 access_key: Option<TempoAccessKeyConfig>,
114 ) -> Result<()>
115 where
116 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
117 N::UnsignedTx: SignableTransaction<Signature>,
118 N::TransactionRequest: FoundryTransactionBuilder<N>,
119 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
120 {
121 let Self { to, mut sig, mut args, data, send_tx, mut tx, command, unlocked, force, path } =
122 self;
123
124 let print_sponsor_hash = tx.tempo.print_sponsor_hash;
125 let expires_at = tx.tempo.resolve_expires();
126 let tempo_sponsor =
127 if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? };
128
129 let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
130
131 if let Some(data) = data {
132 sig = Some(data);
133 }
134
135 let code = if let Some(SendTxSubcommands::Create {
136 code,
137 sig: constructor_sig,
138 args: constructor_args,
139 }) = command
140 {
141 if to.is_none() && !tx.auth.is_empty() {
144 return Err(eyre!(
145 "EIP-7702 transactions can't be CREATE transactions and require a destination address"
146 ));
147 }
148 if to.is_none() && blob_data.is_some() {
151 return Err(eyre!(
152 "EIP-4844 transactions can't be CREATE transactions and require a destination address"
153 ));
154 }
155
156 sig = constructor_sig;
157 args = constructor_args;
158 Some(code)
159 } else {
160 None
161 };
162
163 if let Some(ref to_addr) = to {
165 let is_factory = match to_addr {
166 NameOrAddress::Address(addr) => *addr == TIP20_FACTORY_ADDRESS,
167 NameOrAddress::Name(name) => {
168 Address::from_str(name).ok() == Some(TIP20_FACTORY_ADDRESS)
169 }
170 };
171
172 if !force
173 && is_factory
174 && let Some(ref sig_str) = sig
175 && sig_str.starts_with("createToken")
176 && let Some(currency) = args.get(2)
177 && !is_iso4217_currency(currency)
178 {
179 sh_warn!("{}", iso4217_warning_message(currency))?;
180 let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
181 if !matches!(response.trim(), "y" | "Y") {
182 sh_println!("Aborted.")?;
183 return Ok(());
184 }
185 }
186 }
187
188 let config = send_tx.eth.load_config()?;
189 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
190
191 let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
192
193 if let Some(interval) = send_tx.poll_interval {
194 provider.client().set_poll_interval(Duration::from_secs(interval))
195 }
196
197 if let Some(ref ak) = access_key {
199 tx.tempo.key_id = Some(ak.key_address);
200 }
201
202 let builder = CastTxBuilder::new(&provider, tx, &config)
203 .await?
204 .with_to(to)
205 .await?
206 .with_code_sig_and_args(code, sig, args)
207 .await?
208 .with_blob_data(blob_data)?;
209
210 if print_sponsor_hash {
212 let (tx, from) = if let Some(ref ak) = access_key {
213 let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?;
214 (tx, ak.wallet_address)
215 } else {
216 let signer = pre_resolved_signer.as_ref().ok_or_else(|| {
219 eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)")
220 })?;
221 let from = signer.address();
222 let (tx, _) = builder.build(from).await?;
223 (tx, from)
224 };
225 let hash = tx
226 .compute_sponsor_hash(from)
227 .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?;
228 sh_println!("{hash:?}")?;
229 return Ok(());
230 }
231
232 if let Some(ts) = expires_at {
233 sh_println!("Transaction expires at unix timestamp {ts}")?;
234 }
235
236 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
237
238 let browser = send_tx.browser.run::<N>().await?;
240
241 if unlocked && browser.is_none() {
246 if let Some(config_chain) = config.chain {
248 let current_chain_id = provider.get_chain_id().await?;
249 let config_chain_id = config_chain.id();
250 if config_chain_id != current_chain_id {
253 sh_warn!("Switching to chain {}", config_chain)?;
254 provider
255 .raw_request::<_, ()>(
256 "wallet_switchEthereumChain".into(),
257 [serde_json::json!({
258 "chainId": format!("0x{:x}", config_chain_id),
259 })],
260 )
261 .await?;
262 }
263 }
264
265 let (mut tx_request, _) = builder.build(config.sender).await?;
266 maybe_print_resolved_lane(
267 resolved_lane.as_ref(),
268 tx_request.nonce().unwrap_or_default(),
269 )?;
270 if let Some(sponsor) = &tempo_sponsor {
271 sponsor.attach_and_print::<N>(&mut tx_request, config.sender).await?;
272 }
273
274 cast_send(
275 provider,
276 tx_request,
277 send_tx.cast_async,
278 send_tx.sync,
279 send_tx.confirmations,
280 timeout,
281 )
282 .await
283 } else if let Some(browser) = browser {
286 let chain = builder.chain();
287 let (mut tx_request, _) = builder.build(browser.address()).await?;
288 maybe_print_resolved_lane(
289 resolved_lane.as_ref(),
290 tx_request.nonce().unwrap_or_default(),
291 )?;
292
293 if chain.is_tempo()
297 && let Some(gas) = tx_request.gas_limit()
298 {
299 tx_request.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER);
300 }
301 if let Some(sponsor) = &tempo_sponsor {
302 sponsor.attach_and_print::<N>(&mut tx_request, browser.address()).await?;
303 }
304
305 let tx_hash = browser.send_transaction_via_browser(tx_request).await?;
306
307 let cast = CastTxSender::new(&provider);
308 cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await
309 } else if let Some(ak) = access_key {
313 let signer = match pre_resolved_signer {
314 Some(s) => s,
315 None => send_tx.eth.wallet.signer().await?,
316 };
317 let (mut tx_request, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?;
318 maybe_print_resolved_lane(
319 resolved_lane.as_ref(),
320 tx_request.nonce().unwrap_or_default(),
321 )?;
322 if let Some(sponsor) = &tempo_sponsor {
323 sponsor.attach_and_print::<N>(&mut tx_request, ak.wallet_address).await?;
324 }
325 cast_send_with_access_key(
326 &provider,
327 tx_request,
328 &signer,
329 &ak,
330 send_tx.cast_async,
331 send_tx.confirmations,
332 timeout,
333 )
334 .await
335 } else {
340 let signer = match pre_resolved_signer {
341 Some(s) => s,
342 None => send_tx.eth.wallet.signer().await?,
343 };
344 let from = signer.address();
345
346 tx::validate_from_address(send_tx.eth.wallet.from, from)?;
347
348 let (mut tx_request, _) = builder.build(&signer).await?;
349 maybe_print_resolved_lane(
350 resolved_lane.as_ref(),
351 tx_request.nonce().unwrap_or_default(),
352 )?;
353
354 if let Some(sponsor) = &tempo_sponsor {
355 sponsor.attach_and_print::<N>(&mut tx_request, from).await?;
356 }
357
358 let wallet = EthereumWallet::from(signer);
359 let provider = AlloyProviderBuilder::<_, _, N>::default()
360 .wallet(wallet)
361 .connect_provider(&provider);
362
363 cast_send(
364 provider,
365 tx_request,
366 send_tx.cast_async,
367 send_tx.sync,
368 send_tx.confirmations,
369 timeout,
370 )
371 .await
372 }
373 }
374}
375
376pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
377 provider: P,
378 tx: N::TransactionRequest,
379 cast_async: bool,
380 sync: bool,
381 confs: u64,
382 timeout: u64,
383) -> Result<()>
384where
385 N::TransactionRequest: FoundryTransactionBuilder<N>,
386 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
387{
388 let cast = CastTxSender::new(provider);
389
390 if sync {
391 let receipt = cast.send_sync(tx).await?;
393 sh_println!("{receipt}")?;
394 } else {
395 let pending_tx = cast.send(tx).await?;
396 let tx_hash = *pending_tx.inner().tx_hash();
397 cast.print_tx_result(tx_hash, cast_async, confs, timeout).await?;
398 }
399
400 Ok(())
401}
402
403pub(crate) async fn cast_send_with_access_key<N: Network, P: Provider<N>>(
410 provider: &P,
411 mut tx: N::TransactionRequest,
412 signer: &WalletSigner,
413 access_key: &TempoAccessKeyConfig,
414 cast_async: bool,
415 confirmations: u64,
416 timeout: u64,
417) -> Result<()>
418where
419 N::TransactionRequest: FoundryTransactionBuilder<N>,
420 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
421{
422 tx.set_from(access_key.wallet_address);
423 tx.set_key_id(access_key.key_address);
424 let raw_tx = tx
425 .sign_with_access_key(
426 provider,
427 signer,
428 access_key.wallet_address,
429 access_key.key_address,
430 access_key.key_authorization.as_ref(),
431 )
432 .await?;
433 let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
434 CastTxSender::new(provider).print_tx_result(tx_hash, cast_async, confirmations, timeout).await
435}