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, print_resolved_fee_token_selection},
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(super) 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(super) 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 expires_at = tx_opts.tempo.resolve_expires();
212 let tempo_sponsor = if print_sponsor_hash || sponsor_url.is_some() {
213 None
214 } else {
215 tx_opts.tempo.sponsor_config().await?
216 };
217
218 if let Some(ref url) = sponsor_url {
219 validate_sponsor_url(url)?;
220 if send_tx.browser.browser {
221 eyre::bail!("--sponsor-url cannot be combined with --browser");
222 }
223 if access_key.is_some() {
224 eyre::bail!("--sponsor-url cannot be combined with a Tempo access key");
225 }
226 }
227
228 let config = send_tx.eth.load_config()?;
229 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
230 if let Some(interval) = send_tx.poll_interval {
231 provider.client().set_poll_interval(Duration::from_secs(interval))
232 }
233
234 let resolved_lane = resolve_lane(&mut tx_opts.tempo, &config.root)?;
235 if let Some(ref ak) = access_key {
236 tx_opts.tempo.key_id = Some(ak.key_address);
237 }
238
239 let builder = CastTxBuilder::new(&provider, tx_opts, &config)
240 .await?
241 .with_to(Some(to))
242 .await?
243 .with_code_sig_and_args(None, Some(sig.to_string()), args)
244 .await?;
245 let chain = builder.chain();
246
247 if print_sponsor_hash {
248 let (tx, from) = if let Some(ref ak) = access_key {
249 let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?;
250 (tx, ak.wallet_address)
251 } else {
252 let signer = pre_resolved_signer.as_ref().ok_or_else(|| {
253 eyre::eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)")
254 })?;
255 let from = signer.address();
256 let (tx, _) = builder.build(signer).await?;
257 (tx, from)
258 };
259 let hash = tx
260 .compute_sponsor_hash(from)
261 .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))?;
262 sh_println!("{hash:?}")?;
263 return Ok(());
264 }
265
266 if let Some(ts) = expires_at {
267 sh_status!("Transaction expires at unix timestamp {ts}")?;
268 }
269
270 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
271 if let Some(browser) = send_tx.browser.run::<TempoNetwork>().await? {
272 let (mut tx, _) = builder.with_browser_wallet().build(browser.address()).await?;
273 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
274 if let Some(gas) = tx.gas_limit() {
275 tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER);
276 }
277 if let Some(sponsor) = &tempo_sponsor {
278 sponsor.attach_and_print::<TempoNetwork>(&mut tx, browser.address()).await?;
279 }
280 print_resolved_fee_token_selection(Some(chain), tx.fee_token())?;
281 let tx_hash = browser.send_transaction_via_browser(tx).await?;
282 CastTxSender::new(&provider)
283 .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout)
284 .await?;
285 } else if let Some(ak) = access_key {
286 let signer = pre_resolved_signer
287 .as_ref()
288 .ok_or_else(|| eyre::eyre!("signer required for access key"))?;
289 let (mut tx, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?;
290 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
291 if let Some(sponsor) = &tempo_sponsor {
292 sponsor.attach_and_print::<TempoNetwork>(&mut tx, ak.wallet_address).await?;
293 }
294 cast_send_with_access_key(
295 &provider,
296 tx,
297 signer,
298 &ak,
299 Some(chain),
300 send_tx.cast_async,
301 send_tx.confirmations,
302 timeout,
303 )
304 .await?;
305 } else if let Some(sponsor_url) = sponsor_url {
306 let signer = match pre_resolved_signer {
307 Some(signer) => signer,
308 None => send_tx.eth.wallet.signer().await?,
309 };
310 let from = signer.address();
311 crate::tx::validate_from_address(send_tx.eth.wallet.from, from)?;
312
313 let (mut tx, _) = builder.build(&signer).await?;
314 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
315 tx.set_fee_payer_signature(FEE_PAYER_SIGNATURE_MARKER);
316
317 let wallet = EthereumWallet::from(signer);
318 let default_rpc = config.get_rpc_url_or_localhost_http()?.into_owned();
319 let default = BuiltInConnectionString::from_str(&default_rpc)?;
320 let relay = BuiltInConnectionString::from_str(&sponsor_url)?;
321 let connector =
322 RelayConnector::with_config(default, relay, SponsorshipMode::SignOnly, false);
323 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
324 .wallet(wallet)
325 .connect_with(&connector)
326 .await?;
327 cast_send(
328 provider,
329 tx,
330 Some(chain),
331 send_tx.cast_async,
332 send_tx.sync,
333 send_tx.confirmations,
334 timeout,
335 )
336 .await?;
337 } else {
338 let signer = match pre_resolved_signer {
339 Some(signer) => signer,
340 None => send_tx.eth.wallet.signer().await?,
341 };
342 let from = signer.address();
343 crate::tx::validate_from_address(send_tx.eth.wallet.from, from)?;
344
345 let (mut tx, _) = builder.build(&signer).await?;
346 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
347 if let Some(sponsor) = &tempo_sponsor {
348 sponsor.attach_and_print::<TempoNetwork>(&mut tx, from).await?;
349 }
350
351 let wallet = EthereumWallet::from(signer);
352 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
353 .wallet(wallet)
354 .connect_provider(&provider);
355 cast_send(
356 provider,
357 tx,
358 Some(chain),
359 send_tx.cast_async,
360 send_tx.sync,
361 send_tx.confirmations,
362 timeout,
363 )
364 .await?;
365 }
366
367 Ok(())
368}
369
370impl TxParams {
371 fn into_transaction_opts(self) -> TransactionOpts {
372 TransactionOpts {
373 gas_limit: self.gas_limit,
374 gas_price: self.gas_price,
375 priority_gas_price: self.priority_gas_price,
376 value: None,
377 nonce: self.nonce,
378 legacy: false,
379 blob: false,
380 eip4844: false,
381 blob_gas_price: None,
382 auth: Vec::new(),
383 access_list: None,
384 tempo: self.tempo,
385 }
386 }
387}