1use std::{str::FromStr, time::Duration};
2
3use crate::{
4 cmd::send::{cast_send, cast_send_with_access_key},
5 format_uint_exp,
6 tx::{CastTxSender, SendTxOpts, TxParams},
7};
8use alloy_consensus::{SignableTransaction, Signed};
9use alloy_eips::BlockId;
10use alloy_ens::NameOrAddress;
11use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
12use alloy_primitives::{Address, U256};
13use alloy_provider::{Provider, fillers::RecommendedFillers};
14use alloy_signer::{Signature, Signer};
15use alloy_sol_types::sol;
16use clap::Parser;
17use foundry_cli::{
18 opts::RpcOpts,
19 utils::{LoadConfig, get_chain, get_provider},
20};
21use foundry_common::{
22 FoundryTransactionBuilder,
23 fmt::{UIfmt, UIfmtReceiptExt},
24 provider::{ProviderBuilder, RetryProviderWithSigner},
25 shell,
26 tempo::TEMPO_BROWSER_GAS_BUFFER,
27};
28#[doc(hidden)]
29pub use foundry_config::{Chain, utils::*};
30use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
31use tempo_alloy::TempoNetwork;
32use tempo_contracts::precompiles::PATH_USD_ADDRESS;
33
34sol! {
35 #[sol(rpc)]
36 interface IERC20 {
37 #[derive(Debug)]
38 function name() external view returns (string);
39 function symbol() external view returns (string);
40 function decimals() external view returns (uint8);
41 function totalSupply() external view returns (uint256);
42 function balanceOf(address owner) external view returns (uint256);
43 function transfer(address to, uint256 amount) external returns (bool);
44 function approve(address spender, uint256 amount) external returns (bool);
45 function allowance(address owner, address spender) external view returns (uint256);
46 function mint(address to, uint256 amount) external;
47 function burn(uint256 amount) external;
48 }
49}
50
51pub(crate) fn build_provider_with_signer<N: Network + RecommendedFillers>(
53 tx_opts: &SendTxOpts,
54 signer: WalletSigner,
55) -> eyre::Result<RetryProviderWithSigner<N>>
56where
57 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
58 N::UnsignedTx: SignableTransaction<Signature>,
59{
60 let config = tx_opts.eth.load_config()?;
61 let wallet = EthereumWallet::from(signer);
62 let provider = ProviderBuilder::<N>::from_config(&config)?.build_with_wallet(wallet)?;
63 if let Some(interval) = tx_opts.poll_interval {
64 provider.client().set_poll_interval(Duration::from_secs(interval))
65 }
66 Ok(provider)
67}
68
69#[derive(Debug, Parser, Clone)]
71pub enum Erc20Subcommand {
72 #[command(visible_alias = "b")]
74 Balance {
75 #[arg(value_parser = NameOrAddress::from_str)]
77 token: NameOrAddress,
78
79 #[arg(value_parser = NameOrAddress::from_str)]
81 owner: NameOrAddress,
82
83 #[arg(long, short = 'B')]
85 block: Option<BlockId>,
86
87 #[command(flatten)]
88 rpc: RpcOpts,
89 },
90
91 #[command(visible_aliases = ["t", "send"])]
93 Transfer {
94 #[arg(value_parser = NameOrAddress::from_str)]
96 token: NameOrAddress,
97
98 #[arg(value_parser = NameOrAddress::from_str)]
100 to: NameOrAddress,
101
102 amount: String,
104
105 #[command(flatten)]
106 send_tx: SendTxOpts,
107
108 #[command(flatten)]
109 tx: TxParams,
110 },
111
112 #[command(visible_alias = "a")]
114 Approve {
115 #[arg(value_parser = NameOrAddress::from_str)]
117 token: NameOrAddress,
118
119 #[arg(value_parser = NameOrAddress::from_str)]
121 spender: NameOrAddress,
122
123 amount: String,
125
126 #[command(flatten)]
127 send_tx: SendTxOpts,
128
129 #[command(flatten)]
130 tx: TxParams,
131 },
132
133 #[command(visible_alias = "al")]
135 Allowance {
136 #[arg(value_parser = NameOrAddress::from_str)]
138 token: NameOrAddress,
139
140 #[arg(value_parser = NameOrAddress::from_str)]
142 owner: NameOrAddress,
143
144 #[arg(value_parser = NameOrAddress::from_str)]
146 spender: NameOrAddress,
147
148 #[arg(long, short = 'B')]
150 block: Option<BlockId>,
151
152 #[command(flatten)]
153 rpc: RpcOpts,
154 },
155
156 #[command(visible_alias = "n")]
158 Name {
159 #[arg(value_parser = NameOrAddress::from_str)]
161 token: NameOrAddress,
162
163 #[arg(long, short = 'B')]
165 block: Option<BlockId>,
166
167 #[command(flatten)]
168 rpc: RpcOpts,
169 },
170
171 #[command(visible_alias = "s")]
173 Symbol {
174 #[arg(value_parser = NameOrAddress::from_str)]
176 token: NameOrAddress,
177
178 #[arg(long, short = 'B')]
180 block: Option<BlockId>,
181
182 #[command(flatten)]
183 rpc: RpcOpts,
184 },
185
186 #[command(visible_alias = "d")]
188 Decimals {
189 #[arg(value_parser = NameOrAddress::from_str)]
191 token: NameOrAddress,
192
193 #[arg(long, short = 'B')]
195 block: Option<BlockId>,
196
197 #[command(flatten)]
198 rpc: RpcOpts,
199 },
200
201 #[command(visible_alias = "ts")]
203 TotalSupply {
204 #[arg(value_parser = NameOrAddress::from_str)]
206 token: NameOrAddress,
207
208 #[arg(long, short = 'B')]
210 block: Option<BlockId>,
211
212 #[command(flatten)]
213 rpc: RpcOpts,
214 },
215
216 #[command(visible_alias = "m")]
218 Mint {
219 #[arg(value_parser = NameOrAddress::from_str)]
221 token: NameOrAddress,
222
223 #[arg(value_parser = NameOrAddress::from_str)]
225 to: NameOrAddress,
226
227 amount: String,
229
230 #[command(flatten)]
231 send_tx: SendTxOpts,
232
233 #[command(flatten)]
234 tx: TxParams,
235 },
236
237 #[command(visible_alias = "bu")]
239 Burn {
240 #[arg(value_parser = NameOrAddress::from_str)]
242 token: NameOrAddress,
243
244 amount: String,
246
247 #[command(flatten)]
248 send_tx: SendTxOpts,
249
250 #[command(flatten)]
251 tx: TxParams,
252 },
253}
254
255impl Erc20Subcommand {
256 const fn rpc_opts(&self) -> &RpcOpts {
257 match self {
258 Self::Allowance { rpc, .. } => rpc,
259 Self::Approve { send_tx, .. } => &send_tx.eth.rpc,
260 Self::Balance { rpc, .. } => rpc,
261 Self::Transfer { send_tx, .. } => &send_tx.eth.rpc,
262 Self::Name { rpc, .. } => rpc,
263 Self::Symbol { rpc, .. } => rpc,
264 Self::Decimals { rpc, .. } => rpc,
265 Self::TotalSupply { rpc, .. } => rpc,
266 Self::Mint { send_tx, .. } => &send_tx.eth.rpc,
267 Self::Burn { send_tx, .. } => &send_tx.eth.rpc,
268 }
269 }
270
271 const fn erc20_opts(&self) -> Option<&TxParams> {
272 match self {
273 Self::Approve { tx, .. }
274 | Self::Transfer { tx, .. }
275 | Self::Mint { tx, .. }
276 | Self::Burn { tx, .. } => Some(tx),
277 Self::Allowance { .. }
278 | Self::Balance { .. }
279 | Self::Name { .. }
280 | Self::Symbol { .. }
281 | Self::Decimals { .. }
282 | Self::TotalSupply { .. } => None,
283 }
284 }
285
286 const fn uses_browser_send(&self) -> bool {
287 match self {
288 Self::Transfer { send_tx, .. }
289 | Self::Approve { send_tx, .. }
290 | Self::Mint { send_tx, .. }
291 | Self::Burn { send_tx, .. } => send_tx.browser.browser,
292 _ => false,
293 }
294 }
295
296 async fn should_use_tempo_network(
297 &self,
298 tempo_access_key: &Option<TempoAccessKeyConfig>,
299 ) -> eyre::Result<bool> {
300 if self.erc20_opts().is_some_and(|erc20| erc20.tempo.is_tempo())
301 || tempo_access_key.is_some()
302 {
303 return Ok(true);
304 }
305
306 if self.uses_browser_send() {
307 let config = self.rpc_opts().load_config()?;
308 return Ok(get_chain(config.chain, &get_provider(&config)?).await?.is_tempo());
309 }
310
311 Ok(false)
312 }
313
314 pub async fn run(self) -> eyre::Result<()> {
315 let (signer, tempo_access_key) = match &self {
317 Self::Transfer { send_tx, .. }
318 | Self::Approve { send_tx, .. }
319 | Self::Mint { send_tx, .. }
320 | Self::Burn { send_tx, .. } => {
321 if send_tx.eth.wallet.from.is_some() {
323 let (s, ak) = send_tx.eth.wallet.maybe_signer().await?;
324 (s, ak)
325 } else {
326 (None, None)
327 }
328 }
329 _ => (None, None),
330 };
331
332 let is_tempo = self.should_use_tempo_network(&tempo_access_key).await?;
333
334 if is_tempo {
335 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
336 } else {
337 self.run_generic::<Ethereum>(signer, None).await
338 }
339 }
340
341 pub async fn run_generic<N: Network + RecommendedFillers>(
342 self,
343 pre_resolved_signer: Option<WalletSigner>,
344 tempo_keychain: Option<TempoAccessKeyConfig>,
345 ) -> eyre::Result<()>
346 where
347 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
348 N::UnsignedTx: SignableTransaction<Signature>,
349 N::TransactionRequest: FoundryTransactionBuilder<N>,
350 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
351 {
352 let config = self.rpc_opts().load_config()?;
353
354 macro_rules! erc20_send {
357 (
358 $token:expr,
359 $send_tx:expr,
360 $tx_opts:expr, |
361 $erc20:ident,
362 $provider:ident |
363 $build_tx:expr
364 ) => {{
365 let mut tx_opts = $tx_opts;
366 let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash;
367 let expires_at = tx_opts.tempo.resolve_expires();
368 let tempo_sponsor =
369 if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? };
370 let needs_sponsor_payload = print_sponsor_hash || tempo_sponsor.is_some();
371 if let Some(ts) = expires_at {
372 sh_println!("Transaction expires at unix timestamp {ts}")?;
373 }
374
375 let timeout = $send_tx.timeout.unwrap_or(config.transaction_timeout);
376 if let Some(ref access_key) = tempo_keychain {
377 let signer = pre_resolved_signer
378 .as_ref()
379 .ok_or_else(|| eyre::eyre!("signer required for access key"))?;
380 let $provider =
381 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
382 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
383 let mut tx = { $build_tx }.into_transaction_request();
384 let chain = get_chain(config.chain, &$provider).await?;
385 tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
386 if needs_sponsor_payload {
387 tx.set_key_id(access_key.key_address);
388 tx.prepare_access_key_authorization(
389 &$provider,
390 access_key.wallet_address,
391 access_key.key_address,
392 access_key.key_authorization.as_ref(),
393 )
394 .await?;
395 fill_tx(&$provider, &mut tx, access_key.wallet_address, chain).await?;
396 if print_sponsor_hash {
397 let hash = tx
398 .compute_sponsor_hash(access_key.wallet_address)
399 .ok_or_else(|| {
400 eyre::eyre!(
401 "This network does not support sponsored transactions"
402 )
403 })?;
404 sh_println!("{hash:?}")?;
405 return Ok(());
406 }
407 if let Some(sponsor) = &tempo_sponsor {
408 sponsor
409 .attach_and_print::<TempoNetwork>(
410 &mut tx,
411 access_key.wallet_address,
412 )
413 .await?;
414 }
415 }
416 cast_send_with_access_key(
417 &$provider,
418 tx,
419 signer,
420 access_key,
421 $send_tx.cast_async,
422 $send_tx.confirmations,
423 timeout,
424 )
425 .await?
426 } else if let Some(browser) = $send_tx.browser.run::<N>().await? {
427 let $provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
428 if let Some(interval) = $send_tx.poll_interval {
429 $provider.client().set_poll_interval(Duration::from_secs(interval));
430 }
431 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
432 let mut tx = { $build_tx }.into_transaction_request();
433 let chain = get_chain(config.chain, &$provider).await?;
434 tx_opts.apply::<N>(&mut tx, chain.is_legacy());
435 if chain.is_tempo() && tx.fee_token().is_none() {
436 tx.set_fee_token(PATH_USD_ADDRESS);
437 }
438 fill_tx(&$provider, &mut tx, browser.address(), chain).await?;
439 if print_sponsor_hash {
440 let hash = tx.compute_sponsor_hash(browser.address()).ok_or_else(|| {
441 eyre::eyre!("This network does not support sponsored transactions")
442 })?;
443 sh_println!("{hash:?}")?;
444 return Ok(());
445 }
446 if let Some(sponsor) = &tempo_sponsor {
447 sponsor.attach_and_print::<N>(&mut tx, browser.address()).await?;
448 }
449 let tx_hash = browser.send_transaction_via_browser(tx).await?;
450 CastTxSender::new(&$provider)
451 .print_tx_result(
452 tx_hash,
453 $send_tx.cast_async,
454 $send_tx.confirmations,
455 timeout,
456 )
457 .await?
458 } else {
459 let signer = pre_resolved_signer.unwrap_or($send_tx.eth.wallet.signer().await?);
460 let from = signer.address();
461 let $provider = build_provider_with_signer::<N>(&$send_tx, signer)?;
462 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
463 let mut tx = { $build_tx }.into_transaction_request();
464 let chain = get_chain(config.chain, &$provider).await?;
465 tx_opts.apply::<N>(&mut tx, chain.is_legacy());
466 if needs_sponsor_payload {
467 fill_tx(&$provider, &mut tx, from, chain).await?;
468 if print_sponsor_hash {
469 let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
470 eyre::eyre!("This network does not support sponsored transactions")
471 })?;
472 sh_println!("{hash:?}")?;
473 return Ok(());
474 }
475 if let Some(sponsor) = &tempo_sponsor {
476 sponsor.attach_and_print::<N>(&mut tx, from).await?;
477 }
478 }
479 cast_send(
480 $provider,
481 tx,
482 $send_tx.cast_async,
483 $send_tx.sync,
484 $send_tx.confirmations,
485 timeout,
486 )
487 .await?
488 }
489 }};
490 }
491
492 match self {
493 Self::Allowance { token, owner, spender, block, .. } => {
495 let provider = get_provider(&config)?;
496 let token = token.resolve(&provider).await?;
497 let owner = owner.resolve(&provider).await?;
498 let spender = spender.resolve(&provider).await?;
499
500 let allowance = IERC20::new(token, &provider)
501 .allowance(owner, spender)
502 .block(block.unwrap_or_default())
503 .call()
504 .await?;
505
506 if shell::is_json() {
507 sh_println!("{}", serde_json::to_string(&allowance.to_string())?)?
508 } else {
509 sh_println!("{}", format_uint_exp(allowance))?
510 }
511 }
512 Self::Balance { token, owner, block, .. } => {
513 let provider = get_provider(&config)?;
514 let token = token.resolve(&provider).await?;
515 let owner = owner.resolve(&provider).await?;
516
517 let balance = IERC20::new(token, &provider)
518 .balanceOf(owner)
519 .block(block.unwrap_or_default())
520 .call()
521 .await?;
522
523 if shell::is_json() {
524 sh_println!("{}", serde_json::to_string(&balance.to_string())?)?
525 } else {
526 sh_println!("{}", format_uint_exp(balance))?
527 }
528 }
529 Self::Name { token, block, .. } => {
530 let provider = get_provider(&config)?;
531 let token = token.resolve(&provider).await?;
532
533 let name = IERC20::new(token, &provider)
534 .name()
535 .block(block.unwrap_or_default())
536 .call()
537 .await?;
538
539 if shell::is_json() {
540 sh_println!("{}", serde_json::to_string(&name)?)?
541 } else {
542 sh_println!("{}", name)?
543 }
544 }
545 Self::Symbol { token, block, .. } => {
546 let provider = get_provider(&config)?;
547 let token = token.resolve(&provider).await?;
548
549 let symbol = IERC20::new(token, &provider)
550 .symbol()
551 .block(block.unwrap_or_default())
552 .call()
553 .await?;
554
555 if shell::is_json() {
556 sh_println!("{}", serde_json::to_string(&symbol)?)?
557 } else {
558 sh_println!("{}", symbol)?
559 }
560 }
561 Self::Decimals { token, block, .. } => {
562 let provider = get_provider(&config)?;
563 let token = token.resolve(&provider).await?;
564
565 let decimals = IERC20::new(token, &provider)
566 .decimals()
567 .block(block.unwrap_or_default())
568 .call()
569 .await?;
570 if shell::is_json() {
571 sh_println!("{}", serde_json::to_string(&decimals)?)?
572 } else {
573 sh_println!("{}", decimals)?
574 }
575 }
576 Self::TotalSupply { token, block, .. } => {
577 let provider = get_provider(&config)?;
578 let token = token.resolve(&provider).await?;
579
580 let total_supply = IERC20::new(token, &provider)
581 .totalSupply()
582 .block(block.unwrap_or_default())
583 .call()
584 .await?;
585
586 if shell::is_json() {
587 sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)?
588 } else {
589 sh_println!("{}", format_uint_exp(total_supply))?
590 }
591 }
592 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
594 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
595 erc20.transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
596 })
597 }
598 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
599 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
600 erc20.approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
601 })
602 }
603 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
604 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
605 erc20.mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
606 })
607 }
608 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
609 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
610 erc20.burn(U256::from_str(&amount)?)
611 })
612 }
613 };
614 Ok(())
615 }
616}
617
618async fn fill_tx<N: Network, P: Provider<N>>(
623 provider: &P,
624 tx: &mut N::TransactionRequest,
625 from: Address,
626 chain: Chain,
627) -> eyre::Result<()>
628where
629 N::TransactionRequest: FoundryTransactionBuilder<N>,
630{
631 tx.set_from(from);
632 tx.set_chain_id(chain.id());
633
634 if tx.nonce().is_none() {
635 tx.set_nonce(provider.get_transaction_count(from).await?);
636 }
637
638 let legacy = chain.is_legacy();
639
640 if legacy {
641 if tx.gas_price().is_none() {
642 tx.set_gas_price(provider.get_gas_price().await?);
643 }
644 } else if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
645 let estimate = provider.estimate_eip1559_fees().await?;
646 if tx.max_fee_per_gas().is_none() {
647 tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
648 }
649 if tx.max_priority_fee_per_gas().is_none() {
650 tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
651 }
652 }
653
654 if tx.gas_limit().is_none() {
655 let mut estimated = provider.estimate_gas(tx.clone()).await?;
656
657 if chain.is_tempo() {
661 estimated += TEMPO_BROWSER_GAS_BUFFER;
662 }
663
664 tx.set_gas_limit(estimated);
665 }
666
667 Ok(())
668}