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