1use std::{str::FromStr, time::Duration};
2
3use crate::{
4 cmd::{
5 call_overrides::CallOverrideOpts,
6 send::{cast_send, cast_send_with_access_key},
7 },
8 format_uint_exp, tempo,
9 tx::{CastTxSender, SendTxOpts, TxParams, fill_transaction_gas_fees},
10};
11use alloy_consensus::{SignableTransaction, Signed};
12use alloy_eips::BlockId;
13use alloy_ens::NameOrAddress;
14use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
15use alloy_primitives::{Address, U256};
16use alloy_provider::{Provider, fillers::RecommendedFillers};
17use alloy_signer::{Signature, Signer};
18use alloy_sol_types::sol;
19use clap::Parser;
20use foundry_cli::{
21 json::{print_json_success, print_scalar},
22 opts::RpcOpts,
23 utils::{LoadConfig, get_chain, get_provider},
24};
25use foundry_common::{
26 FoundryTransactionBuilder,
27 fmt::{UIfmt, UIfmtReceiptExt},
28 provider::{ProviderBuilder, RetryProviderWithSigner},
29 shell,
30 tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_fee_token, resolve_and_set_fee_token},
31};
32#[doc(hidden)]
33pub use foundry_config::{Chain, Eip1559FeeEstimatePreset, utils::*};
34use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
35use tempo_alloy::TempoNetwork;
36
37sol! {
38 #[sol(rpc)]
39 interface IERC20 {
40 #[derive(Debug)]
41 function name() external view returns (string);
42 function symbol() external view returns (string);
43 function decimals() external view returns (uint8);
44 function totalSupply() external view returns (uint256);
45 function balanceOf(address owner) external view returns (uint256);
46 function transfer(address to, uint256 amount) external returns (bool);
47 function approve(address spender, uint256 amount) external returns (bool);
48 function allowance(address owner, address spender) external view returns (uint256);
49 function mint(address to, uint256 amount) external;
50 function burn(uint256 amount) external;
51 }
52}
53
54pub(crate) fn build_provider_with_signer<N: Network + RecommendedFillers>(
56 tx_opts: &SendTxOpts,
57 signer: WalletSigner,
58) -> eyre::Result<RetryProviderWithSigner<N>>
59where
60 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
61 N::UnsignedTx: SignableTransaction<Signature>,
62{
63 let config = tx_opts.eth.load_config()?;
64 let wallet = EthereumWallet::from(signer);
65 let provider = ProviderBuilder::<N>::from_config(&config)?.build_with_wallet(wallet)?;
66 if let Some(interval) = tx_opts.poll_interval {
67 provider.client().set_poll_interval(Duration::from_secs(interval))
68 }
69 Ok(provider)
70}
71
72#[derive(Debug, Parser, Clone)]
74pub enum Erc20Subcommand {
75 #[command(visible_alias = "b")]
77 Balance {
78 #[arg(value_parser = NameOrAddress::from_str)]
80 token: NameOrAddress,
81
82 #[arg(value_parser = NameOrAddress::from_str)]
84 owner: NameOrAddress,
85
86 #[arg(long, short = 'B')]
88 block: Option<BlockId>,
89
90 #[command(flatten)]
91 rpc: RpcOpts,
92
93 #[command(flatten)]
94 overrides: CallOverrideOpts,
95 },
96
97 #[command(visible_aliases = ["t", "send"])]
99 Transfer {
100 #[arg(value_parser = NameOrAddress::from_str)]
102 token: NameOrAddress,
103
104 #[arg(value_parser = NameOrAddress::from_str)]
106 to: NameOrAddress,
107
108 amount: String,
110
111 #[command(flatten)]
112 send_tx: SendTxOpts,
113
114 #[command(flatten)]
115 tx: TxParams,
116 },
117
118 #[command(visible_alias = "a")]
120 Approve {
121 #[arg(value_parser = NameOrAddress::from_str)]
123 token: NameOrAddress,
124
125 #[arg(value_parser = NameOrAddress::from_str)]
127 spender: NameOrAddress,
128
129 amount: String,
131
132 #[command(flatten)]
133 send_tx: SendTxOpts,
134
135 #[command(flatten)]
136 tx: TxParams,
137 },
138
139 #[command(visible_alias = "al")]
141 Allowance {
142 #[arg(value_parser = NameOrAddress::from_str)]
144 token: NameOrAddress,
145
146 #[arg(value_parser = NameOrAddress::from_str)]
148 owner: NameOrAddress,
149
150 #[arg(value_parser = NameOrAddress::from_str)]
152 spender: NameOrAddress,
153
154 #[arg(long, short = 'B')]
156 block: Option<BlockId>,
157
158 #[command(flatten)]
159 rpc: RpcOpts,
160 },
161
162 #[command(visible_alias = "n")]
164 Name {
165 #[arg(value_parser = NameOrAddress::from_str)]
167 token: NameOrAddress,
168
169 #[arg(long, short = 'B')]
171 block: Option<BlockId>,
172
173 #[command(flatten)]
174 rpc: RpcOpts,
175 },
176
177 #[command(visible_alias = "s")]
179 Symbol {
180 #[arg(value_parser = NameOrAddress::from_str)]
182 token: NameOrAddress,
183
184 #[arg(long, short = 'B')]
186 block: Option<BlockId>,
187
188 #[command(flatten)]
189 rpc: RpcOpts,
190 },
191
192 #[command(visible_alias = "d")]
194 Decimals {
195 #[arg(value_parser = NameOrAddress::from_str)]
197 token: NameOrAddress,
198
199 #[arg(long, short = 'B')]
201 block: Option<BlockId>,
202
203 #[command(flatten)]
204 rpc: RpcOpts,
205 },
206
207 #[command(visible_alias = "ts")]
209 TotalSupply {
210 #[arg(value_parser = NameOrAddress::from_str)]
212 token: NameOrAddress,
213
214 #[arg(long, short = 'B')]
216 block: Option<BlockId>,
217
218 #[command(flatten)]
219 rpc: RpcOpts,
220 },
221
222 #[command(visible_alias = "m")]
224 Mint {
225 #[arg(value_parser = NameOrAddress::from_str)]
227 token: NameOrAddress,
228
229 #[arg(value_parser = NameOrAddress::from_str)]
231 to: NameOrAddress,
232
233 amount: String,
235
236 #[command(flatten)]
237 send_tx: SendTxOpts,
238
239 #[command(flatten)]
240 tx: TxParams,
241 },
242
243 #[command(visible_alias = "bu")]
245 Burn {
246 #[arg(value_parser = NameOrAddress::from_str)]
248 token: NameOrAddress,
249
250 amount: String,
252
253 #[command(flatten)]
254 send_tx: SendTxOpts,
255
256 #[command(flatten)]
257 tx: TxParams,
258 },
259}
260
261impl Erc20Subcommand {
262 const fn rpc_opts(&self) -> &RpcOpts {
263 match self {
264 Self::Allowance { rpc, .. } => rpc,
265 Self::Approve { send_tx, .. } => &send_tx.eth.rpc,
266 Self::Balance { rpc, .. } => rpc,
267 Self::Transfer { send_tx, .. } => &send_tx.eth.rpc,
268 Self::Name { rpc, .. } => rpc,
269 Self::Symbol { rpc, .. } => rpc,
270 Self::Decimals { rpc, .. } => rpc,
271 Self::TotalSupply { rpc, .. } => rpc,
272 Self::Mint { send_tx, .. } => &send_tx.eth.rpc,
273 Self::Burn { send_tx, .. } => &send_tx.eth.rpc,
274 }
275 }
276
277 const fn erc20_opts(&self) -> Option<&TxParams> {
278 match self {
279 Self::Approve { tx, .. }
280 | Self::Transfer { tx, .. }
281 | Self::Mint { tx, .. }
282 | Self::Burn { tx, .. } => Some(tx),
283 Self::Allowance { .. }
284 | Self::Balance { .. }
285 | Self::Name { .. }
286 | Self::Symbol { .. }
287 | Self::Decimals { .. }
288 | Self::TotalSupply { .. } => None,
289 }
290 }
291
292 const fn uses_browser_send(&self) -> bool {
293 match self {
294 Self::Transfer { send_tx, .. }
295 | Self::Approve { send_tx, .. }
296 | Self::Mint { send_tx, .. }
297 | Self::Burn { send_tx, .. } => send_tx.browser.browser,
298 _ => false,
299 }
300 }
301
302 async fn should_use_tempo_network(
303 &self,
304 tempo_access_key: &Option<TempoAccessKeyConfig>,
305 has_session: bool,
306 ) -> eyre::Result<bool> {
307 if self.erc20_opts().is_some_and(|erc20| erc20.tempo.is_tempo())
308 || has_session
309 || tempo_access_key.is_some()
310 {
311 return Ok(true);
312 }
313
314 if self.uses_browser_send() {
315 let config = self.rpc_opts().load_config()?;
316 return Ok(get_chain(config.chain, &get_provider(&config)?).await?.is_tempo());
317 }
318
319 Ok(false)
320 }
321
322 fn has_tempo_session(&self) -> eyre::Result<bool> {
323 self.erc20_opts().map_or(Ok(false), |opts| opts.tempo.session_id().map(|id| id.is_some()))
324 }
325
326 pub async fn run(self) -> eyre::Result<()> {
327 let has_session = self.has_tempo_session()?;
328 let (signer, tempo_access_key) = match &self {
330 Self::Transfer { send_tx, .. }
331 | Self::Approve { send_tx, .. }
332 | Self::Mint { send_tx, .. }
333 | Self::Burn { send_tx, .. } => {
334 if !has_session && send_tx.eth.wallet.from.is_some() {
338 let (s, ak) = send_tx.eth.wallet.maybe_signer().await?;
339 (s, ak)
340 } else {
341 (None, None)
342 }
343 }
344 _ => (None, None),
345 };
346
347 let is_tempo = self.should_use_tempo_network(&tempo_access_key, has_session).await?;
348
349 if is_tempo {
350 self.run_generic::<TempoNetwork>(signer, tempo_access_key, has_session).await
351 } else {
352 self.run_generic::<Ethereum>(signer, None, has_session).await
353 }
354 }
355
356 #[allow(clippy::large_stack_frames)]
357 pub async fn run_generic<N: Network + RecommendedFillers>(
358 self,
359 pre_resolved_signer: Option<WalletSigner>,
360 tempo_keychain: Option<TempoAccessKeyConfig>,
361 has_session: bool,
362 ) -> eyre::Result<()>
363 where
364 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
365 N::UnsignedTx: SignableTransaction<Signature>,
366 N::TransactionRequest: FoundryTransactionBuilder<N>,
367 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
368 {
369 let config = self.rpc_opts().load_config()?;
370
371 macro_rules! erc20_send {
374 (
375 $token:expr,
376 $send_tx:expr,
377 $tx_opts:expr, |
378 $erc20:ident,
379 $provider:ident |
380 $build_tx:expr
381 ) => {{
382 let mut tx_opts = $tx_opts;
383 tempo::ensure_session_not_browser(&tx_opts.tempo, $send_tx.browser.browser)?;
384 let (pre_resolved_signer, tempo_keychain) = if has_session {
385 let $provider =
386 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
387 let chain = get_chain(config.chain, &$provider).await?;
388 tempo::resolve_session_or_wallet_signer(
389 &tx_opts.tempo,
390 &$send_tx.eth.wallet,
391 chain.id(),
392 )
393 .await?
394 } else {
395 (pre_resolved_signer, tempo_keychain)
396 };
397 let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash;
398 let sponsor_fee_payer = tx_opts.tempo.sponsor;
399 let expires_at = tx_opts.tempo.resolve_expires();
400 let tempo_sponsor =
401 if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? };
402 let needs_sponsor_payload = print_sponsor_hash || tempo_sponsor.is_some();
403 if let Some(ts) = expires_at {
404 sh_status!("Transaction expires at unix timestamp {ts}")?;
405 }
406
407 let timeout = $send_tx.timeout.unwrap_or(config.transaction_timeout);
408 if let Some(ref access_key) = tempo_keychain {
409 let signer = pre_resolved_signer
410 .as_ref()
411 .ok_or_else(|| eyre::eyre!("signer required for access key"))?;
412 let $provider =
413 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
414 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
415 let mut tx = { $build_tx }.into_transaction_request();
416 let chain = get_chain(config.chain, &$provider).await?;
417 tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
418 tempo::fill_access_key_transaction(
419 &$provider,
420 &mut tx,
421 access_key,
422 chain,
423 config.eip1559_fee_estimate,
424 )
425 .await?;
426 if needs_sponsor_payload {
427 if print_sponsor_hash {
428 if let Some(fee_payer) = sponsor_fee_payer {
429 resolve_and_set_fee_token(
430 (!config.eth_rpc_curl).then_some(&$provider),
431 Some(chain),
432 &mut tx,
433 Some(fee_payer),
434 )
435 .await?;
436 }
437 let hash = tx
438 .compute_sponsor_hash(access_key.wallet_address)
439 .ok_or_else(|| {
440 eyre::eyre!(
441 "This network does not support sponsored transactions"
442 )
443 })?;
444 sh_println!("{hash:?}")?;
445 return Ok(());
446 }
447 if let Some(sponsor) = &tempo_sponsor {
448 sponsor
449 .resolve_and_set_fee_token(
450 (!config.eth_rpc_curl).then_some(&$provider),
451 Some(chain),
452 &mut tx,
453 )
454 .await?;
455 sponsor
456 .attach_and_print::<TempoNetwork>(
457 &mut tx,
458 access_key.wallet_address,
459 )
460 .await?;
461 }
462 }
463 cast_send_with_access_key(
464 &$provider,
465 tx,
466 signer,
467 access_key,
468 tempo_sponsor.is_none().then_some(chain),
469 None,
470 $send_tx.cast_async,
471 $send_tx.confirmations,
472 timeout,
473 tempo_sponsor.is_none() && !config.eth_rpc_curl,
474 )
475 .await?;
476 } else if let Some(browser) = $send_tx.browser.run::<N>().await? {
477 let $provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
478 if let Some(interval) = $send_tx.poll_interval {
479 $provider.client().set_poll_interval(Duration::from_secs(interval));
480 }
481 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
482 let mut tx = { $build_tx }.into_transaction_request();
483 let chain = get_chain(config.chain, &$provider).await?;
484 tx_opts.apply::<N>(&mut tx, chain.is_legacy());
485 fill_tx(
486 &$provider,
487 &mut tx,
488 browser.address(),
489 chain,
490 true,
491 config.eip1559_fee_estimate,
492 )
493 .await?;
494 if print_sponsor_hash {
495 if let Some(fee_payer) = sponsor_fee_payer {
496 resolve_and_set_fee_token(
497 (!config.eth_rpc_curl).then_some(&$provider),
498 Some(chain),
499 &mut tx,
500 Some(fee_payer),
501 )
502 .await?;
503 }
504 let hash = tx.compute_sponsor_hash(browser.address()).ok_or_else(|| {
505 eyre::eyre!("This network does not support sponsored transactions")
506 })?;
507 sh_println!("{hash:?}")?;
508 return Ok(());
509 }
510 if let Some(sponsor) = &tempo_sponsor {
511 sponsor
512 .resolve_and_set_fee_token(
513 (!config.eth_rpc_curl).then_some(&$provider),
514 Some(chain),
515 &mut tx,
516 )
517 .await?;
518 sponsor.attach_and_print::<N>(&mut tx, browser.address()).await?;
519 } else {
520 let fee_token = resolve_and_set_fee_token(
521 (!config.eth_rpc_curl).then_some(&$provider),
522 Some(chain),
523 &mut tx,
524 Some(browser.address()),
525 )
526 .await?;
527 maybe_print_fee_token(
528 (!config.eth_rpc_curl).then_some(&$provider),
529 fee_token,
530 )
531 .await?;
532 }
533 let tx_hash = browser.send_transaction_via_browser(tx).await?;
534 CastTxSender::new(&$provider)
535 .print_tx_result(
536 tx_hash,
537 $send_tx.cast_async,
538 $send_tx.confirmations,
539 timeout,
540 )
541 .await?
542 } else {
543 let signer = pre_resolved_signer.unwrap_or($send_tx.eth.wallet.signer().await?);
544 let from = signer.address();
545 let $provider = build_provider_with_signer::<N>(&$send_tx, signer)?;
546 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
547 let mut tx = { $build_tx }.into_transaction_request();
548 let chain = get_chain(config.chain, &$provider).await?;
549 tx_opts.apply::<N>(&mut tx, chain.is_legacy());
550 if needs_sponsor_payload {
551 fill_tx(
552 &$provider,
553 &mut tx,
554 from,
555 chain,
556 false,
557 config.eip1559_fee_estimate,
558 )
559 .await?;
560 if print_sponsor_hash {
561 if let Some(fee_payer) = sponsor_fee_payer {
562 resolve_and_set_fee_token(
563 (!config.eth_rpc_curl).then_some(&$provider),
564 Some(chain),
565 &mut tx,
566 Some(fee_payer),
567 )
568 .await?;
569 }
570 let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
571 eyre::eyre!("This network does not support sponsored transactions")
572 })?;
573 sh_println!("{hash:?}")?;
574 return Ok(());
575 }
576 if let Some(sponsor) = &tempo_sponsor {
577 sponsor
578 .resolve_and_set_fee_token(
579 (!config.eth_rpc_curl).then_some(&$provider),
580 Some(chain),
581 &mut tx,
582 )
583 .await?;
584 sponsor.attach_and_print::<N>(&mut tx, from).await?;
585 }
586 } else {
587 fill_transaction_gas_fees(
589 &$provider,
590 &mut tx,
591 chain.is_legacy(),
592 false,
593 config.eip1559_fee_estimate,
594 )
595 .await?;
596 }
597 cast_send(
598 $provider,
599 tx,
600 tempo_sponsor.is_none().then_some(chain),
601 None,
602 $send_tx.cast_async,
603 $send_tx.sync,
604 $send_tx.confirmations,
605 timeout,
606 tempo_sponsor.is_none() && !config.eth_rpc_curl,
607 )
608 .await?;
609 }
610 }};
611 }
612
613 match self {
614 Self::Allowance { token, owner, spender, block, .. } => {
616 let provider = get_provider(&config)?;
617 let token = token.resolve(&provider).await?;
618 let owner = owner.resolve(&provider).await?;
619 let spender = spender.resolve(&provider).await?;
620
621 let allowance = IERC20::new(token, &provider)
622 .allowance(owner, spender)
623 .block(block.unwrap_or_default())
624 .call()
625 .await?;
626
627 if shell::is_json() {
628 print_json_success(allowance.to_string())?;
629 } else {
630 sh_println!("{}", format_uint_exp(allowance))?;
631 }
632 }
633 Self::Balance { token, owner, block, overrides, .. } => {
634 let provider = get_provider(&config)?;
635 let token = token.resolve(&provider).await?;
636 let owner = owner.resolve(&provider).await?;
637
638 let token = IERC20::new(token, &provider);
639 let balance_call = token.balanceOf(owner).block(block.unwrap_or_default());
640 let call = balance_call.call();
641 let balance = overrides.apply(call)?.await?;
642
643 if shell::is_json() {
644 print_json_success(balance.to_string())?;
645 } else {
646 sh_println!("{balance}")?;
647 }
648 }
649 Self::Name { token, block, .. } => {
650 let provider = get_provider(&config)?;
651 let token = token.resolve(&provider).await?;
652
653 let name = IERC20::new(token, &provider)
654 .name()
655 .block(block.unwrap_or_default())
656 .call()
657 .await?;
658
659 print_scalar(name)?;
660 }
661 Self::Symbol { token, block, .. } => {
662 let provider = get_provider(&config)?;
663 let token = token.resolve(&provider).await?;
664
665 let symbol = IERC20::new(token, &provider)
666 .symbol()
667 .block(block.unwrap_or_default())
668 .call()
669 .await?;
670
671 print_scalar(symbol)?;
672 }
673 Self::Decimals { token, block, .. } => {
674 let provider = get_provider(&config)?;
675 let token = token.resolve(&provider).await?;
676
677 let decimals = IERC20::new(token, &provider)
678 .decimals()
679 .block(block.unwrap_or_default())
680 .call()
681 .await?;
682 print_scalar(decimals)?;
683 }
684 Self::TotalSupply { token, block, .. } => {
685 let provider = get_provider(&config)?;
686 let token = token.resolve(&provider).await?;
687
688 let total_supply = IERC20::new(token, &provider)
689 .totalSupply()
690 .block(block.unwrap_or_default())
691 .call()
692 .await?;
693
694 if shell::is_json() {
695 print_json_success(total_supply.to_string())?;
696 } else {
697 sh_println!("{}", format_uint_exp(total_supply))?
698 }
699 }
700 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
702 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
703 erc20.transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
704 })
705 }
706 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
707 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
708 erc20.approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
709 })
710 }
711 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
712 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
713 erc20.mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
714 })
715 }
716 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
717 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
718 erc20.burn(U256::from_str(&amount)?)
719 })
720 }
721 };
722 Ok(())
723 }
724}
725
726async fn fill_tx<N: Network, P: Provider<N>>(
731 provider: &P,
732 tx: &mut N::TransactionRequest,
733 from: Address,
734 chain: Chain,
735 browser: bool,
736 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
737) -> eyre::Result<()>
738where
739 N::TransactionRequest: FoundryTransactionBuilder<N>,
740{
741 tx.set_from(from);
742 tx.set_chain_id(chain.id());
743
744 if tx.nonce().is_none() {
745 tx.set_nonce(provider.get_transaction_count(from).await?);
746 }
747
748 let legacy = chain.is_legacy();
749
750 fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await?;
751
752 if tx.gas_limit().is_none() {
753 let mut estimated = provider.estimate_gas(tx.clone()).await?;
754
755 if chain.is_tempo() {
759 estimated += TEMPO_BROWSER_GAS_BUFFER;
760 }
761
762 tx.set_gas_limit(estimated);
763 }
764
765 Ok(())
766}