1use std::{str::FromStr, time::Duration};
2
3use crate::{
4 cmd::send::{cast_send, cast_send_with_access_key},
5 format_uint_exp, tempo,
6 tx::{CastTxSender, SendTxOpts, TxParams, fill_transaction_gas_fees},
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 json::{print_json_success, print_scalar},
19 opts::RpcOpts,
20 utils::{LoadConfig, get_chain, get_provider},
21};
22use foundry_common::{
23 FoundryTransactionBuilder,
24 fmt::{UIfmt, UIfmtReceiptExt},
25 provider::{ProviderBuilder, RetryProviderWithSigner},
26 shell,
27 tempo::{TEMPO_BROWSER_GAS_BUFFER, print_resolved_fee_token_selection},
28};
29#[doc(hidden)]
30pub use foundry_config::{Chain, utils::*};
31use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
32use tempo_alloy::TempoNetwork;
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 has_session: bool,
300 ) -> eyre::Result<bool> {
301 if self.erc20_opts().is_some_and(|erc20| erc20.tempo.is_tempo())
302 || has_session
303 || tempo_access_key.is_some()
304 {
305 return Ok(true);
306 }
307
308 if self.uses_browser_send() {
309 let config = self.rpc_opts().load_config()?;
310 return Ok(get_chain(config.chain, &get_provider(&config)?).await?.is_tempo());
311 }
312
313 Ok(false)
314 }
315
316 fn has_tempo_session(&self) -> eyre::Result<bool> {
317 self.erc20_opts().map_or(Ok(false), |opts| opts.tempo.session_id().map(|id| id.is_some()))
318 }
319
320 pub async fn run(self) -> eyre::Result<()> {
321 let has_session = self.has_tempo_session()?;
322 let (signer, tempo_access_key) = match &self {
324 Self::Transfer { send_tx, .. }
325 | Self::Approve { send_tx, .. }
326 | Self::Mint { send_tx, .. }
327 | Self::Burn { send_tx, .. } => {
328 if !has_session && send_tx.eth.wallet.from.is_some() {
332 let (s, ak) = send_tx.eth.wallet.maybe_signer().await?;
333 (s, ak)
334 } else {
335 (None, None)
336 }
337 }
338 _ => (None, None),
339 };
340
341 let is_tempo = self.should_use_tempo_network(&tempo_access_key, has_session).await?;
342
343 if is_tempo {
344 self.run_generic::<TempoNetwork>(signer, tempo_access_key, has_session).await
345 } else {
346 self.run_generic::<Ethereum>(signer, None, has_session).await
347 }
348 }
349
350 #[allow(clippy::large_stack_frames)]
351 pub async fn run_generic<N: Network + RecommendedFillers>(
352 self,
353 pre_resolved_signer: Option<WalletSigner>,
354 tempo_keychain: Option<TempoAccessKeyConfig>,
355 has_session: bool,
356 ) -> eyre::Result<()>
357 where
358 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
359 N::UnsignedTx: SignableTransaction<Signature>,
360 N::TransactionRequest: FoundryTransactionBuilder<N>,
361 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
362 {
363 let config = self.rpc_opts().load_config()?;
364
365 macro_rules! erc20_send {
368 (
369 $token:expr,
370 $send_tx:expr,
371 $tx_opts:expr, |
372 $erc20:ident,
373 $provider:ident |
374 $build_tx:expr
375 ) => {{
376 let mut tx_opts = $tx_opts;
377 tempo::ensure_session_not_browser(&tx_opts.tempo, $send_tx.browser.browser)?;
378 let (pre_resolved_signer, tempo_keychain) = if has_session {
379 let $provider =
380 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
381 let chain = get_chain(config.chain, &$provider).await?;
382 tempo::resolve_session_or_wallet_signer(
383 &tx_opts.tempo,
384 &$send_tx.eth.wallet,
385 chain.id(),
386 )
387 .await?
388 } else {
389 (pre_resolved_signer, tempo_keychain)
390 };
391 let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash;
392 let expires_at = tx_opts.tempo.resolve_expires();
393 let tempo_sponsor =
394 if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? };
395 let needs_sponsor_payload = print_sponsor_hash || tempo_sponsor.is_some();
396 if let Some(ts) = expires_at {
397 sh_status!("Transaction expires at unix timestamp {ts}")?;
398 }
399
400 let timeout = $send_tx.timeout.unwrap_or(config.transaction_timeout);
401 if let Some(ref access_key) = tempo_keychain {
402 let signer = pre_resolved_signer
403 .as_ref()
404 .ok_or_else(|| eyre::eyre!("signer required for access key"))?;
405 let $provider =
406 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
407 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
408 let mut tx = { $build_tx }.into_transaction_request();
409 let chain = get_chain(config.chain, &$provider).await?;
410 tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
411 tempo::fill_access_key_transaction(&$provider, &mut tx, access_key, chain)
412 .await?;
413 if needs_sponsor_payload {
414 if print_sponsor_hash {
415 let hash = tx
416 .compute_sponsor_hash(access_key.wallet_address)
417 .ok_or_else(|| {
418 eyre::eyre!(
419 "This network does not support sponsored transactions"
420 )
421 })?;
422 sh_println!("{hash:?}")?;
423 return Ok(());
424 }
425 if let Some(sponsor) = &tempo_sponsor {
426 sponsor
427 .attach_and_print::<TempoNetwork>(
428 &mut tx,
429 access_key.wallet_address,
430 )
431 .await?;
432 }
433 }
434 cast_send_with_access_key(
435 &$provider,
436 tx,
437 signer,
438 access_key,
439 Some(chain),
440 $send_tx.cast_async,
441 $send_tx.confirmations,
442 timeout,
443 )
444 .await?;
445 } else if let Some(browser) = $send_tx.browser.run::<N>().await? {
446 let $provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
447 if let Some(interval) = $send_tx.poll_interval {
448 $provider.client().set_poll_interval(Duration::from_secs(interval));
449 }
450 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
451 let mut tx = { $build_tx }.into_transaction_request();
452 let chain = get_chain(config.chain, &$provider).await?;
453 tx_opts.apply::<N>(&mut tx, chain.is_legacy());
454 fill_tx(&$provider, &mut tx, browser.address(), chain, true).await?;
455 if print_sponsor_hash {
456 let hash = tx.compute_sponsor_hash(browser.address()).ok_or_else(|| {
457 eyre::eyre!("This network does not support sponsored transactions")
458 })?;
459 sh_println!("{hash:?}")?;
460 return Ok(());
461 }
462 if let Some(sponsor) = &tempo_sponsor {
463 sponsor.attach_and_print::<N>(&mut tx, browser.address()).await?;
464 }
465 print_resolved_fee_token_selection(Some(chain), tx.fee_token())?;
466 let tx_hash = browser.send_transaction_via_browser(tx).await?;
467 CastTxSender::new(&$provider)
468 .print_tx_result(
469 tx_hash,
470 $send_tx.cast_async,
471 $send_tx.confirmations,
472 timeout,
473 )
474 .await?
475 } else {
476 let signer = pre_resolved_signer.unwrap_or($send_tx.eth.wallet.signer().await?);
477 let from = signer.address();
478 let $provider = build_provider_with_signer::<N>(&$send_tx, signer)?;
479 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
480 let mut tx = { $build_tx }.into_transaction_request();
481 let chain = get_chain(config.chain, &$provider).await?;
482 tx_opts.apply::<N>(&mut tx, chain.is_legacy());
483 if needs_sponsor_payload {
484 fill_tx(&$provider, &mut tx, from, chain, false).await?;
485 if print_sponsor_hash {
486 let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
487 eyre::eyre!("This network does not support sponsored transactions")
488 })?;
489 sh_println!("{hash:?}")?;
490 return Ok(());
491 }
492 if let Some(sponsor) = &tempo_sponsor {
493 sponsor.attach_and_print::<N>(&mut tx, from).await?;
494 }
495 }
496 cast_send(
497 $provider,
498 tx,
499 Some(chain),
500 $send_tx.cast_async,
501 $send_tx.sync,
502 $send_tx.confirmations,
503 timeout,
504 )
505 .await?;
506 }
507 }};
508 }
509
510 match self {
511 Self::Allowance { token, owner, spender, block, .. } => {
513 let provider = get_provider(&config)?;
514 let token = token.resolve(&provider).await?;
515 let owner = owner.resolve(&provider).await?;
516 let spender = spender.resolve(&provider).await?;
517
518 let allowance = IERC20::new(token, &provider)
519 .allowance(owner, spender)
520 .block(block.unwrap_or_default())
521 .call()
522 .await?;
523
524 if shell::is_json() {
525 print_json_success(allowance.to_string())?;
526 } else {
527 sh_println!("{}", format_uint_exp(allowance))?;
528 }
529 }
530 Self::Balance { token, owner, block, .. } => {
531 let provider = get_provider(&config)?;
532 let token = token.resolve(&provider).await?;
533 let owner = owner.resolve(&provider).await?;
534
535 let balance = IERC20::new(token, &provider)
536 .balanceOf(owner)
537 .block(block.unwrap_or_default())
538 .call()
539 .await?;
540
541 if shell::is_json() {
542 print_json_success(balance.to_string())?;
543 } else {
544 sh_println!("{balance}")?;
545 }
546 }
547 Self::Name { token, block, .. } => {
548 let provider = get_provider(&config)?;
549 let token = token.resolve(&provider).await?;
550
551 let name = IERC20::new(token, &provider)
552 .name()
553 .block(block.unwrap_or_default())
554 .call()
555 .await?;
556
557 print_scalar(name)?;
558 }
559 Self::Symbol { token, block, .. } => {
560 let provider = get_provider(&config)?;
561 let token = token.resolve(&provider).await?;
562
563 let symbol = IERC20::new(token, &provider)
564 .symbol()
565 .block(block.unwrap_or_default())
566 .call()
567 .await?;
568
569 print_scalar(symbol)?;
570 }
571 Self::Decimals { token, block, .. } => {
572 let provider = get_provider(&config)?;
573 let token = token.resolve(&provider).await?;
574
575 let decimals = IERC20::new(token, &provider)
576 .decimals()
577 .block(block.unwrap_or_default())
578 .call()
579 .await?;
580 print_scalar(decimals)?;
581 }
582 Self::TotalSupply { token, block, .. } => {
583 let provider = get_provider(&config)?;
584 let token = token.resolve(&provider).await?;
585
586 let total_supply = IERC20::new(token, &provider)
587 .totalSupply()
588 .block(block.unwrap_or_default())
589 .call()
590 .await?;
591
592 if shell::is_json() {
593 print_json_success(total_supply.to_string())?;
594 } else {
595 sh_println!("{}", format_uint_exp(total_supply))?
596 }
597 }
598 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
600 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
601 erc20.transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
602 })
603 }
604 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
605 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
606 erc20.approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
607 })
608 }
609 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
610 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
611 erc20.mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
612 })
613 }
614 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
615 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
616 erc20.burn(U256::from_str(&amount)?)
617 })
618 }
619 };
620 Ok(())
621 }
622}
623
624async fn fill_tx<N: Network, P: Provider<N>>(
629 provider: &P,
630 tx: &mut N::TransactionRequest,
631 from: Address,
632 chain: Chain,
633 browser: bool,
634) -> eyre::Result<()>
635where
636 N::TransactionRequest: FoundryTransactionBuilder<N>,
637{
638 tx.set_from(from);
639 tx.set_chain_id(chain.id());
640
641 if tx.nonce().is_none() {
642 tx.set_nonce(provider.get_transaction_count(from).await?);
643 }
644
645 let legacy = chain.is_legacy();
646
647 fill_transaction_gas_fees(provider, tx, legacy, browser).await?;
648
649 if tx.gas_limit().is_none() {
650 let mut estimated = provider.estimate_gas(tx.clone()).await?;
651
652 if chain.is_tempo() {
656 estimated += TEMPO_BROWSER_GAS_BUFFER;
657 }
658
659 tx.set_gas_limit(estimated);
660 }
661
662 Ok(())
663}