Skip to main content

cast/cmd/
erc20.rs

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