1use crate::{
2 cmd::send::{
3 cast_send, cast_send_with_tempo_wallet, cast_send_with_tempo_wallet_via_sponsor,
4 validate_sponsor_url,
5 },
6 tempo,
7 tx::{CastTxBuilder, CastTxSender, SendTxOpts, TxParams},
8};
9use alloy_ens::NameOrAddress;
10use alloy_network::{EthereumWallet, TransactionBuilder};
11use alloy_primitives::{Address, B256};
12use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
13use alloy_signer::Signer;
14use clap::Parser;
15use foundry_cli::{
16 opts::TransactionOpts,
17 utils::{LoadConfig, get_chain, maybe_print_resolved_lane, resolve_lane},
18};
19use foundry_common::{
20 FoundryTransactionBuilder,
21 provider::ProviderBuilder,
22 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
23};
24use foundry_wallets::{TempoAccountsWallet, WalletSigner};
25use std::{str::FromStr, time::Duration};
26use tempo_alloy::TempoNetwork;
27use tempo_primitives::transaction::FEE_PAYER_SIGNATURE_MARKER;
28
29mod create;
30pub(crate) use create::iso4217_warning_message;
31pub(crate) mod logo;
32pub(crate) mod mine;
33
34#[derive(Debug, Parser, Clone)]
36pub enum Tip20Subcommand {
37 #[command(visible_alias = "c")]
39 Create {
40 name: String,
42
43 symbol: String,
45
46 currency: String,
50
51 #[arg(value_parser = NameOrAddress::from_str)]
53 quote_token: NameOrAddress,
54
55 #[arg(value_parser = NameOrAddress::from_str)]
57 admin: NameOrAddress,
58
59 salt: B256,
61
62 #[arg(long, value_name = "URI")]
64 logo_uri: Option<String>,
65
66 #[arg(long)]
68 force: bool,
69
70 #[command(flatten)]
71 send_tx: SendTxOpts,
72
73 #[command(flatten)]
74 tx: TxParams,
75 },
76
77 LogoCheck {
79 #[arg(value_name = "URI")]
81 logo_uri: String,
82 },
83
84 LogoSet {
86 #[arg(value_parser = NameOrAddress::from_str)]
88 token: NameOrAddress,
89
90 #[arg(value_name = "URI")]
92 logo_uri: String,
93
94 #[command(flatten)]
95 send_tx: SendTxOpts,
96
97 #[command(flatten)]
98 tx: TxParams,
99 },
100
101 #[command(visible_alias = "m")]
103 Mine {
104 #[arg(value_name = "ADDRESS")]
106 master: Address,
107
108 #[arg(long, conflicts_with_all = ["seed", "no_random"], value_name = "HEX")]
110 salt: Option<B256>,
111
112 #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
114 threads: Option<usize>,
115
116 #[arg(long, value_name = "HEX")]
118 seed: Option<B256>,
119
120 #[arg(long, conflicts_with = "seed")]
122 no_random: bool,
123
124 #[arg(long, conflicts_with_all = ["seed", "no_random"])]
126 register: bool,
127
128 #[command(flatten)]
129 send_tx: SendTxOpts,
130
131 #[command(flatten)]
132 tx: TxParams,
133 },
134}
135
136impl Tip20Subcommand {
137 pub async fn run(self) -> eyre::Result<()> {
138 match self {
139 Self::Create {
140 name,
141 symbol,
142 currency,
143 quote_token,
144 admin,
145 salt,
146 logo_uri,
147 force,
148 send_tx,
149 tx,
150 } => {
151 create::run(
152 name,
153 symbol,
154 currency,
155 quote_token,
156 admin,
157 salt,
158 logo_uri,
159 force,
160 send_tx,
161 tx,
162 )
163 .await?;
164 }
165 Self::LogoCheck { logo_uri } => {
166 logo::check(logo_uri)?;
167 }
168 Self::LogoSet { token, logo_uri, send_tx, tx } => {
169 logo::set(token, logo_uri, send_tx, tx).await?;
170 }
171 Self::Mine { master, salt, threads, seed, no_random, register, send_tx, tx } => {
172 let output = mine::run(master, salt, threads, seed, no_random)?;
173 if register {
174 mine::register(master, output.salt, send_tx, tx).await?;
175 }
176 }
177 }
178 Ok(())
179 }
180}
181
182pub(crate) async fn resolve_tip20_signer(
183 send_tx: &SendTxOpts,
184 tx_params: &TxParams,
185) -> eyre::Result<(Option<WalletSigner>, Option<TempoAccountsWallet>)> {
186 if tx_params.tempo.session_id()?.is_some() {
187 tempo::ensure_session_not_browser(&tx_params.tempo, send_tx.browser.browser)?;
188 }
189
190 let config = send_tx.eth.load_config()?;
191 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
192 let chain = get_chain(config.chain, &provider).await?;
193 tempo::resolve_session_or_wallet_signer(&tx_params.tempo, &send_tx.eth.wallet, chain.id()).await
194}
195
196pub(crate) async fn send_tip20_transaction(
197 to: NameOrAddress,
198 sig: &'static str,
199 args: Vec<String>,
200 send_tx: SendTxOpts,
201 tx_params: TxParams,
202 pre_resolved_signer: Option<WalletSigner>,
203 access_key: Option<TempoAccountsWallet>,
204) -> eyre::Result<()> {
205 let mut tx_opts = tx_params.into_transaction_opts();
206 let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash;
207 let sponsor_url = tx_opts.tempo.sponsor_url.clone();
208 let sponsor_fee_payer = tx_opts.tempo.sponsor;
209 let expires_at = tx_opts.tempo.resolve_expires();
210 let tempo_sponsor = if print_sponsor_hash || sponsor_url.is_some() {
211 None
212 } else {
213 tx_opts.tempo.sponsor_config().await?
214 };
215
216 if let Some(ref url) = sponsor_url {
217 validate_sponsor_url(url)?;
218 if send_tx.browser.browser {
219 eyre::bail!("--sponsor-url cannot be combined with --browser");
220 }
221 }
222
223 let config = send_tx.eth.load_config()?;
224 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
225 if let Some(interval) = send_tx.poll_interval {
226 provider.client().set_poll_interval(Duration::from_secs(interval))
227 }
228
229 let resolved_lane = resolve_lane(&mut tx_opts.tempo, &config.root)?;
230 if let Some(ref ak) = access_key {
231 tx_opts.tempo.key_id = Some(ak.key_id()?);
232 }
233
234 let builder = CastTxBuilder::new(&provider, tx_opts, &config)
235 .await?
236 .with_to(Some(to))
237 .await?
238 .with_code_sig_and_args(None, Some(sig.to_string()), args)
239 .await?;
240 let chain = builder.chain();
241
242 if print_sponsor_hash {
243 return Box::pin(async {
246 let (mut tx, from) = if let Some(ref ak) = access_key {
247 let (tx, _, prepared) = builder.build_with_tempo_wallet(ak).await?;
248 (tx, prepared.account())
249 } else {
250 let signer = pre_resolved_signer.as_ref().ok_or_else(|| {
251 eyre::eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)")
252 })?;
253 let from = signer.address();
254 let (tx, _) = builder.build(signer).await?;
255 (tx, from)
256 };
257 if let Some(fee_payer) = sponsor_fee_payer {
258 resolve_and_set_fee_token(
259 (!config.eth_rpc_curl).then_some(&provider),
260 Some(chain),
261 &mut tx,
262 Some(fee_payer),
263 )
264 .await?;
265 }
266 let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
267 eyre::eyre!("This network does not support sponsored transactions")
268 })?;
269 sh_println!("{hash:?}")?;
270 eyre::Ok(())
271 })
272 .await;
273 }
274
275 if let Some(ts) = expires_at {
276 sh_status!("Transaction expires at unix timestamp {ts}")?;
277 }
278
279 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
280 if let Some(browser) = send_tx.browser.run::<TempoNetwork>().await? {
282 let (mut tx, _) = Box::pin(builder.with_browser_wallet().build(browser.address())).await?;
283 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
284 if let Some(sponsor) = &tempo_sponsor {
285 attach_sponsor(
286 sponsor,
287 (!config.eth_rpc_curl).then_some(&provider),
288 chain,
289 &mut tx,
290 browser.address(),
291 )
292 .await?;
293 } else {
294 let fee_token = resolve_and_set_fee_token(
295 (!config.eth_rpc_curl).then_some(&provider),
296 Some(chain),
297 &mut tx,
298 Some(browser.address()),
299 )
300 .await?;
301 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
302 }
303 let tx_hash = browser.send_transaction_via_browser(tx).await?;
304 CastTxSender::new(&provider)
305 .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout)
306 .await?;
307 } else if let Some(ak) = access_key {
308 let (mut tx, _, prepared) = Box::pin(builder.build_with_tempo_wallet(&ak)).await?;
309 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
310 if let Some(sponsor) = &tempo_sponsor {
311 attach_sponsor(
312 sponsor,
313 (!config.eth_rpc_curl).then_some(&provider),
314 chain,
315 &mut tx,
316 prepared.account(),
317 )
318 .await?;
319 }
320 if let Some(sponsor_url) = sponsor_url.as_deref() {
321 cast_send_with_tempo_wallet_via_sponsor(
322 &provider,
323 tx,
324 &prepared,
325 sponsor_url,
326 send_tx.cast_async,
327 send_tx.sync,
328 send_tx.confirmations,
329 timeout,
330 )
331 .await?;
332 } else {
333 cast_send_with_tempo_wallet(
334 &provider,
335 tx,
336 &prepared,
337 tempo_sponsor.is_none().then_some(chain),
338 None,
339 send_tx.cast_async,
340 send_tx.sync,
341 send_tx.confirmations,
342 timeout,
343 tempo_sponsor.is_none() && !config.eth_rpc_curl,
344 )
345 .await?;
346 }
347 } else if let Some(sponsor_url) = sponsor_url {
348 let (signer, _) = resolve_send_signer(pre_resolved_signer, &send_tx.eth).await?;
349
350 let (mut tx, _) = Box::pin(builder.build(&signer)).await?;
351 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
352 tx.set_fee_payer_signature(FEE_PAYER_SIGNATURE_MARKER);
353
354 let wallet = EthereumWallet::from(signer);
355 let connector = tempo::sponsor_relay_connector(&provider, &sponsor_url)?;
356 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
357 .wallet(wallet)
358 .connect_with(&connector)
359 .await?;
360 cast_send(
361 provider,
362 tx,
363 None,
364 None,
365 send_tx.cast_async,
366 send_tx.sync,
367 send_tx.confirmations,
368 timeout,
369 false,
370 )
371 .await?;
372 } else {
373 let (signer, from) = resolve_send_signer(pre_resolved_signer, &send_tx.eth).await?;
374
375 let (mut tx, _) = builder.build(&signer).await?;
376 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
377 if let Some(sponsor) = &tempo_sponsor {
378 attach_sponsor(
379 sponsor,
380 (!config.eth_rpc_curl).then_some(&provider),
381 chain,
382 &mut tx,
383 from,
384 )
385 .await?;
386 }
387
388 let wallet = EthereumWallet::from(signer);
389 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
390 .wallet(wallet)
391 .connect_provider(&provider);
392 cast_send(
393 provider,
394 tx,
395 tempo_sponsor.is_none().then_some(chain),
396 None,
397 send_tx.cast_async,
398 send_tx.sync,
399 send_tx.confirmations,
400 timeout,
401 tempo_sponsor.is_none() && !config.eth_rpc_curl,
402 )
403 .await?;
404 }
405
406 Ok(())
407}
408
409async fn resolve_send_signer(
412 pre_resolved: Option<WalletSigner>,
413 eth: &foundry_cli::opts::EthereumOpts,
414) -> eyre::Result<(WalletSigner, Address)> {
415 let signer = match pre_resolved {
416 Some(signer) => signer,
417 None => eth.wallet.signer().await?,
418 };
419 let from = signer.address();
420 crate::tx::validate_from_address(eth.wallet.from, from)?;
421 Ok((signer, from))
422}
423
424async fn attach_sponsor<P>(
426 sponsor: &crate::tempo::TempoSponsor,
427 provider: Option<&P>,
428 chain: foundry_config::Chain,
429 tx: &mut <TempoNetwork as alloy_network::Network>::TransactionRequest,
430 payer: Address,
431) -> eyre::Result<()>
432where
433 P: Provider<TempoNetwork>,
434{
435 sponsor
436 .resolve_and_set_fee_token(
437 provider.map(|p| p as &dyn Provider<TempoNetwork>),
438 Some(chain),
439 tx,
440 )
441 .await?;
442 sponsor.attach_and_print::<TempoNetwork>(tx, payer).await?;
443 Ok(())
444}
445
446impl TxParams {
447 fn into_transaction_opts(self) -> TransactionOpts {
448 TransactionOpts {
449 gas_limit: self.gas_limit,
450 gas_price: self.gas_price,
451 priority_gas_price: self.priority_gas_price,
452 value: None,
453 nonce: self.nonce,
454 legacy: false,
455 blob: false,
456 eip4844: false,
457 blob_gas_price: None,
458 auth: Vec::new(),
459 access_list: None,
460 tempo: self.tempo,
461 }
462 }
463}