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