mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-07-26 22:53:37 +00:00
fix(balances): don't hide unknown-price tokens as dust (renders Lux treasury)
The dust filter hid any token with fiatBalance < $0.01. On chains with no fiat price oracle the Client Gateway returns fiatConversion "0" (and thus fiatBalance "0") for every token — Lux/Zoo/Pars C-Chains — so the filter wrongly hid the entire native balance. safe.lux.network showed the Lux Foundation treasury (0xE54cAf7E, ~994.7B LUX) as an empty "1 small balance" instead of its real holdings. A token is only dust if we can price it. Keep tokens whose price is unknown (fiatConversion <= 0) and the native token; only hide genuinely-priced dust. Decomplected: the two duplicate filterDustTokens copies (useVisibleBalances, useHiddenTokenCounts) are now one price-aware helper in utils/balances.ts, so the assets table and the "N small balances" indicator can never disagree. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -384,4 +384,68 @@ describe('useVisibleBalances', () => {
|
||||
|
||||
expect(result.current.balances.items).toHaveLength(2)
|
||||
})
|
||||
|
||||
test('keeps tokens with unknown price (no oracle) even when hideDust is enabled', () => {
|
||||
// On chains with no fiat price oracle the CGW returns fiatConversion "0" and
|
||||
// fiatBalance "0" for every token (Lux/Zoo/Pars C-Chains). Such a token's fiat
|
||||
// value is unknown, not zero, so it must not be filtered as dust — this is the
|
||||
// Lux Foundation treasury case (~994.7B LUX, fiatConversion "0").
|
||||
const nativeAddress = toBeHex('0x0', 20)
|
||||
const balance: Balances = {
|
||||
fiatTotal: '0',
|
||||
items: [
|
||||
{
|
||||
balance: '994729897350342903214765220166',
|
||||
fiatBalance: '0',
|
||||
fiatConversion: '0',
|
||||
tokenInfo: {
|
||||
address: nativeAddress,
|
||||
decimals: 18,
|
||||
logoUri: '',
|
||||
name: 'Lux',
|
||||
symbol: 'LUX',
|
||||
type: TokenType.NATIVE_TOKEN,
|
||||
},
|
||||
},
|
||||
{
|
||||
balance: '5000',
|
||||
fiatBalance: '0',
|
||||
fiatConversion: '0',
|
||||
tokenInfo: {
|
||||
address: visibleTokenAddress,
|
||||
decimals: 18,
|
||||
logoUri: '',
|
||||
name: 'Unpriced ERC20',
|
||||
symbol: 'UNP',
|
||||
type: TokenType.ERC20,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
jest.spyOn(useBalancesHooks, 'default').mockImplementation(() => ({
|
||||
balances: balance,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
}))
|
||||
|
||||
jest.spyOn(store, 'useAppSelector').mockImplementation((selector) =>
|
||||
selector({
|
||||
settings: {
|
||||
currency: 'USD',
|
||||
shortName: { copy: true, qr: true, show: true },
|
||||
theme: { darkMode: false },
|
||||
hiddenTokens: { ['4']: [] },
|
||||
hideDust: true,
|
||||
},
|
||||
chains: { data: [], error: undefined, loading: false, loaded: true },
|
||||
} as unknown as store.RootState),
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useVisibleBalances())
|
||||
|
||||
expect(result.current.balances.items).toHaveLength(2)
|
||||
expect(result.current.balances.items.map((i) => i.tokenInfo.symbol)).toEqual(['LUX', 'UNP'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useHasFeature } from './useChains'
|
||||
import useSafeInfo from './useSafeInfo'
|
||||
import { useBalancesGetBalancesV1Query } from '@safe-global/store/gateway/AUTO_GENERATED/balances'
|
||||
import { FEATURES } from '@safe-global/utils/utils/chains'
|
||||
import { DUST_THRESHOLD } from '@/config/constants'
|
||||
import { filterDustTokens } from '@/utils/balances'
|
||||
import useBalances from './useBalances'
|
||||
import useHiddenTokens from './useHiddenTokens'
|
||||
|
||||
@@ -14,11 +14,6 @@ export interface HiddenTokenCounts {
|
||||
hiddenByDustFilter: number
|
||||
}
|
||||
|
||||
const filterDustTokens = (items: ReturnType<typeof useBalances>['balances']['items'], hideDust: boolean) => {
|
||||
if (!hideDust) return items
|
||||
return items.filter((balanceItem) => Number(balanceItem.fiatBalance) >= DUST_THRESHOLD)
|
||||
}
|
||||
|
||||
export const useHiddenTokenCounts = (): HiddenTokenCounts => {
|
||||
const { balances: currentBalances } = useBalances()
|
||||
const hiddenTokens = useHiddenTokens()
|
||||
|
||||
@@ -5,7 +5,7 @@ import useHiddenTokens from './useHiddenTokens'
|
||||
import type { PortfolioBalances } from './loadables/useLoadBalances'
|
||||
import { useAppSelector } from '@/store'
|
||||
import { selectHideDust } from '@/store/settingsSlice'
|
||||
import { DUST_THRESHOLD } from '@/config/constants'
|
||||
import { filterDustTokens } from '@/utils/balances'
|
||||
import useSafeInfo from './useSafeInfo'
|
||||
import { useNativeTokenDisplay } from './useNativeTokenDisplay'
|
||||
import { TokenType } from '@safe-global/store/gateway/types'
|
||||
@@ -29,11 +29,6 @@ const truncateNumber = (balance: string): string => {
|
||||
const filterHiddenTokens = (items: PortfolioBalances['items'], hiddenAssets: string[]) =>
|
||||
items.filter((balanceItem) => !hiddenAssets.includes(balanceItem.tokenInfo.address))
|
||||
|
||||
const filterDustTokens = (items: PortfolioBalances['items'], hideDust: boolean) => {
|
||||
if (!hideDust) return items
|
||||
return items.filter((balanceItem) => Number(balanceItem.fiatBalance) >= DUST_THRESHOLD)
|
||||
}
|
||||
|
||||
const getVisibleFiatTotal = (balances: PortfolioBalances, hiddenAssets: string[]): string => {
|
||||
return safeFormatUnits(
|
||||
balances.items
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { DUST_THRESHOLD } from '@/config/constants'
|
||||
import { TokenType } from '@safe-global/store/gateway/types'
|
||||
|
||||
type DustFilterableItem = {
|
||||
fiatBalance: string
|
||||
fiatConversion: string
|
||||
tokenInfo: { type: string }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide "dust" — tokens worth less than DUST_THRESHOLD in the selected fiat currency.
|
||||
*
|
||||
* A token can only be dust if we can actually price it. On chains with no fiat
|
||||
* price oracle the Client Gateway returns `fiatConversion: "0"` (e.g. the
|
||||
* Lux/Zoo/Pars C-Chains), which makes `fiatBalance` always "0" too. That means
|
||||
* the fiat value is UNKNOWN, not zero — filtering on it would wrongly hide the
|
||||
* entire balance, however large (the Lux Foundation treasury holds ~994.7B LUX).
|
||||
* So a token is kept when it is the native token, when its price is unknown, or
|
||||
* when its priced value clears the threshold. Only genuinely-priced dust is hidden.
|
||||
*/
|
||||
export const filterDustTokens = <T extends DustFilterableItem>(items: T[], hideDust: boolean): T[] => {
|
||||
if (!hideDust) return items
|
||||
return items.filter(
|
||||
(item) =>
|
||||
item.tokenInfo.type === TokenType.NATIVE_TOKEN ||
|
||||
!(Number(item.fiatConversion) > 0) ||
|
||||
Number(item.fiatBalance) >= DUST_THRESHOLD,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user