feat: improve logic around hidden section of mini-portfolio balances spam tokens (#6988)

This commit is contained in:
Nate Wienert 2023-08-14 11:06:40 -10:00 committed by GitHub
parent 51dc10b467
commit 485764fe38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 32 deletions

@ -18,7 +18,9 @@ describe('Token explore filter', () => {
searchFor('dao')
cy.get('@filteredTokens').then((filteredTokens) => {
cy.get('[data-cy="token-name"]').should('deep.equal', filteredTokens)
cy.get('[data-cy="token-name"]').then((tokens) => {
cy.wrap(Array.from(tokens)).should('deep.equal', Array.from(filteredTokens))
})
})
})
})

File diff suppressed because one or more lines are too long

@ -3,7 +3,7 @@ import { TraceEvent } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/AccountDrawer/PrefetchBalancesWrapper'
import Row from 'components/Row'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import { PortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { EllipsisStyle, ThemedText } from 'theme'
import { formatNumber, NumberType } from 'utils/formatNumbers'
import { splitHiddenTokens } from 'utils/splitHiddenTokens'
import { useToggleAccountDrawer } from '../..'
import { PortfolioArrow } from '../../AuthenticatedHeader'
@ -20,12 +21,6 @@ import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean) {
return !hideSmallBalances || (tokenBalance.denominatedValue?.value ?? 0) > HIDE_SMALL_USD_BALANCES_THRESHOLD
}
export default function Tokens({ account }: { account: string }) {
const toggleWalletDrawer = useToggleAccountDrawer()
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
@ -33,27 +28,18 @@ export default function Tokens({ account }: { account: string }) {
const { data } = useCachedPortfolioBalancesQuery({ account })
const visibleTokens = useMemo(() => {
return !hideSmallBalances
? data?.portfolios?.[0].tokenBalances ?? []
: data?.portfolios?.[0].tokenBalances?.filter((tokenBalance) =>
meetsThreshold(tokenBalance, hideSmallBalances)
) ?? []
}, [data?.portfolios, hideSmallBalances])
const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
const hiddenTokens = useMemo(() => {
return !hideSmallBalances
? []
: data?.portfolios?.[0].tokenBalances?.filter(
(tokenBalance) => !meetsThreshold(tokenBalance, hideSmallBalances)
) ?? []
}, [data?.portfolios, hideSmallBalances])
const { visibleTokens, hiddenTokens } = useMemo(
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
[hideSmallBalances, tokenBalances]
)
if (!data) {
return <PortfolioSkeleton />
}
if (data?.portfolios?.[0].tokenBalances?.length === 0) {
if (tokenBalances?.length === 0) {
// TODO: consider launching moonpay here instead of just closing the drawer
return <EmptyWalletModule type="token" onNavigateClick={toggleWalletDrawer} />
}
@ -64,10 +50,7 @@ export default function Tokens({ account }: { account: string }) {
<PortfolioTabWrapper>
{visibleTokens.map(
(tokenBalance) =>
tokenBalance.token &&
meetsThreshold(tokenBalance, hideSmallBalances) && (
<TokenRow key={tokenBalance.id} {...tokenBalance} token={tokenBalance.token} />
)
tokenBalance.token && <TokenRow key={tokenBalance.id} {...tokenBalance} token={tokenBalance.token} />
)}
<ExpandoRow isExpanded={showHiddenTokens} toggle={toggleHiddenTokens} numItems={hiddenTokens.length}>
{hiddenTokens.map(
@ -86,10 +69,6 @@ const TokenNameText = styled(ThemedText.SubHeader)`
${EllipsisStyle}
`
type TokenBalance = NonNullable<
NonNullable<NonNullable<PortfolioBalancesQuery['portfolios']>[number]>['tokenBalances']
>[number]
type PortfolioToken = NonNullable<TokenBalance['token']>
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {

@ -35,6 +35,7 @@ gql`
tokenProject {
id
logoUrl
isSpam
}
}
token {

@ -0,0 +1,117 @@
import { Currency, TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { splitHiddenTokens } from './splitHiddenTokens'
const tokens: TokenBalance[] = [
// low balance
{
id: 'low-balance',
ownerAddress: '',
__typename: 'TokenBalance',
denominatedValue: {
id: '',
value: 0.5,
},
tokenProjectMarket: {
id: '',
currency: Currency.Eth,
tokenProject: {
id: '',
tokens: [],
isSpam: false,
},
},
},
// spam
{
id: 'spam',
ownerAddress: '',
__typename: 'TokenBalance',
denominatedValue: {
id: '',
value: 100,
},
tokenProjectMarket: {
id: '',
currency: Currency.Eth,
tokenProject: {
id: '',
tokens: [],
isSpam: true,
},
},
},
// valid
{
id: 'valid',
ownerAddress: '',
__typename: 'TokenBalance',
denominatedValue: {
id: '',
value: 100,
},
tokenProjectMarket: {
id: '',
currency: Currency.Eth,
tokenProject: {
id: '',
tokens: [],
isSpam: false,
},
},
},
// empty value
{
id: 'undefined-value',
ownerAddress: '',
__typename: 'TokenBalance',
denominatedValue: {
id: '',
// @ts-ignore this is evidently possible but not represented in our types
value: undefined,
},
tokenProjectMarket: {
id: '',
currency: Currency.Eth,
tokenProject: {
id: '',
tokens: [],
isSpam: false,
},
},
},
]
describe('splitHiddenTokens', () => {
it('splits spam tokens into hidden but keeps small balances if hideSmallBalances = false', () => {
const { visibleTokens, hiddenTokens } = splitHiddenTokens(tokens, {
hideSmallBalances: false,
})
expect(hiddenTokens.length).toBe(1)
expect(hiddenTokens[0].id).toBe('spam')
expect(visibleTokens.length).toBe(3)
expect(visibleTokens[0].id).toBe('low-balance')
expect(visibleTokens[1].id).toBe('valid')
})
it('splits low balance into hidden by default', () => {
const { visibleTokens, hiddenTokens } = splitHiddenTokens(tokens)
expect(hiddenTokens.length).toBe(2)
expect(hiddenTokens[0].id).toBe('low-balance')
expect(hiddenTokens[1].id).toBe('spam')
expect(visibleTokens.length).toBe(2)
expect(visibleTokens[0].id).toBe('valid')
})
it('splits undefined value tokens into visible', () => {
const { visibleTokens } = splitHiddenTokens(tokens)
expect(visibleTokens.length).toBe(2)
expect(visibleTokens[0].id).toBe('valid')
expect(visibleTokens[1].id).toBe('undefined-value')
})
})

@ -0,0 +1,39 @@
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
export function splitHiddenTokens(
tokenBalances: TokenBalance[],
options?: {
hideSmallBalances?: boolean
}
) {
const visibleTokens: TokenBalance[] = []
const hiddenTokens: TokenBalance[] = []
for (const tokenBalance of tokenBalances) {
const isValidValue =
// if undefined we keep visible (see https://linear.app/uniswap/issue/WEB-1940/[mp]-update-how-we-handle-what-goes-in-hidden-token-section-of-mini)
typeof tokenBalance.denominatedValue?.value === 'undefined' ||
// if below $1
options?.hideSmallBalances === false ||
meetsThreshold(tokenBalance)
if (
isValidValue &&
// a spam token
!tokenBalance.tokenProjectMarket?.tokenProject?.isSpam
) {
visibleTokens.push(tokenBalance)
} else {
hiddenTokens.push(tokenBalance)
}
}
return { visibleTokens, hiddenTokens }
}
function meetsThreshold(tokenBalance: TokenBalance) {
const value = tokenBalance.denominatedValue?.value ?? 0
return value > HIDE_SMALL_USD_BALANCES_THRESHOLD
}