From 165c16716c25029d2ba189605c8ad4474a6c2df9 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:53:33 -0300 Subject: [PATCH 1/6] feat(token-select): sort by positive balance by default when wallet connected - Add sortByBalance prop to TokenSelect / TokenInput / TokenDropdown; defaults to isWalletConnected so held tokens surface without any consumer-side change - Decouple balance fetch/sort from showBalance so the balance column and the sort are independent concerns; showBalance only controls the per-row balance column - Add updateTokensWithRawBalances export for on-chain multicall path - Add multicall fallback in useTokens: when the active chain is not covered by LI.FI (e.g. Sepolia), fetch ERC-20 balances via multicall and native balance via getBalance, then sort with the same sortFn - Add tests asserting positive-balance tokens precede zero-balance tokens and that source order is preserved within each group Closes #466 --- .../sharedComponents/TokenDropdown.tsx | 3 +- .../sharedComponents/TokenInput/index.tsx | 5 +- .../TokenSelect/List/TokenBalance.tsx | 10 +- .../sharedComponents/TokenSelect/index.tsx | 11 +- src/hooks/useTokens.test.ts | 107 +++++++++++++- src/hooks/useTokens.ts | 130 ++++++++++++++++-- 6 files changed, 244 insertions(+), 22 deletions(-) diff --git a/src/components/sharedComponents/TokenDropdown.tsx b/src/components/sharedComponents/TokenDropdown.tsx index 5e68f989..ba34631f 100644 --- a/src/components/sharedComponents/TokenDropdown.tsx +++ b/src/components/sharedComponents/TokenDropdown.tsx @@ -28,8 +28,9 @@ type Props = ComponentPropsWithoutRef<'span'> & TokenDropdownProps * @param {string} [props.placeholder] - Placeholder text for the search input. * @param {number} [props.containerHeight] - Height of the virtualized tokens list. * @param {number} [props.itemHeight] - Height of each item in the tokens list. - * @param {boolean} [props.showBalance] - Whether to show the token balance in the list. + * @param {boolean} [props.showBalance] - Whether to show the token balance column in each row. * @param {boolean} [props.showTopTokens] - Whether to show the top tokens section in the list. + * @param {boolean} [props.sortByBalance] - Sort tokens with a positive balance to the top, ordered by USD value descending. Defaults to true when a wallet is connected. * @param {ComponentPropsWithoutRef<'span'>} props.restProps - Additional props for the span element. * * @example diff --git a/src/components/sharedComponents/TokenInput/index.tsx b/src/components/sharedComponents/TokenInput/index.tsx index 7497c428..8fb86b61 100644 --- a/src/components/sharedComponents/TokenInput/index.tsx +++ b/src/components/sharedComponents/TokenInput/index.tsx @@ -58,8 +58,9 @@ type Props = FlexProps & TokenInputProps * @param {number} [props.iconSize=32] - Optional size of the token icon in the list. Default is 32. * @param {number} [props.itemHeight=64] - Optional height of each item in the list. Default is 64. * @param {boolean} [props.showAddTokenButton=false] - Optional flag to allow adding a token. Default is false. - * @param {boolean} [props.showBalance=false] - Optional flag to show the token balance in the list. Default is false. + * @param {boolean} [props.showBalance=false] - Optional flag to show the token balance column in each row. Default is false. * @param {boolean} [props.showTopTokens=false] - Optional flag to show the top tokens in the list. Default is false. + * @param {boolean} [props.sortByBalance] - Sort tokens with a positive balance to the top, ordered by USD value descending. Defaults to true when a wallet is connected. */ const TokenInput: FC = ({ containerHeight, @@ -73,6 +74,7 @@ const TokenInput: FC = ({ showBalance, showTopTokens, singleToken, + sortByBalance, thousandSeparator = true, title, tokenInput, @@ -230,6 +232,7 @@ const TokenInput: FC = ({ showAddTokenButton={showAddTokenButton} showBalance={showBalance} showTopTokens={showTopTokens} + sortByBalance={sortByBalance} > (({ isLoading, token if (hasExtensions) { const balance = formatUnits((token.extensions?.balance ?? 0n) as bigint, token.decimals) - const value = ( - Number.parseFloat((token.extensions?.priceUSD ?? '0') as string) * Number.parseFloat(balance) - ).toFixed(2) + const priceUSD = token.extensions?.priceUSD as string | undefined + const usdLabel = + priceUSD !== undefined + ? `$ ${(Number.parseFloat(priceUSD) * Number.parseFloat(balance)).toFixed(2)}` + : NO_PRICE_DATA_LABEL return ( {balance} - $ {value} + {usdLabel} ) } diff --git a/src/components/sharedComponents/TokenSelect/index.tsx b/src/components/sharedComponents/TokenSelect/index.tsx index b2baead7..f037ffbd 100644 --- a/src/components/sharedComponents/TokenSelect/index.tsx +++ b/src/components/sharedComponents/TokenSelect/index.tsx @@ -25,6 +25,7 @@ export interface TokenSelectProps { showAddTokenButton?: boolean showTopTokens?: boolean showBalance?: boolean + sortByBalance?: boolean } /** @ignore */ @@ -42,8 +43,9 @@ type Props = FlexProps & TokenSelectProps * @param {number} [props.iconSize=32] - Optional size of the token icon in the list. Default is 32. * @param {number} [props.itemHeight=64] - Optional height of each item in the list. Default is 64. * @param {boolean} [props.showAddTokenButton=false] - Optional flag to allow adding a token. Default is false. - * @param {boolean} [props.showBalance=false] - Optional flag to show the token balance in the list. Default is false. + * @param {boolean} [props.showBalance=false] - Optional flag to show the token balance column in each row. Default is false. * @param {boolean} [props.showTopTokens=false] - Optional flag to show the top tokens in the list. Default is false. + * @param {boolean} [props.sortByBalance] - Sort tokens with a positive balance to the top, ordered by USD value descending. Defaults to true when a wallet is connected. */ const TokenSelect = withSuspenseAndRetry( ({ @@ -59,9 +61,10 @@ const TokenSelect = withSuspenseAndRetry( showAddTokenButton = false, showBalance = false, showTopTokens = false, + sortByBalance, ...restProps }) => { - const { appChainId, walletChainId } = useWeb3Status() + const { appChainId, isWalletConnected, walletChainId } = useWeb3Status() const [chainId, setChainId] = useState(() => getValidChainId({ @@ -122,9 +125,11 @@ const TokenSelect = withSuspenseAndRetry( previousDepsRef.current = [appChainId, currentNetworkId, walletChainId] }, [appChainId, currentNetworkId, networks, walletChainId]) + const resolvedSortByBalance = sortByBalance ?? isWalletConnected + const { isLoadingBalances, tokensByChainId } = useTokens({ chainId, - withBalance: showBalance, + withBalance: showBalance || resolvedSortByBalance, }) const { searchResult, searchTerm, setSearchTerm } = useTokenSearch( diff --git a/src/hooks/useTokens.test.ts b/src/hooks/useTokens.test.ts index f3d3b62c..8523eaec 100644 --- a/src/hooks/useTokens.test.ts +++ b/src/hooks/useTokens.test.ts @@ -2,7 +2,7 @@ import type { TokenAmount, TokensResponse } from '@lifi/sdk' import { zeroAddress } from 'viem' import { describe, expect, it, vi } from 'vitest' import type { Token, Tokens } from '@/src/types/token' -import { updateTokensBalances } from './useTokens' +import { updateTokensBalances, updateTokensWithRawBalances } from './useTokens' // Mimic a setup that overrides PUBLIC_NATIVE_TOKEN_ADDRESS to the Aave-style sentinel // (0xEeee...), which env.ts lowercases. The merge must bridge this back to LI.FI's @@ -31,6 +31,14 @@ const makeLifiToken = (address: string, symbol: string, decimals: number, priceU priceUSD, }) +const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + +const threeTokens: Tokens = [ + { chainId: 1, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, + { chainId: 1, address: usdcAddress, name: 'USD Coin', symbol: 'USDC', decimals: 6 }, + { chainId: 1, address: daiAddress, name: 'Dai', symbol: 'DAI', decimals: 18 }, +] + describe('updateTokensBalances', () => { it('merges LI.FI native balance onto a local native token that uses a non-zero sentinel', () => { const prices: TokensResponse = { @@ -68,4 +76,101 @@ describe('updateTokensBalances', () => { expect(token.extensions?.priceUSD).toBe('0') } }) + + it('places tokens with positive balance before zero-balance tokens', () => { + const prices: TokensResponse = { + tokens: { + 1: [ + makeLifiToken(LOCAL_NATIVE, 'ETH', 18, '2300'), + makeLifiToken(usdcAddress, 'USDC', 6, '1'), + makeLifiToken(daiAddress, 'DAI', 18, '1'), + ], + }, + } + const balances: TokenAmount[] = [ + { ...makeLifiToken(LOCAL_NATIVE, 'ETH', 18, '2300'), amount: 0n }, + { ...makeLifiToken(usdcAddress, 'USDC', 6, '1'), amount: 5_000_000n }, + { ...makeLifiToken(daiAddress, 'DAI', 18, '1'), amount: 0n }, + ] + + const { tokens } = updateTokensBalances(threeTokens, [balances, prices]) + + expect(tokens[0].symbol).toBe('USDC') + expect(tokens[1].symbol).toBe('ETH') + expect(tokens[2].symbol).toBe('DAI') + }) + + it('zero-balance tokens retain source order among themselves', () => { + const prices: TokensResponse = { + tokens: { + 1: [ + makeLifiToken(LOCAL_NATIVE, 'ETH', 18, '2300'), + makeLifiToken(usdcAddress, 'USDC', 6, '1'), + makeLifiToken(daiAddress, 'DAI', 18, '1'), + ], + }, + } + const balances: TokenAmount[] = [] + + const { tokens } = updateTokensBalances(threeTokens, [balances, prices]) + + expect(tokens[0].symbol).toBe('ETH') + expect(tokens[1].symbol).toBe('USDC') + expect(tokens[2].symbol).toBe('DAI') + }) +}) + +describe('updateTokensWithRawBalances', () => { + it('places tokens with positive balance before zero-balance tokens', () => { + const rawBalances: Record> = { + 11155111: { + [LOCAL_NATIVE]: 2_000_000_000_000_000_000n, + [usdcAddress]: 0n, + }, + } + const sepoliaTokens: Tokens = [ + { chainId: 11155111, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, + { chainId: 11155111, address: usdcAddress, name: 'USD Coin', symbol: 'USDC', decimals: 6 }, + ] + + const { tokens } = updateTokensWithRawBalances(sepoliaTokens, rawBalances) + + expect(tokens[0].symbol).toBe('ETH') + expect(tokens[1].symbol).toBe('USDC') + }) + + it('zero-balance tokens retain source order', () => { + const rawBalances: Record> = { + 11155111: { + [LOCAL_NATIVE]: 0n, + [usdcAddress]: 0n, + [daiAddress]: 0n, + }, + } + const sepoliaTokens: Tokens = [ + { chainId: 11155111, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, + { chainId: 11155111, address: usdcAddress, name: 'USD Coin', symbol: 'USDC', decimals: 6 }, + { chainId: 11155111, address: daiAddress, name: 'Dai', symbol: 'DAI', decimals: 18 }, + ] + + const { tokens } = updateTokensWithRawBalances(sepoliaTokens, rawBalances) + + expect(tokens[0].symbol).toBe('ETH') + expect(tokens[1].symbol).toBe('USDC') + expect(tokens[2].symbol).toBe('DAI') + }) + + it('sets balance extension but omits priceUSD so the balance column shows N/A', () => { + const rawBalances: Record> = { + 11155111: { [LOCAL_NATIVE]: 500n }, + } + const sepoliaTokens: Tokens = [ + { chainId: 11155111, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, + ] + + const { tokens } = updateTokensWithRawBalances(sepoliaTokens, rawBalances) + + expect(tokens[0].extensions?.balance).toBe(500n) + expect(tokens[0].extensions?.priceUSD).toBeUndefined() + }) }) diff --git a/src/hooks/useTokens.ts b/src/hooks/useTokens.ts index 10c9e87d..4acbe196 100644 --- a/src/hooks/useTokens.ts +++ b/src/hooks/useTokens.ts @@ -9,14 +9,15 @@ import { } from '@lifi/sdk' import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' -import { type Address, type Chain, formatUnits } from 'viem' +import { type Address, type Chain, erc20Abi, formatUnits, getAddress } from 'viem' +import { usePublicClient } from 'wagmi' import { env } from '@/src/env' import { useTokenLists } from '@/src/hooks/useTokenLists' import { useWeb3Status } from '@/src/hooks/useWeb3Status' import { lifiRpcUrls } from '@/src/lib/networks.config' import type { Token, Tokens } from '@/src/types/token' -import { toLocalNativeAddress } from '@/src/utils/address' +import { isNativeToken, toLocalNativeAddress } from '@/src/utils/address' import { logger } from '@/src/utils/logger' import type { TokensMap } from '@/src/utils/tokenListsCache' @@ -128,31 +129,87 @@ export const useTokens = ( enabled: canFetchBalance && !!tokensPricesByChain && chainsToFetch.length > 0, }) + // Multicall fallback: used when a specific chain is provided and LI.FI does not + // cover it (e.g. Sepolia). We wait for the LI.FI chains list to load so we can + // confirm the chain is absent before triggering on-chain calls. + const publicClient = usePublicClient({ chainId }) + const useOnchainFallback = + canFetchBalance && !!chainId && !isLoadingChains && !!chains && !lifiChainsId.includes(chainId) + + const { data: onchainBalances, isLoading: isLoadingOnchainBalances } = useQuery({ + queryKey: ['onchain', 'balances', account, chainId], + queryFn: async () => { + // biome-ignore lint/style/noNonNullAssertion: chainId guarded by enabled: useOnchainFallback + const tokensForChain = tokensData.tokensByChainId[chainId!] ?? [] + const nativeTokens = tokensForChain.filter((t) => isNativeToken(t.address)) + const erc20Tokens = tokensForChain.filter((t) => !isNativeToken(t.address)) + + const balances: Record = {} + + const nativeResults = await Promise.all( + nativeTokens.map((t) => + // biome-ignore lint/style/noNonNullAssertion: publicClient and account guarded by enabled: useOnchainFallback && !!publicClient + publicClient!.getBalance({ address: account! as Address }).then((b) => ({ t, b })), + ), + ) + for (const { t, b } of nativeResults) { + balances[t.address] = b + } + + if (erc20Tokens.length > 0) { + // biome-ignore lint/style/noNonNullAssertion: publicClient and account guarded by enabled: useOnchainFallback && !!publicClient + const results = await publicClient!.multicall({ + contracts: erc20Tokens.map((token) => ({ + address: getAddress(token.address), + abi: erc20Abi, + functionName: 'balanceOf' as const, + // biome-ignore lint/style/noNonNullAssertion: account guarded by enabled: useOnchainFallback + args: [account! as Address], + })), + }) + results.forEach((result, i) => { + balances[erc20Tokens[i].address] = + result.status === 'success' ? (result.result as bigint) : 0n + }) + } + + return balances + }, + staleTime: BALANCE_EXPIRATION_TIME, + refetchInterval: BALANCE_EXPIRATION_TIME, + gcTime: Number.POSITIVE_INFINITY, + enabled: useOnchainFallback && !!publicClient, + }) + const cache = useMemo(() => { - if ( - withBalance && - account && - !isLoadingPrices && - !isLoadingBalances && - tokensBalances && - tokensPricesByChain - ) { - return updateTokensBalances(tokensData.tokens, [tokensBalances, tokensPricesByChain]) + if (withBalance && account) { + if (!isLoadingPrices && !isLoadingBalances && tokensBalances && tokensPricesByChain) { + return updateTokensBalances(tokensData.tokens, [tokensBalances, tokensPricesByChain]) + } + if (useOnchainFallback && !isLoadingOnchainBalances && onchainBalances && chainId) { + return updateTokensWithRawBalances(tokensData.tokens, { [chainId]: onchainBalances }) + } } return tokensData }, [ account, + chainId, isLoadingBalances, + isLoadingOnchainBalances, isLoadingPrices, + onchainBalances, tokensBalances, tokensData, tokensPricesByChain, + useOnchainFallback, withBalance, ]) return { ...cache, - isLoadingBalances: Boolean(isLoadingChains || isLoadingBalances || isLoadingPrices), + isLoadingBalances: Boolean( + isLoadingChains || isLoadingBalances || isLoadingPrices || isLoadingOnchainBalances, + ), isLoadingPrices: Boolean(isLoadingChains || isLoadingPrices), } } @@ -233,6 +290,45 @@ export function updateTokensBalances( return { tokens: tokensWithBalances, tokensByChainId: tokensByChain } } +/** + * Updates tokens with raw on-chain balances (no price data), sorting tokens with + * a positive balance before zero-balance tokens. Within each group, source order + * is preserved. Used as a fallback for chains not covered by LI.FI. + * + * @param tokens - The array of tokens to enrich. + * @param rawBalances - Map of `chainId → address → bigint balance`. + * @returns Updated tokens and tokens grouped by chain ID. + */ +export function updateTokensWithRawBalances( + tokens: Tokens, + rawBalances: Record>, +) { + const tokensWithBalances = tokens.map( + (token): Token => ({ + ...token, + extensions: { + balance: rawBalances[token.chainId]?.[token.address] ?? 0n, + }, + }), + ) + + tokensWithBalances.sort(sortByBalancePresenceFn) + + const tokensByChainId = tokensWithBalances.reduce( + (acc, token) => { + if (!acc[token.chainId]) { + acc[token.chainId] = [token] + } else { + acc[token.chainId].push(token) + } + return acc + }, + {} as Record, + ) + + return { tokens: tokensWithBalances, tokensByChainId } +} + /** * A sorting function used to sort tokens by balance. * @param a The first token. @@ -248,3 +344,13 @@ function sortFn(a: Token, b: Token) { Number.parseFloat((a.extensions?.priceUSD as string) ?? '0') ) } + +/** + * Sorts tokens so those with a positive balance come first, preserving source + * order within each group. Used when USD price data is unavailable. + */ +function sortByBalancePresenceFn(a: Token, b: Token) { + const aHas = ((a.extensions?.balance as bigint) ?? 0n) > 0n ? 1 : 0 + const bHas = ((b.extensions?.balance as bigint) ?? 0n) > 0n ? 1 : 0 + return bHas - aHas +} From ebb18fbc127c0dae2dfa8c259a49899d3aa5fa5c Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:24:12 -0300 Subject: [PATCH 2/6] refactor(wallet): expose web3Status from useWalletStatus to avoid double hook call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WalletStatusVerifier was calling useWeb3Status() directly in addition to calling it internally via useWalletStatus(), executing the hook body twice per render. Exposing web3Status from useWalletStatus eliminates the redundant call. Also fixes || to ?? for chain ID fallback (prevents a falsy 0 chainId from silently falling through) and adds chainId={sepolia.id} to the TransactionButton in NativeToken — without it the inner button checked against appChainId instead of the Sepolia chain already verified by the parent WalletStatusVerifier. --- .../home/Examples/demos/TransactionButton/NativeToken.tsx | 2 ++ src/hooks/useWalletStatus.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx index 890f4225..c9055276 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx @@ -1,6 +1,7 @@ import { Dialog } from '@chakra-ui/react' import { type ReactElement, useState } from 'react' import { type Hash, parseEther, type TransactionReceipt } from 'viem' +import { sepolia } from 'viem/chains' import { useSendTransaction } from 'wagmi' import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper' import TransactionButton from '@/src/components/sharedComponents/TransactionButton' @@ -47,6 +48,7 @@ const NativeToken = () => { title="Native token demo" > Date: Tue, 21 Apr 2026 16:08:56 -0300 Subject: [PATCH 3/6] fix: address normalisation in multicall path and complete web3Status refactor - Lowercase token addresses when keying the on-chain balances map and when reading in updateTokensWithRawBalances, preventing silent zero-balance fallback if token lists mix address casing - Complete web3Status exposure from useWalletStatus (was missing from the previous commit): add web3Status to WalletStatus interface and return, remove the redundant useWeb3Status() call from WalletStatusVerifier - Assert result.current.web3Status in useWalletStatus tests - Update all useWalletStatus mock shapes across test files to carry web3Status - Document multicall fallback path and absent priceUSD in useTokens JSDoc - Add inline comment on NativeToken chainId prop - Move threeTokens fixture inside its describe block --- .../demos/TransactionButton/NativeToken.tsx | 2 + .../sharedComponents/SignButton.test.tsx | 4 ++ .../TransactionButton.test.tsx | 5 +++ .../WalletStatusVerifier.test.tsx | 40 +++++++++++-------- .../sharedComponents/WalletStatusVerifier.tsx | 5 +-- src/hooks/useTokens.test.ts | 11 +++-- src/hooks/useTokens.ts | 10 +++-- src/hooks/useWalletStatus.test.ts | 3 ++ src/hooks/useWalletStatus.ts | 8 ++-- src/hooks/useWeb3Status.test.ts | 21 ++++++---- 10 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx index c9055276..03d40677 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx @@ -47,6 +47,8 @@ const NativeToken = () => { text="Demo transaction that sends 0.1 Sepolia ETH from / to your wallet." title="Native token demo" > + {/* chainId must be explicit: the parent WalletStatusVerifier already verified Sepolia, + but TransactionButton checks against appChainId without it. */} { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra() @@ -79,6 +80,7 @@ describe('SignButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra( @@ -102,6 +104,7 @@ describe('SignButton', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra() @@ -119,6 +122,7 @@ describe('SignButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra() diff --git a/src/components/sharedComponents/TransactionButton.test.tsx b/src/components/sharedComponents/TransactionButton.test.tsx index 032dc575..26df9b8e 100644 --- a/src/components/sharedComponents/TransactionButton.test.tsx +++ b/src/components/sharedComponents/TransactionButton.test.tsx @@ -61,6 +61,7 @@ describe('TransactionButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra(Send) @@ -77,6 +78,7 @@ describe('TransactionButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra( @@ -102,6 +104,7 @@ describe('TransactionButton', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra(Send) @@ -121,6 +124,7 @@ describe('TransactionButton', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra( @@ -144,6 +148,7 @@ describe('TransactionButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: undefined as unknown as ReturnType['web3Status'], }) renderWithChakra(Send ETH) diff --git a/src/components/sharedComponents/WalletStatusVerifier.test.tsx b/src/components/sharedComponents/WalletStatusVerifier.test.tsx index 8487e9b3..7f9fc23e 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.test.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.test.tsx @@ -3,10 +3,26 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createElement, type ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Web3Status } from '@/src/hooks/useWeb3Status' import { useWeb3StatusConnected, WalletStatusVerifier } from './WalletStatusVerifier' const mockSwitchChain = vi.fn() +const mockWeb3Status = { + readOnlyClient: undefined, + appChainId: 1, + address: '0xdeadbeef', + balance: undefined, + connectingWallet: false, + switchingChain: false, + isWalletConnected: true, + walletClient: undefined, + isWalletSynced: true, + walletChainId: 1, + switchChain: vi.fn(), + disconnect: vi.fn(), +} as unknown as Web3Status + vi.mock('@/src/hooks/useWalletStatus', () => ({ useWalletStatus: vi.fn(() => ({ isReady: false, @@ -15,23 +31,7 @@ vi.mock('@/src/hooks/useWalletStatus', () => ({ targetChain: { id: 1, name: 'Ethereum' }, targetChainId: 1, switchChain: mockSwitchChain, - })), -})) - -vi.mock('@/src/hooks/useWeb3Status', () => ({ - useWeb3Status: vi.fn(() => ({ - readOnlyClient: {}, - appChainId: 1, - address: '0xdeadbeef', - balance: undefined, - connectingWallet: false, - switchingChain: false, - isWalletConnected: true, - walletClient: undefined, - isWalletSynced: true, - walletChainId: 1, - switchChain: vi.fn(), - disconnect: vi.fn(), + web3Status: mockWeb3Status, })), })) @@ -65,6 +65,7 @@ describe('WalletStatusVerifier', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: mockWeb3Status, }) renderWithChakra( @@ -87,6 +88,7 @@ describe('WalletStatusVerifier', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: mockWeb3Status, }) renderWithChakra( @@ -111,6 +113,7 @@ describe('WalletStatusVerifier', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, + web3Status: mockWeb3Status, }) renderWithChakra( @@ -134,6 +137,7 @@ describe('WalletStatusVerifier', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: mockWeb3Status, }) renderWithChakra( @@ -159,6 +163,7 @@ describe('WalletStatusVerifier', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, + web3Status: mockWeb3Status, }) renderWithChakra( @@ -179,6 +184,7 @@ describe('WalletStatusVerifier', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, + web3Status: mockWeb3Status, }) const ChildComponent = () => { diff --git a/src/components/sharedComponents/WalletStatusVerifier.tsx b/src/components/sharedComponents/WalletStatusVerifier.tsx index 4c883563..5d30c336 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.tsx @@ -1,7 +1,7 @@ import { createContext, type FC, type ReactElement, type ReactNode, useContext } from 'react' import SwitchChainButton from '@/src/components/sharedComponents/ui/SwitchChainButton' import { useWalletStatus } from '@/src/hooks/useWalletStatus' -import { useWeb3Status, type Web3Status } from '@/src/hooks/useWeb3Status' +import type { Web3Status } from '@/src/hooks/useWeb3Status' import type { ChainsIds } from '@/src/lib/networks.config' import { ConnectWalletButton } from '@/src/providers/Web3Provider' import type { RequiredNonNull } from '@/src/types/utils' @@ -51,9 +51,8 @@ const WalletStatusVerifier: FC = ({ fallback = , switchChainLabel = 'Switch to', }: WalletStatusVerifierProps) => { - const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } = + const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain, web3Status } = useWalletStatus({ chainId }) - const web3Status = useWeb3Status() if (needsConnect) { return fallback diff --git a/src/hooks/useTokens.test.ts b/src/hooks/useTokens.test.ts index 8523eaec..5d493772 100644 --- a/src/hooks/useTokens.test.ts +++ b/src/hooks/useTokens.test.ts @@ -33,13 +33,12 @@ const makeLifiToken = (address: string, symbol: string, decimals: number, priceU const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' -const threeTokens: Tokens = [ - { chainId: 1, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, - { chainId: 1, address: usdcAddress, name: 'USD Coin', symbol: 'USDC', decimals: 6 }, - { chainId: 1, address: daiAddress, name: 'Dai', symbol: 'DAI', decimals: 18 }, -] - describe('updateTokensBalances', () => { + const threeTokens: Tokens = [ + { chainId: 1, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, + { chainId: 1, address: usdcAddress, name: 'USD Coin', symbol: 'USDC', decimals: 6 }, + { chainId: 1, address: daiAddress, name: 'Dai', symbol: 'DAI', decimals: 18 }, + ] it('merges LI.FI native balance onto a local native token that uses a non-zero sentinel', () => { const prices: TokensResponse = { tokens: { diff --git a/src/hooks/useTokens.ts b/src/hooks/useTokens.ts index 4acbe196..69aa752a 100644 --- a/src/hooks/useTokens.ts +++ b/src/hooks/useTokens.ts @@ -41,6 +41,10 @@ export const lifiConfig = createConfig({ * - Automatic sorting by token value (balance × price) * - Periodic refetching for up-to-date balances and prices * + * On chains not covered by LI.FI (e.g. Sepolia), balance fetching falls back to a + * direct on-chain multicall. In this mode `priceUSD` is absent from token extensions, + * so any UI that reads `extensions.priceUSD` should treat `undefined` as "N/A". + * * @param {Object} params - Parameters for tokens fetching * @param {Address} [params.account] - Account address for balance fetching (defaults to connected wallet) * @param {Chain['id']} [params.chainId] - Specific chain ID to filter tokens (defaults to all supported chains) @@ -153,7 +157,7 @@ export const useTokens = ( ), ) for (const { t, b } of nativeResults) { - balances[t.address] = b + balances[t.address.toLowerCase()] = b } if (erc20Tokens.length > 0) { @@ -168,7 +172,7 @@ export const useTokens = ( })), }) results.forEach((result, i) => { - balances[erc20Tokens[i].address] = + balances[erc20Tokens[i].address.toLowerCase()] = result.status === 'success' ? (result.result as bigint) : 0n }) } @@ -307,7 +311,7 @@ export function updateTokensWithRawBalances( (token): Token => ({ ...token, extensions: { - balance: rawBalances[token.chainId]?.[token.address] ?? 0n, + balance: rawBalances[token.chainId]?.[token.address.toLowerCase()] ?? 0n, }, }), ) diff --git a/src/hooks/useWalletStatus.test.ts b/src/hooks/useWalletStatus.test.ts index ce5bd0fd..63ba8c2c 100644 --- a/src/hooks/useWalletStatus.test.ts +++ b/src/hooks/useWalletStatus.test.ts @@ -110,6 +110,9 @@ describe('useWalletStatus', () => { expect(result.current.needsConnect).toBe(false) expect(result.current.needsChainSwitch).toBe(false) expect(result.current.isReady).toBe(true) + expect(result.current.web3Status).toBeDefined() + expect(result.current.web3Status.appChainId).toBe(1) + expect(result.current.web3Status.isWalletConnected).toBe(true) }) it('uses provided chainId over appChainId', () => { diff --git a/src/hooks/useWalletStatus.ts b/src/hooks/useWalletStatus.ts index 44a04d7f..0a769bab 100644 --- a/src/hooks/useWalletStatus.ts +++ b/src/hooks/useWalletStatus.ts @@ -1,7 +1,7 @@ import type { Chain } from 'viem' import { extractChain } from 'viem' -import { useWeb3Status } from '@/src/hooks/useWeb3Status' +import { useWeb3Status, type Web3Status } from '@/src/hooks/useWeb3Status' import { type ChainsIds, chains } from '@/src/lib/networks.config' interface UseWalletStatusOptions { @@ -15,11 +15,12 @@ interface WalletStatus { targetChain: Chain targetChainId: ChainsIds switchChain: (chainId: ChainsIds) => void + web3Status: Web3Status } export const useWalletStatus = (options?: UseWalletStatusOptions): WalletStatus => { - const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = - useWeb3Status() + const web3Status = useWeb3Status() + const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = web3Status const targetChainId = options?.chainId ?? appChainId ?? chains[0].id const targetChain = extractChain({ chains, id: targetChainId }) @@ -35,5 +36,6 @@ export const useWalletStatus = (options?: UseWalletStatusOptions): WalletStatus targetChain, targetChainId, switchChain, + web3Status, } } diff --git a/src/hooks/useWeb3Status.test.ts b/src/hooks/useWeb3Status.test.ts index 3fe34ca3..02e71aec 100644 --- a/src/hooks/useWeb3Status.test.ts +++ b/src/hooks/useWeb3Status.test.ts @@ -141,15 +141,22 @@ describe('useWeb3StatusConnected', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: vi.fn(), + web3Status: { + address: '0xdeadbeef' as Address, + appChainId: 1, + balance: undefined, + connectingWallet: false, + disconnect: vi.fn(), + isWalletConnected: true, + isWalletSynced: true, + readOnlyClient: undefined, + switchChain: vi.fn(), + switchingChain: false, + walletChainId: 1, + walletClient: undefined, + } as unknown as ReturnType['web3Status'], }) - vi.mocked(wagmi.useAccount).mockReturnValueOnce({ - address: '0xdeadbeef' as Address, - chainId: 1, - isConnected: true, - isConnecting: false, - } as unknown as ReturnType) - const wrapper = ({ children }: { children: React.ReactNode }) => createElement(WalletStatusVerifier, null, children) From 9f5b7732935fff00e97bdf6080f97c18501ea44e Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:35:14 -0300 Subject: [PATCH 4/6] test(transaction-button): replace web3Status undefined cast with valid stub --- .../TransactionButton.test.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/sharedComponents/TransactionButton.test.tsx b/src/components/sharedComponents/TransactionButton.test.tsx index 26df9b8e..c733d990 100644 --- a/src/components/sharedComponents/TransactionButton.test.tsx +++ b/src/components/sharedComponents/TransactionButton.test.tsx @@ -2,6 +2,7 @@ import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react' import { render, screen } from '@testing-library/react' import { createElement, type ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Web3Status } from '@/src/hooks/useWeb3Status' import TransactionButton from './TransactionButton' const mockSwitchChain = vi.fn() @@ -43,6 +44,21 @@ vi.mock('wagmi', () => ({ const { useWalletStatus } = await import('@/src/hooks/useWalletStatus') const mockedUseWalletStatus = vi.mocked(useWalletStatus) +const mockWeb3Status = { + readOnlyClient: undefined, + appChainId: 1, + address: '0xdeadbeef', + balance: undefined, + connectingWallet: false, + switchingChain: false, + isWalletConnected: true, + walletClient: undefined, + isWalletSynced: true, + walletChainId: 1, + switchChain: vi.fn(), + disconnect: vi.fn(), +} as unknown as Web3Status + const system = createSystem(defaultConfig) const renderWithChakra = (ui: ReactNode) => @@ -61,7 +77,7 @@ describe('TransactionButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra(Send) @@ -78,7 +94,7 @@ describe('TransactionButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra( @@ -104,7 +120,7 @@ describe('TransactionButton', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra(Send) @@ -124,7 +140,7 @@ describe('TransactionButton', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra( @@ -148,7 +164,7 @@ describe('TransactionButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra(Send ETH) From d8c5ae20c67a08f39fd82b5fd1d11cb49266b147 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:36:30 -0300 Subject: [PATCH 5/6] test(sign-button): replace web3Status undefined cast with valid stub --- .../sharedComponents/SignButton.test.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/sharedComponents/SignButton.test.tsx b/src/components/sharedComponents/SignButton.test.tsx index b0f82cb9..4e5c40d5 100644 --- a/src/components/sharedComponents/SignButton.test.tsx +++ b/src/components/sharedComponents/SignButton.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react' import type { ReactNode } from 'react' import { createElement } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Web3Status } from '@/src/hooks/useWeb3Status' import SignButton from './SignButton' const mockSwitchChain = vi.fn() @@ -45,6 +46,21 @@ vi.mock('wagmi', () => ({ const { useWalletStatus } = await import('@/src/hooks/useWalletStatus') const mockedUseWalletStatus = vi.mocked(useWalletStatus) +const mockWeb3Status = { + readOnlyClient: undefined, + appChainId: 1, + address: '0xdeadbeef', + balance: undefined, + connectingWallet: false, + switchingChain: false, + isWalletConnected: true, + walletClient: undefined, + isWalletSynced: true, + walletChainId: 1, + switchChain: vi.fn(), + disconnect: vi.fn(), +} as unknown as Web3Status + const system = createSystem(defaultConfig) const renderWithChakra = (ui: ReactNode) => @@ -63,7 +79,7 @@ describe('SignButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra() @@ -80,7 +96,7 @@ describe('SignButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra( @@ -104,7 +120,7 @@ describe('SignButton', () => { >['targetChain'], targetChainId: 10, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra() @@ -122,7 +138,7 @@ describe('SignButton', () => { targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], targetChainId: 1, switchChain: mockSwitchChain, - web3Status: undefined as unknown as ReturnType['web3Status'], + web3Status: mockWeb3Status, }) renderWithChakra() From 0897b15b78b5fbb994ae7722a83df4f7c628ef78 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:45:51 -0300 Subject: [PATCH 6/6] fix(token-select): decouple sortByBalance from balance fetching --- .../sharedComponents/TokenSelect/index.tsx | 1 + src/hooks/useTokens.test.ts | 45 +++++++++++++++++++ src/hooks/useTokens.ts | 29 +++++++++--- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/components/sharedComponents/TokenSelect/index.tsx b/src/components/sharedComponents/TokenSelect/index.tsx index f037ffbd..28d0f51e 100644 --- a/src/components/sharedComponents/TokenSelect/index.tsx +++ b/src/components/sharedComponents/TokenSelect/index.tsx @@ -130,6 +130,7 @@ const TokenSelect = withSuspenseAndRetry( const { isLoadingBalances, tokensByChainId } = useTokens({ chainId, withBalance: showBalance || resolvedSortByBalance, + sortByBalance: resolvedSortByBalance, }) const { searchResult, searchTerm, setSearchTerm } = useTokenSearch( diff --git a/src/hooks/useTokens.test.ts b/src/hooks/useTokens.test.ts index 5d493772..b2b17906 100644 --- a/src/hooks/useTokens.test.ts +++ b/src/hooks/useTokens.test.ts @@ -117,6 +117,31 @@ describe('updateTokensBalances', () => { expect(tokens[1].symbol).toBe('USDC') expect(tokens[2].symbol).toBe('DAI') }) + + it('preserves source order when sortByBalance is false', () => { + const prices: TokensResponse = { + tokens: { + 1: [ + makeLifiToken(LOCAL_NATIVE, 'ETH', 18, '2300'), + makeLifiToken(usdcAddress, 'USDC', 6, '1'), + makeLifiToken(daiAddress, 'DAI', 18, '1'), + ], + }, + } + const balances: TokenAmount[] = [ + { ...makeLifiToken(LOCAL_NATIVE, 'ETH', 18, '2300'), amount: 0n }, + { ...makeLifiToken(usdcAddress, 'USDC', 6, '1'), amount: 5_000_000n }, + { ...makeLifiToken(daiAddress, 'DAI', 18, '1'), amount: 0n }, + ] + + const { tokens } = updateTokensBalances(threeTokens, [balances, prices], { + sortByBalance: false, + }) + + expect(tokens[0].symbol).toBe('ETH') + expect(tokens[1].symbol).toBe('USDC') + expect(tokens[2].symbol).toBe('DAI') + }) }) describe('updateTokensWithRawBalances', () => { @@ -172,4 +197,24 @@ describe('updateTokensWithRawBalances', () => { expect(tokens[0].extensions?.balance).toBe(500n) expect(tokens[0].extensions?.priceUSD).toBeUndefined() }) + + it('preserves source order when sortByBalance is false', () => { + const rawBalances: Record> = { + 11155111: { + [LOCAL_NATIVE]: 0n, + [usdcAddress]: 1_000_000n, + }, + } + const sepoliaTokens: Tokens = [ + { chainId: 11155111, address: LOCAL_NATIVE, name: 'Ether', symbol: 'ETH', decimals: 18 }, + { chainId: 11155111, address: usdcAddress, name: 'USD Coin', symbol: 'USDC', decimals: 6 }, + ] + + const { tokens } = updateTokensWithRawBalances(sepoliaTokens, rawBalances, { + sortByBalance: false, + }) + + expect(tokens[0].symbol).toBe('ETH') + expect(tokens[1].symbol).toBe('USDC') + }) }) diff --git a/src/hooks/useTokens.ts b/src/hooks/useTokens.ts index 69aa752a..95338b9b 100644 --- a/src/hooks/useTokens.ts +++ b/src/hooks/useTokens.ts @@ -49,6 +49,7 @@ export const lifiConfig = createConfig({ * @param {Address} [params.account] - Account address for balance fetching (defaults to connected wallet) * @param {Chain['id']} [params.chainId] - Specific chain ID to filter tokens (defaults to all supported chains) * @param {boolean} [params.withBalance=true] - Whether to fetch token balances + * @param {boolean} [params.sortByBalance=true] - Whether to sort tokens by balance. When false, source order is preserved even if balances are fetched. * * @returns {Object} Token data and loading state * @returns {Token[]} returns.tokens - Array of tokens with price and balance information @@ -78,12 +79,15 @@ export const useTokens = ( account, chainId, withBalance, + sortByBalance = true, }: { account?: Address chainId?: Chain['id'] withBalance?: boolean + sortByBalance?: boolean } = { withBalance: true, + sortByBalance: true, }, ) => { const { address } = useWeb3Status() @@ -188,10 +192,16 @@ export const useTokens = ( const cache = useMemo(() => { if (withBalance && account) { if (!isLoadingPrices && !isLoadingBalances && tokensBalances && tokensPricesByChain) { - return updateTokensBalances(tokensData.tokens, [tokensBalances, tokensPricesByChain]) + return updateTokensBalances(tokensData.tokens, [tokensBalances, tokensPricesByChain], { + sortByBalance, + }) } if (useOnchainFallback && !isLoadingOnchainBalances && onchainBalances && chainId) { - return updateTokensWithRawBalances(tokensData.tokens, { [chainId]: onchainBalances }) + return updateTokensWithRawBalances( + tokensData.tokens, + { [chainId]: onchainBalances }, + { sortByBalance }, + ) } } return tokensData @@ -202,6 +212,7 @@ export const useTokens = ( isLoadingOnchainBalances, isLoadingPrices, onchainBalances, + sortByBalance, tokensBalances, tokensData, tokensPricesByChain, @@ -228,6 +239,7 @@ export const useTokens = ( export function updateTokensBalances( tokens: Tokens, results: [Array, TokensResponse], + { sortByBalance = true }: { sortByBalance?: boolean } = {}, ) { const [balanceTokens, prices] = results @@ -273,9 +285,11 @@ export function updateTokensBalances( }) logger.timeEnd('extending tokens with balance info') - logger.time('sorting tokens by balance') - tokensWithBalances.sort(sortFn) - logger.timeEnd('sorting tokens by balance') + if (sortByBalance) { + logger.time('sorting tokens by balance') + tokensWithBalances.sort(sortFn) + logger.timeEnd('sorting tokens by balance') + } logger.time('updating tokens cache') const tokensByChain = tokensWithBalances.reduce( @@ -306,6 +320,7 @@ export function updateTokensBalances( export function updateTokensWithRawBalances( tokens: Tokens, rawBalances: Record>, + { sortByBalance = true }: { sortByBalance?: boolean } = {}, ) { const tokensWithBalances = tokens.map( (token): Token => ({ @@ -316,7 +331,9 @@ export function updateTokensWithRawBalances( }), ) - tokensWithBalances.sort(sortByBalancePresenceFn) + if (sortByBalance) { + tokensWithBalances.sort(sortByBalancePresenceFn) + } const tokensByChainId = tokensWithBalances.reduce( (acc, token) => {