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? {
281 let (mut tx, _) = builder.with_browser_wallet().build(browser.address()).await?;
282 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
283 if let Some(sponsor) = &tempo_sponsor {
284 attach_sponsor(
285 sponsor,
286 (!config.eth_rpc_curl).then_some(&provider),
287 chain,
288 &mut tx,
289 browser.address(),
290 )
291 .await?;
292 } else {
293 let fee_token = resolve_and_set_fee_token(
294 (!config.eth_rpc_curl).then_some(&provider),
295 Some(chain),
296 &mut tx,
297 Some(browser.address()),
298 )
299 .await?;
300 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
301 }
302 let tx_hash = browser.send_transaction_via_browser(tx).await?;
303 CastTxSender::new(&provider)
304 .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout)
305 .await?;
306 } else if let Some(ak) = access_key {
307 let (mut tx, _, prepared) = builder.build_with_tempo_wallet(&ak).await?;
308 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
309 if let Some(sponsor) = &tempo_sponsor {
310 attach_sponsor(
311 sponsor,
312 (!config.eth_rpc_curl).then_some(&provider),
313 chain,
314 &mut tx,
315 prepared.account(),
316 )
317 .await?;
318 }
319 if let Some(sponsor_url) = sponsor_url.as_deref() {
320 cast_send_with_tempo_wallet_via_sponsor(
321 &provider,
322 tx,
323 &prepared,
324 sponsor_url,
325 send_tx.cast_async,
326 send_tx.sync,
327 send_tx.confirmations,
328 timeout,
329 )
330 .await?;
331 } else {
332 cast_send_with_tempo_wallet(
333 &provider,
334 tx,
335 &prepared,
336 tempo_sponsor.is_none().then_some(chain),
337 None,
338 send_tx.cast_async,
339 send_tx.sync,
340 send_tx.confirmations,
341 timeout,
342 tempo_sponsor.is_none() && !config.eth_rpc_curl,
343 )
344 .await?;
345 }
346 } else if let Some(sponsor_url) = sponsor_url {
347 let (signer, _) = resolve_send_signer(pre_resolved_signer, &send_tx.eth).await?;
348
349 let (mut tx, _) = builder.build(&signer).await?;
350 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
351 tx.set_fee_payer_signature(FEE_PAYER_SIGNATURE_MARKER);
352
353 let wallet = EthereumWallet::from(signer);
354 let connector = tempo::sponsor_relay_connector(&provider, &sponsor_url)?;
355 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
356 .wallet(wallet)
357 .connect_with(&connector)
358 .await?;
359 cast_send(
360 provider,
361 tx,
362 None,
363 None,
364 send_tx.cast_async,
365 send_tx.sync,
366 send_tx.confirmations,
367 timeout,
368 false,
369 )
370 .await?;
371 } else {
372 let (signer, from) = resolve_send_signer(pre_resolved_signer, &send_tx.eth).await?;
373
374 let (mut tx, _) = builder.build(&signer).await?;
375 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
376 if let Some(sponsor) = &tempo_sponsor {
377 attach_sponsor(
378 sponsor,
379 (!config.eth_rpc_curl).then_some(&provider),
380 chain,
381 &mut tx,
382 from,
383 )
384 .await?;
385 }
386
387 let wallet = EthereumWallet::from(signer);
388 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
389 .wallet(wallet)
390 .connect_provider(&provider);
391 cast_send(
392 provider,
393 tx,
394 tempo_sponsor.is_none().then_some(chain),
395 None,
396 send_tx.cast_async,
397 send_tx.sync,
398 send_tx.confirmations,
399 timeout,
400 tempo_sponsor.is_none() && !config.eth_rpc_curl,
401 )
402 .await?;
403 }
404
405 Ok(())
406}
407
408async fn resolve_send_signer(
411 pre_resolved: Option<WalletSigner>,
412 eth: &foundry_cli::opts::EthereumOpts,
413) -> eyre::Result<(WalletSigner, Address)> {
414 let signer = match pre_resolved {
415 Some(signer) => signer,
416 None => eth.wallet.signer().await?,
417 };
418 let from = signer.address();
419 crate::tx::validate_from_address(eth.wallet.from, from)?;
420 Ok((signer, from))
421}
422
423async fn attach_sponsor<P>(
425 sponsor: &crate::tempo::TempoSponsor,
426 provider: Option<&P>,
427 chain: foundry_config::Chain,
428 tx: &mut <TempoNetwork as alloy_network::Network>::TransactionRequest,
429 payer: Address,
430) -> eyre::Result<()>
431where
432 P: Provider<TempoNetwork>,
433{
434 sponsor
435 .resolve_and_set_fee_token(
436 provider.map(|p| p as &dyn Provider<TempoNetwork>),
437 Some(chain),
438 tx,
439 )
440 .await?;
441 sponsor.attach_and_print::<TempoNetwork>(tx, payer).await?;
442 Ok(())
443}
444
445impl TxParams {
446 fn into_transaction_opts(self) -> TransactionOpts {
447 TransactionOpts {
448 gas_limit: self.gas_limit,
449 gas_price: self.gas_price,
450 priority_gas_price: self.priority_gas_price,
451 value: None,
452 nonce: self.nonce,
453 legacy: false,
454 blob: false,
455 eip4844: false,
456 blob_gas_price: None,
457 auth: Vec::new(),
458 access_list: None,
459 tempo: self.tempo,
460 }
461 }
462}