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::{AnyNetwork, 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 fmt::{UIfmt, UIfmtReceiptExt},
19 provider::{ProviderBuilder, RetryProviderWithSigner},
20 shell,
21};
22#[doc(hidden)]
23pub use foundry_config::{Chain, utils::*};
24use foundry_primitives::FoundryTransactionBuilder;
25use tempo_alloy::TempoNetwork;
26
27sol! {
28 #[sol(rpc)]
29 interface IERC20 {
30 #[derive(Debug)]
31 function name() external view returns (string);
32 function symbol() external view returns (string);
33 function decimals() external view returns (uint8);
34 function totalSupply() external view returns (uint256);
35 function balanceOf(address owner) external view returns (uint256);
36 function transfer(address to, uint256 amount) external returns (bool);
37 function approve(address spender, uint256 amount) external returns (bool);
38 function allowance(address owner, address spender) external view returns (uint256);
39 function mint(address to, uint256 amount) external;
40 function burn(uint256 amount) external;
41 }
42}
43
44#[derive(Debug, Clone, Args)]
48pub struct Erc20TxOpts {
49 #[arg(long, env = "ETH_GAS_LIMIT")]
51 pub gas_limit: Option<U256>,
52
53 #[arg(long, env = "ETH_GAS_PRICE")]
55 pub gas_price: Option<U256>,
56
57 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
59 pub priority_gas_price: Option<U256>,
60
61 #[arg(long)]
63 pub nonce: Option<U64>,
64
65 #[command(flatten)]
66 pub tempo: TempoOpts,
67}
68
69pub(crate) async fn get_provider_with_wallet<N: Network + RecommendedFillers>(
71 tx_opts: &SendTxOpts,
72) -> eyre::Result<RetryProviderWithSigner<N>>
73where
74 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
75 N::UnsignedTx: SignableTransaction<Signature>,
76{
77 let config = tx_opts.eth.load_config()?;
78 let signer = tx_opts.eth.wallet.signer().await?;
79 let wallet = EthereumWallet::from(signer);
80 let provider = ProviderBuilder::<N>::from_config(&config)?.build_with_wallet(wallet)?;
81 if let Some(interval) = tx_opts.poll_interval {
82 provider.client().set_poll_interval(Duration::from_secs(interval))
83 }
84 Ok(provider)
85}
86
87impl Erc20TxOpts {
88 fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
90 where
91 N::TransactionRequest: FoundryTransactionBuilder<N>,
92 {
93 if let Some(gas_limit) = self.gas_limit {
94 tx.set_gas_limit(gas_limit.to());
95 }
96
97 if let Some(gas_price) = self.gas_price {
98 if legacy {
99 tx.set_gas_price(gas_price.to());
100 } else {
101 tx.set_max_fee_per_gas(gas_price.to());
102 }
103 }
104
105 if !legacy && let Some(priority_fee) = self.priority_gas_price {
106 tx.set_max_priority_fee_per_gas(priority_fee.to());
107 }
108
109 self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
110 }
111}
112
113#[derive(Debug, Parser, Clone)]
115pub enum Erc20Subcommand {
116 #[command(visible_alias = "b")]
118 Balance {
119 #[arg(value_parser = NameOrAddress::from_str)]
121 token: NameOrAddress,
122
123 #[arg(value_parser = NameOrAddress::from_str)]
125 owner: NameOrAddress,
126
127 #[arg(long, short = 'B')]
129 block: Option<BlockId>,
130
131 #[command(flatten)]
132 rpc: RpcOpts,
133 },
134
135 #[command(visible_aliases = ["t", "send"])]
137 Transfer {
138 #[arg(value_parser = NameOrAddress::from_str)]
140 token: NameOrAddress,
141
142 #[arg(value_parser = NameOrAddress::from_str)]
144 to: NameOrAddress,
145
146 amount: String,
148
149 #[command(flatten)]
150 send_tx: SendTxOpts,
151
152 #[command(flatten)]
153 tx: Erc20TxOpts,
154 },
155
156 #[command(visible_alias = "a")]
158 Approve {
159 #[arg(value_parser = NameOrAddress::from_str)]
161 token: NameOrAddress,
162
163 #[arg(value_parser = NameOrAddress::from_str)]
165 spender: NameOrAddress,
166
167 amount: String,
169
170 #[command(flatten)]
171 send_tx: SendTxOpts,
172
173 #[command(flatten)]
174 tx: Erc20TxOpts,
175 },
176
177 #[command(visible_alias = "al")]
179 Allowance {
180 #[arg(value_parser = NameOrAddress::from_str)]
182 token: NameOrAddress,
183
184 #[arg(value_parser = NameOrAddress::from_str)]
186 owner: NameOrAddress,
187
188 #[arg(value_parser = NameOrAddress::from_str)]
190 spender: NameOrAddress,
191
192 #[arg(long, short = 'B')]
194 block: Option<BlockId>,
195
196 #[command(flatten)]
197 rpc: RpcOpts,
198 },
199
200 #[command(visible_alias = "n")]
202 Name {
203 #[arg(value_parser = NameOrAddress::from_str)]
205 token: NameOrAddress,
206
207 #[arg(long, short = 'B')]
209 block: Option<BlockId>,
210
211 #[command(flatten)]
212 rpc: RpcOpts,
213 },
214
215 #[command(visible_alias = "s")]
217 Symbol {
218 #[arg(value_parser = NameOrAddress::from_str)]
220 token: NameOrAddress,
221
222 #[arg(long, short = 'B')]
224 block: Option<BlockId>,
225
226 #[command(flatten)]
227 rpc: RpcOpts,
228 },
229
230 #[command(visible_alias = "d")]
232 Decimals {
233 #[arg(value_parser = NameOrAddress::from_str)]
235 token: NameOrAddress,
236
237 #[arg(long, short = 'B')]
239 block: Option<BlockId>,
240
241 #[command(flatten)]
242 rpc: RpcOpts,
243 },
244
245 #[command(visible_alias = "ts")]
247 TotalSupply {
248 #[arg(value_parser = NameOrAddress::from_str)]
250 token: NameOrAddress,
251
252 #[arg(long, short = 'B')]
254 block: Option<BlockId>,
255
256 #[command(flatten)]
257 rpc: RpcOpts,
258 },
259
260 #[command(visible_alias = "m")]
262 Mint {
263 #[arg(value_parser = NameOrAddress::from_str)]
265 token: NameOrAddress,
266
267 #[arg(value_parser = NameOrAddress::from_str)]
269 to: NameOrAddress,
270
271 amount: String,
273
274 #[command(flatten)]
275 send_tx: SendTxOpts,
276
277 #[command(flatten)]
278 tx: Erc20TxOpts,
279 },
280
281 #[command(visible_alias = "bu")]
283 Burn {
284 #[arg(value_parser = NameOrAddress::from_str)]
286 token: NameOrAddress,
287
288 amount: String,
290
291 #[command(flatten)]
292 send_tx: SendTxOpts,
293
294 #[command(flatten)]
295 tx: Erc20TxOpts,
296 },
297}
298
299impl Erc20Subcommand {
300 fn rpc_opts(&self) -> &RpcOpts {
301 match self {
302 Self::Allowance { rpc, .. } => rpc,
303 Self::Approve { send_tx, .. } => &send_tx.eth.rpc,
304 Self::Balance { rpc, .. } => rpc,
305 Self::Transfer { send_tx, .. } => &send_tx.eth.rpc,
306 Self::Name { rpc, .. } => rpc,
307 Self::Symbol { rpc, .. } => rpc,
308 Self::Decimals { rpc, .. } => rpc,
309 Self::TotalSupply { rpc, .. } => rpc,
310 Self::Mint { send_tx, .. } => &send_tx.eth.rpc,
311 Self::Burn { send_tx, .. } => &send_tx.eth.rpc,
312 }
313 }
314
315 fn erc20_opts(&self) -> Option<&Erc20TxOpts> {
316 match self {
317 Self::Approve { tx, .. }
318 | Self::Transfer { tx, .. }
319 | Self::Mint { tx, .. }
320 | Self::Burn { tx, .. } => Some(tx),
321 Self::Allowance { .. }
322 | Self::Balance { .. }
323 | Self::Name { .. }
324 | Self::Symbol { .. }
325 | Self::Decimals { .. }
326 | Self::TotalSupply { .. } => None,
327 }
328 }
329
330 pub async fn run(self) -> eyre::Result<()> {
331 if let Some(erc20) = self.erc20_opts()
332 && erc20.tempo.is_tempo()
333 {
334 self.run_generic::<TempoNetwork>().await
335 } else {
336 self.run_generic::<AnyNetwork>().await
337 }
338 }
339
340 pub async fn run_generic<N: Network + RecommendedFillers>(self) -> eyre::Result<()>
341 where
342 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
343 N::UnsignedTx: SignableTransaction<Signature>,
344 N::TransactionRequest: FoundryTransactionBuilder<N>,
345 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
346 {
347 let config = self.rpc_opts().load_config()?;
348
349 match self {
350 Self::Allowance { token, owner, spender, block, .. } => {
352 let provider = get_provider(&config)?;
353 let token = token.resolve(&provider).await?;
354 let owner = owner.resolve(&provider).await?;
355 let spender = spender.resolve(&provider).await?;
356
357 let allowance = IERC20::new(token, &provider)
358 .allowance(owner, spender)
359 .block(block.unwrap_or_default())
360 .call()
361 .await?;
362
363 if shell::is_json() {
364 sh_println!("{}", serde_json::to_string(&allowance.to_string())?)?
365 } else {
366 sh_println!("{}", format_uint_exp(allowance))?
367 }
368 }
369 Self::Balance { token, owner, block, .. } => {
370 let provider = get_provider(&config)?;
371 let token = token.resolve(&provider).await?;
372 let owner = owner.resolve(&provider).await?;
373
374 let balance = IERC20::new(token, &provider)
375 .balanceOf(owner)
376 .block(block.unwrap_or_default())
377 .call()
378 .await?;
379
380 if shell::is_json() {
381 sh_println!("{}", serde_json::to_string(&balance.to_string())?)?
382 } else {
383 sh_println!("{}", format_uint_exp(balance))?
384 }
385 }
386 Self::Name { token, block, .. } => {
387 let provider = get_provider(&config)?;
388 let token = token.resolve(&provider).await?;
389
390 let name = IERC20::new(token, &provider)
391 .name()
392 .block(block.unwrap_or_default())
393 .call()
394 .await?;
395
396 if shell::is_json() {
397 sh_println!("{}", serde_json::to_string(&name)?)?
398 } else {
399 sh_println!("{}", name)?
400 }
401 }
402 Self::Symbol { token, block, .. } => {
403 let provider = get_provider(&config)?;
404 let token = token.resolve(&provider).await?;
405
406 let symbol = IERC20::new(token, &provider)
407 .symbol()
408 .block(block.unwrap_or_default())
409 .call()
410 .await?;
411
412 if shell::is_json() {
413 sh_println!("{}", serde_json::to_string(&symbol)?)?
414 } else {
415 sh_println!("{}", symbol)?
416 }
417 }
418 Self::Decimals { token, block, .. } => {
419 let provider = get_provider(&config)?;
420 let token = token.resolve(&provider).await?;
421
422 let decimals = IERC20::new(token, &provider)
423 .decimals()
424 .block(block.unwrap_or_default())
425 .call()
426 .await?;
427 if shell::is_json() {
428 sh_println!("{}", serde_json::to_string(&decimals)?)?
429 } else {
430 sh_println!("{}", decimals)?
431 }
432 }
433 Self::TotalSupply { token, block, .. } => {
434 let provider = get_provider(&config)?;
435 let token = token.resolve(&provider).await?;
436
437 let total_supply = IERC20::new(token, &provider)
438 .totalSupply()
439 .block(block.unwrap_or_default())
440 .call()
441 .await?;
442
443 if shell::is_json() {
444 sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)?
445 } else {
446 sh_println!("{}", format_uint_exp(total_supply))?
447 }
448 }
449 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
451 let provider = get_provider_with_wallet::<N>(&send_tx).await?;
452 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
453 .transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
454 .into_transaction_request();
455
456 tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
457
458 cast_send(
459 provider,
460 tx,
461 send_tx.cast_async,
462 send_tx.sync,
463 send_tx.confirmations,
464 send_tx.timeout.unwrap_or(config.transaction_timeout),
465 )
466 .await?
467 }
468 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
469 let provider = get_provider_with_wallet::<N>(&send_tx).await?;
470 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
471 .approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
472 .into_transaction_request();
473
474 tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
475
476 cast_send(
477 provider,
478 tx,
479 send_tx.cast_async,
480 send_tx.sync,
481 send_tx.confirmations,
482 send_tx.timeout.unwrap_or(config.transaction_timeout),
483 )
484 .await?
485 }
486 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
487 let provider = get_provider_with_wallet::<N>(&send_tx).await?;
488 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
489 .mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
490 .into_transaction_request();
491
492 tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
493
494 cast_send(
495 provider,
496 tx,
497 send_tx.cast_async,
498 send_tx.sync,
499 send_tx.confirmations,
500 send_tx.timeout.unwrap_or(config.transaction_timeout),
501 )
502 .await?
503 }
504 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
505 let provider = get_provider_with_wallet::<N>(&send_tx).await?;
506 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
507 .burn(U256::from_str(&amount)?)
508 .into_transaction_request();
509
510 tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
511
512 cast_send(
513 provider,
514 tx,
515 send_tx.cast_async,
516 send_tx.sync,
517 send_tx.confirmations,
518 send_tx.timeout.unwrap_or(config.transaction_timeout),
519 )
520 .await?
521 }
522 };
523 Ok(())
524 }
525}