1use std::{str::FromStr, time::Duration};
2
3use crate::{cmd::send::cast_send, format_uint_exp, tx::SendTxOpts};
4use alloy_consensus::{SignableTransaction, Signed};
5use alloy_eips::BlockId;
6use alloy_ens::NameOrAddress;
7use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
8use alloy_primitives::{U64, U256};
9use alloy_provider::{Provider, fillers::RecommendedFillers};
10use alloy_signer::Signature;
11use alloy_sol_types::sol;
12use clap::{Args, Parser};
13use foundry_cli::{
14 opts::{RpcOpts, TempoOpts},
15 utils::{LoadConfig, get_chain, get_provider},
16};
17use foundry_common::{
18 FoundryTransactionBuilder,
19 fmt::{UIfmt, UIfmtReceiptExt},
20 provider::{ProviderBuilder, RetryProviderWithSigner},
21 shell,
22};
23#[doc(hidden)]
24pub use foundry_config::{Chain, utils::*};
25use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
26use tempo_alloy::TempoNetwork;
27
28sol! {
29 #[sol(rpc)]
30 interface IERC20 {
31 #[derive(Debug)]
32 function name() external view returns (string);
33 function symbol() external view returns (string);
34 function decimals() external view returns (uint8);
35 function totalSupply() external view returns (uint256);
36 function balanceOf(address owner) external view returns (uint256);
37 function transfer(address to, uint256 amount) external returns (bool);
38 function approve(address spender, uint256 amount) external returns (bool);
39 function allowance(address owner, address spender) external view returns (uint256);
40 function mint(address to, uint256 amount) external;
41 function burn(uint256 amount) external;
42 }
43}
44
45#[derive(Debug, Clone, Args)]
49#[command(next_help_heading = "Transaction options")]
50pub struct Erc20TxOpts {
51 #[arg(long, env = "ETH_GAS_LIMIT")]
53 pub gas_limit: Option<U256>,
54
55 #[arg(long, env = "ETH_GAS_PRICE")]
57 pub gas_price: Option<U256>,
58
59 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
61 pub priority_gas_price: Option<U256>,
62
63 #[arg(long)]
65 pub nonce: Option<U64>,
66
67 #[command(flatten)]
68 pub tempo: TempoOpts,
69}
70
71pub(crate) fn build_provider_with_signer<N: Network + RecommendedFillers>(
73 tx_opts: &SendTxOpts,
74 signer: WalletSigner,
75) -> eyre::Result<RetryProviderWithSigner<N>>
76where
77 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
78 N::UnsignedTx: SignableTransaction<Signature>,
79{
80 let config = tx_opts.eth.load_config()?;
81 let wallet = EthereumWallet::from(signer);
82 let provider = ProviderBuilder::<N>::from_config(&config)?.build_with_wallet(wallet)?;
83 if let Some(interval) = tx_opts.poll_interval {
84 provider.client().set_poll_interval(Duration::from_secs(interval))
85 }
86 Ok(provider)
87}
88
89impl Erc20TxOpts {
90 fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
92 where
93 N::TransactionRequest: FoundryTransactionBuilder<N>,
94 {
95 if let Some(gas_limit) = self.gas_limit {
96 tx.set_gas_limit(gas_limit.to());
97 }
98
99 if let Some(gas_price) = self.gas_price {
100 if legacy {
101 tx.set_gas_price(gas_price.to());
102 } else {
103 tx.set_max_fee_per_gas(gas_price.to());
104 }
105 }
106
107 if !legacy && let Some(priority_fee) = self.priority_gas_price {
108 tx.set_max_priority_fee_per_gas(priority_fee.to());
109 }
110
111 self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
112 }
113}
114
115#[derive(Debug, Parser, Clone)]
117pub enum Erc20Subcommand {
118 #[command(visible_alias = "b")]
120 Balance {
121 #[arg(value_parser = NameOrAddress::from_str)]
123 token: NameOrAddress,
124
125 #[arg(value_parser = NameOrAddress::from_str)]
127 owner: NameOrAddress,
128
129 #[arg(long, short = 'B')]
131 block: Option<BlockId>,
132
133 #[command(flatten)]
134 rpc: RpcOpts,
135 },
136
137 #[command(visible_aliases = ["t", "send"])]
139 Transfer {
140 #[arg(value_parser = NameOrAddress::from_str)]
142 token: NameOrAddress,
143
144 #[arg(value_parser = NameOrAddress::from_str)]
146 to: NameOrAddress,
147
148 amount: String,
150
151 #[command(flatten)]
152 send_tx: SendTxOpts,
153
154 #[command(flatten)]
155 tx: Erc20TxOpts,
156 },
157
158 #[command(visible_alias = "a")]
160 Approve {
161 #[arg(value_parser = NameOrAddress::from_str)]
163 token: NameOrAddress,
164
165 #[arg(value_parser = NameOrAddress::from_str)]
167 spender: NameOrAddress,
168
169 amount: String,
171
172 #[command(flatten)]
173 send_tx: SendTxOpts,
174
175 #[command(flatten)]
176 tx: Erc20TxOpts,
177 },
178
179 #[command(visible_alias = "al")]
181 Allowance {
182 #[arg(value_parser = NameOrAddress::from_str)]
184 token: NameOrAddress,
185
186 #[arg(value_parser = NameOrAddress::from_str)]
188 owner: NameOrAddress,
189
190 #[arg(value_parser = NameOrAddress::from_str)]
192 spender: NameOrAddress,
193
194 #[arg(long, short = 'B')]
196 block: Option<BlockId>,
197
198 #[command(flatten)]
199 rpc: RpcOpts,
200 },
201
202 #[command(visible_alias = "n")]
204 Name {
205 #[arg(value_parser = NameOrAddress::from_str)]
207 token: NameOrAddress,
208
209 #[arg(long, short = 'B')]
211 block: Option<BlockId>,
212
213 #[command(flatten)]
214 rpc: RpcOpts,
215 },
216
217 #[command(visible_alias = "s")]
219 Symbol {
220 #[arg(value_parser = NameOrAddress::from_str)]
222 token: NameOrAddress,
223
224 #[arg(long, short = 'B')]
226 block: Option<BlockId>,
227
228 #[command(flatten)]
229 rpc: RpcOpts,
230 },
231
232 #[command(visible_alias = "d")]
234 Decimals {
235 #[arg(value_parser = NameOrAddress::from_str)]
237 token: NameOrAddress,
238
239 #[arg(long, short = 'B')]
241 block: Option<BlockId>,
242
243 #[command(flatten)]
244 rpc: RpcOpts,
245 },
246
247 #[command(visible_alias = "ts")]
249 TotalSupply {
250 #[arg(value_parser = NameOrAddress::from_str)]
252 token: NameOrAddress,
253
254 #[arg(long, short = 'B')]
256 block: Option<BlockId>,
257
258 #[command(flatten)]
259 rpc: RpcOpts,
260 },
261
262 #[command(visible_alias = "m")]
264 Mint {
265 #[arg(value_parser = NameOrAddress::from_str)]
267 token: NameOrAddress,
268
269 #[arg(value_parser = NameOrAddress::from_str)]
271 to: NameOrAddress,
272
273 amount: String,
275
276 #[command(flatten)]
277 send_tx: SendTxOpts,
278
279 #[command(flatten)]
280 tx: Erc20TxOpts,
281 },
282
283 #[command(visible_alias = "bu")]
285 Burn {
286 #[arg(value_parser = NameOrAddress::from_str)]
288 token: NameOrAddress,
289
290 amount: String,
292
293 #[command(flatten)]
294 send_tx: SendTxOpts,
295
296 #[command(flatten)]
297 tx: Erc20TxOpts,
298 },
299}
300
301impl Erc20Subcommand {
302 const fn rpc_opts(&self) -> &RpcOpts {
303 match self {
304 Self::Allowance { rpc, .. } => rpc,
305 Self::Approve { send_tx, .. } => &send_tx.eth.rpc,
306 Self::Balance { rpc, .. } => rpc,
307 Self::Transfer { send_tx, .. } => &send_tx.eth.rpc,
308 Self::Name { rpc, .. } => rpc,
309 Self::Symbol { rpc, .. } => rpc,
310 Self::Decimals { rpc, .. } => rpc,
311 Self::TotalSupply { rpc, .. } => rpc,
312 Self::Mint { send_tx, .. } => &send_tx.eth.rpc,
313 Self::Burn { send_tx, .. } => &send_tx.eth.rpc,
314 }
315 }
316
317 const fn erc20_opts(&self) -> Option<&Erc20TxOpts> {
318 match self {
319 Self::Approve { tx, .. }
320 | Self::Transfer { tx, .. }
321 | Self::Mint { tx, .. }
322 | Self::Burn { tx, .. } => Some(tx),
323 Self::Allowance { .. }
324 | Self::Balance { .. }
325 | Self::Name { .. }
326 | Self::Symbol { .. }
327 | Self::Decimals { .. }
328 | Self::TotalSupply { .. } => None,
329 }
330 }
331
332 pub async fn run(self) -> eyre::Result<()> {
333 let (signer, tempo_access_key) = match &self {
335 Self::Transfer { send_tx, .. }
336 | Self::Approve { send_tx, .. }
337 | Self::Mint { send_tx, .. }
338 | Self::Burn { send_tx, .. } => {
339 if send_tx.eth.wallet.from.is_some() {
341 let (s, ak) = send_tx.eth.wallet.maybe_signer().await?;
342 (s, ak)
343 } else {
344 (None, None)
345 }
346 }
347 _ => (None, None),
348 };
349
350 let is_tempo = self.erc20_opts().is_some_and(|erc20| erc20.tempo.is_tempo())
351 || tempo_access_key.is_some();
352
353 if is_tempo {
354 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
355 } else {
356 self.run_generic::<Ethereum>(signer, None).await
357 }
358 }
359
360 pub async fn run_generic<N: Network + RecommendedFillers>(
361 self,
362 pre_resolved_signer: Option<WalletSigner>,
363 tempo_keychain: Option<TempoAccessKeyConfig>,
364 ) -> eyre::Result<()>
365 where
366 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
367 N::UnsignedTx: SignableTransaction<Signature>,
368 N::TransactionRequest: FoundryTransactionBuilder<N>,
369 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
370 {
371 let config = self.rpc_opts().load_config()?;
372
373 macro_rules! erc20_send {
376 (
377 $token:expr,
378 $send_tx:expr,
379 $tx_opts:expr, |
380 $erc20:ident,
381 $provider:ident |
382 $build_tx:expr
383 ) => {{
384 let timeout = $send_tx.timeout.unwrap_or(config.transaction_timeout);
385 if let Some(ref access_key) = tempo_keychain {
386 let signer =
387 pre_resolved_signer.as_ref().expect("signer required for access key");
388 let $provider =
389 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
390 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
391 let mut tx = { $build_tx }.into_transaction_request();
392 $tx_opts.apply::<TempoNetwork>(
393 &mut tx,
394 get_chain(config.chain, &$provider).await?.is_legacy(),
395 );
396 apply_tempo_access_key::<TempoNetwork>(&mut tx, Some(access_key));
397 send_tempo_keychain(
398 &$provider,
399 tx,
400 signer,
401 access_key,
402 $send_tx.cast_async,
403 $send_tx.confirmations,
404 timeout,
405 )
406 .await?
407 } else {
408 let signer = pre_resolved_signer.unwrap_or($send_tx.eth.wallet.signer().await?);
409 let $provider = build_provider_with_signer::<N>(&$send_tx, signer)?;
410 let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider);
411 let mut tx = { $build_tx }.into_transaction_request();
412 $tx_opts.apply::<N>(
413 &mut tx,
414 get_chain(config.chain, &$provider).await?.is_legacy(),
415 );
416 cast_send(
417 $provider,
418 tx,
419 $send_tx.cast_async,
420 $send_tx.sync,
421 $send_tx.confirmations,
422 timeout,
423 )
424 .await?
425 }
426 }};
427 }
428
429 match self {
430 Self::Allowance { token, owner, spender, block, .. } => {
432 let provider = get_provider(&config)?;
433 let token = token.resolve(&provider).await?;
434 let owner = owner.resolve(&provider).await?;
435 let spender = spender.resolve(&provider).await?;
436
437 let allowance = IERC20::new(token, &provider)
438 .allowance(owner, spender)
439 .block(block.unwrap_or_default())
440 .call()
441 .await?;
442
443 if shell::is_json() {
444 sh_println!("{}", serde_json::to_string(&allowance.to_string())?)?
445 } else {
446 sh_println!("{}", format_uint_exp(allowance))?
447 }
448 }
449 Self::Balance { token, owner, block, .. } => {
450 let provider = get_provider(&config)?;
451 let token = token.resolve(&provider).await?;
452 let owner = owner.resolve(&provider).await?;
453
454 let balance = IERC20::new(token, &provider)
455 .balanceOf(owner)
456 .block(block.unwrap_or_default())
457 .call()
458 .await?;
459
460 if shell::is_json() {
461 sh_println!("{}", serde_json::to_string(&balance.to_string())?)?
462 } else {
463 sh_println!("{}", format_uint_exp(balance))?
464 }
465 }
466 Self::Name { token, block, .. } => {
467 let provider = get_provider(&config)?;
468 let token = token.resolve(&provider).await?;
469
470 let name = IERC20::new(token, &provider)
471 .name()
472 .block(block.unwrap_or_default())
473 .call()
474 .await?;
475
476 if shell::is_json() {
477 sh_println!("{}", serde_json::to_string(&name)?)?
478 } else {
479 sh_println!("{}", name)?
480 }
481 }
482 Self::Symbol { token, block, .. } => {
483 let provider = get_provider(&config)?;
484 let token = token.resolve(&provider).await?;
485
486 let symbol = IERC20::new(token, &provider)
487 .symbol()
488 .block(block.unwrap_or_default())
489 .call()
490 .await?;
491
492 if shell::is_json() {
493 sh_println!("{}", serde_json::to_string(&symbol)?)?
494 } else {
495 sh_println!("{}", symbol)?
496 }
497 }
498 Self::Decimals { token, block, .. } => {
499 let provider = get_provider(&config)?;
500 let token = token.resolve(&provider).await?;
501
502 let decimals = IERC20::new(token, &provider)
503 .decimals()
504 .block(block.unwrap_or_default())
505 .call()
506 .await?;
507 if shell::is_json() {
508 sh_println!("{}", serde_json::to_string(&decimals)?)?
509 } else {
510 sh_println!("{}", decimals)?
511 }
512 }
513 Self::TotalSupply { token, block, .. } => {
514 let provider = get_provider(&config)?;
515 let token = token.resolve(&provider).await?;
516
517 let total_supply = IERC20::new(token, &provider)
518 .totalSupply()
519 .block(block.unwrap_or_default())
520 .call()
521 .await?;
522
523 if shell::is_json() {
524 sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)?
525 } else {
526 sh_println!("{}", format_uint_exp(total_supply))?
527 }
528 }
529 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
531 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
532 erc20.transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
533 })
534 }
535 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
536 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
537 erc20.approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
538 })
539 }
540 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
541 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
542 erc20.mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
543 })
544 }
545 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
546 erc20_send!(token, send_tx, tx_opts, |erc20, provider| {
547 erc20.burn(U256::from_str(&amount)?)
548 })
549 }
550 };
551 Ok(())
552 }
553}
554
555fn apply_tempo_access_key<N: Network>(
560 tx: &mut N::TransactionRequest,
561 config: Option<&TempoAccessKeyConfig>,
562) where
563 N::TransactionRequest: FoundryTransactionBuilder<N>,
564{
565 if let Some(config) = config {
566 tx.set_from(config.wallet_address);
567 tx.set_key_id(config.key_address);
568 }
569}
570
571async fn send_tempo_keychain<P: Provider<TempoNetwork>>(
577 provider: &P,
578 tx: <TempoNetwork as Network>::TransactionRequest,
579 signer: &WalletSigner,
580 access_key: &TempoAccessKeyConfig,
581 cast_async: bool,
582 confirmations: u64,
583 timeout: u64,
584) -> eyre::Result<()> {
585 let raw_tx = tx
586 .sign_with_access_key(
587 provider,
588 signer,
589 access_key.wallet_address,
590 access_key.key_address,
591 access_key.key_authorization.as_ref(),
592 )
593 .await?;
594
595 let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
596
597 let cast = crate::tx::CastTxSender::new(provider);
598 cast.print_tx_result(tx_hash, cast_async, confirmations, timeout).await
599}