1+ import type { Asset } from '@metamask/assets-controllers' ;
12import { useSelector } from 'react-redux' ;
23import { useCallback , useMemo } from 'react' ;
34import { BigNumber } from 'bignumber.js' ;
@@ -32,30 +33,6 @@ export interface EnrichTokenRequest {
3233
3334const EMPTY_REQUESTS : EnrichTokenRequest [ ] = [ ] ;
3435
35- function useAccountGroupAssets ( accountAddress ?: string | null ) {
36- const internalAccountsById = useSelector ( selectInternalAccountsById ) ;
37- const accountToGroupMap = useSelector ( selectAccountToGroupMap ) ;
38-
39- const accountGroupId = useMemo ( ( ) => {
40- if ( ! accountAddress ) return undefined ;
41- const internalAccountId = Object . keys ( internalAccountsById ) . find (
42- ( id ) =>
43- internalAccountsById [ id ] . address . toLowerCase ( ) ===
44- accountAddress . toLowerCase ( ) ,
45- ) ;
46- if ( ! internalAccountId ) return undefined ;
47- return accountToGroupMap [ internalAccountId ] ?. id ;
48- } , [ accountAddress , internalAccountsById , accountToGroupMap ] ) ;
49-
50- const selectOverrideAssets = useCallback (
51- ( state : RootState ) => selectAssetsByAccountGroupId ( state , accountGroupId ) ,
52- [ accountGroupId ] ,
53- ) ;
54-
55- const overrideAssets = useSelector ( selectOverrideAssets ) ;
56- return accountGroupId ? overrideAssets : undefined ;
57- }
58-
5936export function useAccountTokens ( {
6037 includeNoBalance = false ,
6138 tokenFilter,
@@ -111,18 +88,11 @@ export function useAccountTokens({
11188 } ) ;
11289 } , [ assets , includeNoBalance , tokenFilter ] ) ;
11390
114- // useTokenFiatRates crashes on non-hex addresses (Solana etc.), so we
115- // build requests for EVM assets only. Non-EVM assets are handled below in
116- // the render memo by falling back to `asset.fiat.balance`.
11791 const fiatRateRequests = useMemo < TokenFiatRateRequest [ ] > (
11892 ( ) =>
11993 assetsWithBalance
120- . filter ( ( asset ) => asset . chainId && ! isNonEvmChainId ( asset . chainId ) )
121- . map ( ( asset ) => ( {
122- address : asset . address as Hex ,
123- chainId : asset . chainId as Hex ,
124- currency : fiatCurrency . toLowerCase ( ) ,
125- } ) ) ,
94+ . filter ( isEvmRateEligible )
95+ . map ( ( asset ) => buildFiatRateRequest ( asset , fiatCurrency ) ) ,
12696 [ assetsWithBalance , fiatCurrency ] ,
12797 ) ;
12898
@@ -146,38 +116,30 @@ export function useAccountTokens({
146116 Boolean ( chainId ) &&
147117 isNetworkTestnet ( chainId as string ) ;
148118
149- // EVM assets use the useTokenFiatRates-derived rate (picks up stablecoin
150- // bypass, and lets us convert to USD in pay flow). Non-EVM assets fall
151- // back to the assets-controller's preferred-currency `fiat.balance`,
152- // which is the pre-existing behavior for Solana/Bitcoin/etc.
153- // EVM assets and rates are in lockstep — both arrays derived from the
154- // same predicate over the same list, so a single index walk pairs them.
119+ const sortableFiatByAsset = new WeakMap < AssetType , BigNumber > ( ) ;
155120 let evmIndex = 0 ;
156121 const processedAssets = assetsWithBalance . map ( ( asset ) => {
157- const isEvm = asset . chainId && ! isNonEvmChainId ( asset . chainId ) ;
158- const rate = isEvm ? fiatRates [ evmIndex ++ ] : undefined ;
159-
160- let fiatAmount : BigNumber . Value | undefined ;
161- if ( isFiatHidden ( asset . chainId ) ) {
162- fiatAmount = undefined ;
163- } else if ( isEvm ) {
164- fiatAmount =
165- rate !== undefined && asset . balance
166- ? new BigNumber ( asset . balance ) . multipliedBy ( rate )
167- : undefined ;
168- } else {
169- fiatAmount = asset . fiat ?. balance ;
170- }
122+ const rate = isEvmRateEligible ( asset ) ? fiatRates [ evmIndex ++ ] : undefined ;
123+ const fiatAmount = deriveAssetFiat (
124+ asset ,
125+ rate ,
126+ isFiatHidden ( asset . chainId ) ,
127+ ) ;
171128
172129 const balanceInSelectedCurrency =
173130 fiatAmount !== undefined ? formatFiat ( fiatAmount ) : undefined ;
174131
175- return {
132+ const processed = {
176133 ...asset ,
177134 networkBadgeSource : getNetworkBadgeSource ( asset . chainId as Hex ) ,
178135 balanceInSelectedCurrency,
179136 standard : TokenStandard . ERC20 as const ,
180137 } as AssetType ;
138+
139+ if ( fiatAmount !== undefined ) {
140+ sortableFiatByAsset . set ( processed , fiatAmount ) ;
141+ }
142+ return processed ;
181143 } ) ;
182144
183145 if ( enrichTokenRequests . length > 0 ) {
@@ -218,10 +180,16 @@ export function useAccountTokens({
218180 }
219181 }
220182
221- const sortableFiatBalance = ( asset : AssetType ) =>
222- isFiatHidden ( asset . chainId )
223- ? new BigNumber ( 0 )
224- : new BigNumber ( asset . fiat ?. balance || 0 ) ;
183+ // Sort by the same fiat we display. Falls back to the assets-controller
184+ // preferred-currency balance for tokens that have no derived fiat (e.g.
185+ // enrichment placeholders added below with `zeroFiat`), so unknown
186+ // tokens sink to the bottom.
187+ const sortableFiatBalance = ( asset : AssetType ) => {
188+ if ( isFiatHidden ( asset . chainId ) ) return new BigNumber ( 0 ) ;
189+ const derived = sortableFiatByAsset . get ( asset ) ;
190+ if ( derived !== undefined ) return derived ;
191+ return new BigNumber ( asset . fiat ?. balance || 0 ) ;
192+ } ;
225193
226194 return processedAssets . sort (
227195 ( a , b ) => sortableFiatBalance ( b ) . comparedTo ( sortableFiatBalance ( a ) ) || 0 ,
@@ -236,3 +204,83 @@ export function useAccountTokens({
236204 fiatRates ,
237205 ] ) as unknown as AssetType [ ] ;
238206}
207+
208+ function useAccountGroupAssets ( accountAddress ?: string | null ) {
209+ const internalAccountsById = useSelector ( selectInternalAccountsById ) ;
210+ const accountToGroupMap = useSelector ( selectAccountToGroupMap ) ;
211+
212+ const accountGroupId = useMemo ( ( ) => {
213+ if ( ! accountAddress ) return undefined ;
214+ const internalAccountId = Object . keys ( internalAccountsById ) . find (
215+ ( id ) =>
216+ internalAccountsById [ id ] . address . toLowerCase ( ) ===
217+ accountAddress . toLowerCase ( ) ,
218+ ) ;
219+ if ( ! internalAccountId ) return undefined ;
220+ return accountToGroupMap [ internalAccountId ] ?. id ;
221+ } , [ accountAddress , internalAccountsById , accountToGroupMap ] ) ;
222+
223+ const selectOverrideAssets = useCallback (
224+ ( state : RootState ) => selectAssetsByAccountGroupId ( state , accountGroupId ) ,
225+ [ accountGroupId ] ,
226+ ) ;
227+
228+ const overrideAssets = useSelector ( selectOverrideAssets ) ;
229+ return accountGroupId ? overrideAssets : undefined ;
230+ }
231+
232+ /**
233+ * Whether the asset is EVM-scoped with a hex address, i.e. eligible for
234+ * `useTokenFiatRates` (which crashes on non-hex addresses like Solana).
235+ * Non-EVM assets fall back to `asset.fiat.balance` in the render loop.
236+ *
237+ * This predicate is the single source of truth for the paired walks over
238+ * `assetsWithBalance` — request build and rate consumption — so they cannot
239+ * drift out of lockstep.
240+ */
241+ function isEvmRateEligible ( asset : Asset ) : boolean {
242+ return (
243+ Boolean ( asset . chainId ) &&
244+ ! isNonEvmChainId ( asset . chainId ) &&
245+ 'address' in asset &&
246+ Boolean ( asset . address )
247+ ) ;
248+ }
249+
250+ function buildFiatRateRequest (
251+ asset : Asset ,
252+ currency : string ,
253+ ) : TokenFiatRateRequest {
254+ return {
255+ address : ( asset as { address : Hex } ) . address ,
256+ chainId : asset . chainId as Hex ,
257+ currency : currency . toLowerCase ( ) ,
258+ } ;
259+ }
260+
261+ /**
262+ * Fiat amount to display for a single asset, or `undefined` to hide.
263+ *
264+ * - Testnet-hidden → undefined.
265+ * - EVM with rate → `balance * rate` (picks up stablecoin bypass).
266+ * - EVM zero balance without rate → `0` (currency-invariant, avoids
267+ * hiding `$0` rows when market data hasn't loaded).
268+ * - EVM non-zero without rate → undefined (can't safely render).
269+ * - Non-EVM → the assets-controller's preferred-currency `fiat.balance`.
270+ */
271+ function deriveAssetFiat (
272+ asset : Asset ,
273+ rate : number | undefined ,
274+ isFiatHidden : boolean ,
275+ ) : BigNumber | undefined {
276+ if ( isFiatHidden ) return undefined ;
277+
278+ if ( isEvmRateEligible ( asset ) ) {
279+ const balance = asset . balance ? new BigNumber ( asset . balance ) : undefined ;
280+ if ( rate !== undefined && balance ) return balance . multipliedBy ( rate ) ;
281+ if ( balance ?. isZero ( ) ) return new BigNumber ( 0 ) ;
282+ return undefined ;
283+ }
284+
285+ return asset . fiat ?. balance ? new BigNumber ( asset . fiat . balance ) : undefined ;
286+ }
0 commit comments