feat: [info] add multi-chain balances on TDP (#7493)

* feat: wip, [info] add TDP crosschain balances

* very wip new balances

* progress on balances

* wip new balance

* add todo for native tokens

* fix bridge info caching

* fix bridge info caching & clean up

* cleanup query logic

* remove pollinginterval enum change

* fix logo flickering

* minor comment cleanup

* more minor comment cleanup

* use gqlToCurrency instead

* css changes for balance box

* css changes for mobile balance summary footer

* fix apollo client caching tokens merge

* clarify comment

* make chainId required

* comment cleanup

* fix: balance fetch caching

* fix prefetchbalancewrapper css jank

* remove padding

* delete extraneous borderRadius

* update comment

* should not show balancecard at all if no balances

* rename to multichain

* changes to mobile bar css

* use surface1 theme background

* oops add back bottom-bar

* fix cypress tests ??

* revert change

* broken apollo merge??

* remove extraneous tokens call

* remove apollo merge for portfolio>tokens

* oops fix some pr review

* load portfolio balances as it updates

* pr review

* update comment linear ticket

* remove extraneous chainId prop

* increase timeout time

* should not do symbols check

* pr review

* pr review

* refactor multichainbalances into map

* remove address native

* nit pr review

* use portfoliobalance fragment

* fix typechecking gql

* TYPES

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>
This commit is contained in:
Kristie Huang 2023-11-27 13:40:19 -05:00 committed by GitHub
parent 4a5a41c59e
commit fc7ecc7e3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 395 additions and 146 deletions

@ -110,7 +110,7 @@ const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>`
margin-top: 1px;
`
function calculcateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
function calculateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
if (!price0 || !price1) return undefined
const value0 = parseFloat(position.amount0.toExact()) * price0
@ -124,7 +124,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
const { chainId, position, pool, details, inRange, closed } = positionInfo
const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
const liquidityValue = calculateLiquidityValue(priceA, priceB, position)
const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer()

@ -4,7 +4,8 @@ import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrap
import Row from 'components/Row'
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
import { PortfolioToken } from 'graphql/data/portfolios'
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
@ -28,7 +29,7 @@ export default function Tokens({ account }: { account: string }) {
const { data } = useCachedPortfolioBalancesQuery({ account })
const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
const tokenBalances = data?.portfolios?.[0].tokenBalances
const { visibleTokens, hiddenTokens } = useMemo(
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
@ -69,9 +70,12 @@ const TokenNameText = styled(ThemedText.SubHeader)`
${EllipsisStyle}
`
type PortfolioToken = NonNullable<TokenBalance['token']>
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
function TokenRow({
token,
quantity,
denominatedValue,
tokenProjectMarket,
}: PortfolioTokenBalancePartsFragment & { token: PortfolioToken }) {
const { formatDelta } = useFormatter()
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0

@ -3,6 +3,7 @@ import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphq
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
import usePrevious from 'hooks/usePrevious'
import { atom, useAtom } from 'jotai'
import ms from 'ms'
import { PropsWithChildren, useCallback, useEffect } from 'react'
import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks'
@ -31,8 +32,9 @@ const hasUnfetchedBalancesAtom = atom<boolean>(true)
export default function PrefetchBalancesWrapper({
children,
shouldFetchOnAccountUpdate,
shouldFetchOnHover = true,
className,
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; className?: string }>) {
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; shouldFetchOnHover?: boolean; className?: string }>) {
const { account } = useWeb3React()
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
@ -40,8 +42,13 @@ export default function PrefetchBalancesWrapper({
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
const fetchBalances = useCallback(() => {
if (account) {
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
setHasUnfetchedBalances(false)
// Backend takes <2sec to get the updated portfolio value after a transaction
// This timeout is an interim solution while we're working on a websocket that'll ping the client when connected account gets changes
// TODO(WEB-3131): remove this timeout after websocket is implemented
setTimeout(() => {
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
setHasUnfetchedBalances(false)
}, ms('3.5s'))
}
}, [account, prefetchPortfolioBalances, setHasUnfetchedBalances])
@ -62,12 +69,18 @@ export default function PrefetchBalancesWrapper({
}
}, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances])
// Temporary workaround to fix balances on TDP - this fetches balances if shouldFetchOnAccountUpdate becomes true while hasUnfetchedBalances is true
// TODO(WEB-3071) remove this logic once balance provider refactor is done
useEffect(() => {
if (hasUnfetchedBalances && shouldFetchOnAccountUpdate) fetchBalances()
}, [fetchBalances, hasUnfetchedBalances, shouldFetchOnAccountUpdate])
const onHover = useCallback(() => {
if (hasUnfetchedBalances) fetchBalances()
}, [fetchBalances, hasUnfetchedBalances])
return (
<div onMouseEnter={onHover} className={className}>
<div onMouseEnter={shouldFetchOnHover ? onHover : undefined} className={className}>
{children}
</div>
)

@ -5,7 +5,6 @@ import { ChainId, Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { Trace } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
@ -101,7 +100,7 @@ export function CurrencySearch({
}, [chainId, data?.portfolios])
const sortedTokens: Token[] = useMemo(() => {
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances
const portfolioTokens = splitHiddenTokens(portfolioTokenBalances ?? [])
.visibleTokens.map((tokenBalance) => {
if (!tokenBalance?.token?.chain || !tokenBalance.token?.address || !tokenBalance.token?.decimals) {

@ -1,22 +1,30 @@
import { Trans } from '@lingui/macro'
import { ChainId, Currency } from '@uniswap/sdk-core'
import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import { getChainInfo } from 'constants/chainInfo'
import { asSupportedChain } from 'constants/chains'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
import { Chain, PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { useMemo } from 'react'
import styled, { useTheme } from 'styled-components'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const BalancesCard = styled.div`
border-radius: 16px;
import { MultiChainMap } from '.'
const BalancesCard = styled.div<{ isInfoTDPEnabled?: boolean }>`
color: ${({ theme }) => theme.neutral1};
display: none;
display: flex;
flex-direction: column;
gap: 24px;
height: fit-content;
padding: 16px;
${({ isInfoTDPEnabled }) => !isInfoTDPEnabled && 'padding: 16px;'}
width: 100%;
// 768 hardcoded to match NFT-redesign navbar breakpoints
@ -48,11 +56,13 @@ const BalanceContainer = styled.div`
flex: 1;
`
const BalanceAmountsContainer = styled.div`
const BalanceAmountsContainer = styled.div<{ isInfoTDPEnabled?: boolean }>`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'margin-left: 12px;'}
`
const StyledNetworkLabel = styled.div`
@ -61,49 +71,187 @@ const StyledNetworkLabel = styled.div`
line-height: 16px;
`
export default function BalanceSummary({ token }: { token: Currency }) {
const { account, chainId } = useWeb3React()
const theme = useTheme()
const { label, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
const balance = useCurrencyBalance(account, token)
const { formatCurrencyAmount } = useFormatter()
interface BalanceProps {
currency?: Currency
chainId?: ChainId
balance?: CurrencyAmount<Currency> // TODO(WEB-3026): only used for pre-Info-project calculations, should remove after project goes live
gqlBalance?: PortfolioTokenBalancePartsFragment
onClick?: () => void
}
const Balance = ({ currency, chainId = ChainId.MAINNET, balance, gqlBalance, onClick }: BalanceProps) => {
const { formatCurrencyAmount, formatNumber } = useFormatter()
const { label: chainName, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
const currencies = useMemo(() => [currency], [currency])
const isInfoTDPEnabled = useInfoExplorePageEnabled()
const formattedBalance = formatCurrencyAmount({
amount: balance,
type: NumberType.TokenNonTx,
})
const formattedUsdValue = formatCurrencyAmount({
amount: useStablecoinValue(balance),
type: NumberType.FiatTokenStats,
type: NumberType.PortfolioBalance,
})
const formattedGqlBalance = formatNumber({
input: gqlBalance?.quantity,
type: NumberType.TokenNonTx,
})
const formattedUsdGqlValue = formatNumber({
input: gqlBalance?.denominatedValue?.value,
type: NumberType.PortfolioBalance,
})
const currencies = useMemo(() => [token], [token])
if (isInfoTDPEnabled) {
return (
<BalanceRow onClick={onClick}>
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
<BalanceAmountsContainer isInfoTDPEnabled>
<BalanceItem>
<ThemedText.BodyPrimary>{formattedUsdGqlValue}</ThemedText.BodyPrimary>
</BalanceItem>
<BalanceItem>
<ThemedText.BodySecondary>{formattedGqlBalance}</ThemedText.BodySecondary>
</BalanceItem>
</BalanceAmountsContainer>
</BalanceRow>
)
} else {
return (
<BalanceRow>
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
<BalanceContainer>
<BalanceAmountsContainer>
<BalanceItem>
<ThemedText.SubHeader>
{formattedBalance} {currency?.symbol}
</ThemedText.SubHeader>
</BalanceItem>
<BalanceItem>
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
</BalanceItem>
</BalanceAmountsContainer>
<StyledNetworkLabel color={color}>{chainName}</StyledNetworkLabel>
</BalanceContainer>
</BalanceRow>
)
}
}
if (!account || !balance) {
const ConnectedChainBalanceSummary = ({
connectedChainBalance,
}: {
connectedChainBalance?: CurrencyAmount<Currency>
}) => {
const { chainId: connectedChainId } = useWeb3React()
if (!connectedChainId || !connectedChainBalance || !connectedChainBalance.greaterThan(0)) return null
const token = connectedChainBalance.currency
const { label: chainName } = getChainInfo(asSupportedChain(connectedChainId) ?? ChainId.MAINNET)
return (
<BalanceSection>
<ThemedText.SubHeaderSmall color="neutral1">
<Trans>Your balance on {chainName}</Trans>
</ThemedText.SubHeaderSmall>
<Balance currency={token} chainId={connectedChainId} balance={connectedChainBalance} />
</BalanceSection>
)
}
const PageChainBalanceSummary = ({ pageChainBalance }: { pageChainBalance?: PortfolioTokenBalancePartsFragment }) => {
if (!pageChainBalance || !pageChainBalance.token) return null
const currency = gqlToCurrency(pageChainBalance.token)
return (
<BalanceSection>
<ThemedText.HeadlineSmall color="neutral1">
<Trans>Your balance</Trans>
</ThemedText.HeadlineSmall>
<Balance currency={currency} chainId={currency?.chainId} gqlBalance={pageChainBalance} />
</BalanceSection>
)
}
const OtherChainsBalanceSummary = ({
otherChainBalances,
hasPageChainBalance,
}: {
otherChainBalances: readonly PortfolioTokenBalancePartsFragment[]
hasPageChainBalance: boolean
}) => {
const navigate = useNavigate()
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
if (!otherChainBalances.length) return null
return (
<BalanceSection>
{hasPageChainBalance ? (
<ThemedText.SubHeaderSmall>
<Trans>On other networks</Trans>
</ThemedText.SubHeaderSmall>
) : (
<ThemedText.HeadlineSmall>
<Trans>Balance on other networks</Trans>
</ThemedText.HeadlineSmall>
)}
{otherChainBalances.map((balance) => {
const currency = balance.token && gqlToCurrency(balance.token)
const chainId = (balance.token && supportedChainIdFromGQLChain(balance.token.chain)) ?? ChainId.MAINNET
return (
<Balance
key={balance.id}
currency={currency}
chainId={chainId}
gqlBalance={balance}
onClick={() =>
navigate(
getTokenDetailsURL({
address: balance.token?.address,
chain: balance.token?.chain ?? Chain.Ethereum,
isInfoExplorePageEnabled,
})
)
}
/>
)
})}
</BalanceSection>
)
}
export default function BalanceSummary({
currency,
chain,
multiChainMap,
}: {
currency: Currency
chain: Chain
multiChainMap: MultiChainMap
}) {
const { account } = useWeb3React()
const isInfoTDPEnabled = useInfoTDPEnabled()
const connectedChainBalance = useCurrencyBalance(account, currency)
const pageChainBalance = multiChainMap[chain].balance
const otherChainBalances: PortfolioTokenBalancePartsFragment[] = []
for (const [key, value] of Object.entries(multiChainMap)) {
if (key !== chain && value.balance !== undefined) {
otherChainBalances.push(value.balance)
}
}
const hasBalances = pageChainBalance || Boolean(otherChainBalances.length)
if (!account || !hasBalances) {
return null
}
return (
<BalancesCard>
<BalanceSection>
<ThemedText.SubHeaderSmall color={theme.neutral1}>
<Trans>Your balance on {label}</Trans>
</ThemedText.SubHeaderSmall>
<BalanceRow>
<PortfolioLogo currencies={currencies} chainId={token.chainId} size="2rem" />
<BalanceContainer>
<BalanceAmountsContainer>
<BalanceItem>
<ThemedText.SubHeader>
{formattedBalance} {token.symbol}
</ThemedText.SubHeader>
</BalanceItem>
<BalanceItem>
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
</BalanceItem>
</BalanceAmountsContainer>
<StyledNetworkLabel color={color}>{label}</StyledNetworkLabel>
</BalanceContainer>
</BalanceRow>
</BalanceSection>
<BalancesCard isInfoTDPEnabled={isInfoTDPEnabled}>
{!isInfoTDPEnabled && <ConnectedChainBalanceSummary connectedChainBalance={connectedChainBalance} />}
{isInfoTDPEnabled && (
<>
<PageChainBalanceSummary pageChainBalance={pageChainBalance} />
<OtherChainsBalanceSummary otherChainBalances={otherChainBalances} hasPageChainBalance={!!pageChainBalance} />
</>
)}
</BalancesCard>
)
}

@ -2,21 +2,20 @@ import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import styled from 'styled-components'
import { StyledInternalLink } from 'theme/components'
import styled, { css } from 'styled-components'
import { StyledInternalLink, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const Wrapper = styled.div`
const Wrapper = styled.div<{ isInfoTDPEnabled?: boolean }>`
align-content: center;
align-items: center;
border: 1px solid ${({ theme }) => theme.surface3};
border-bottom: none;
background-color: ${({ theme }) => theme.surface1};
border-radius: 20px 20px 0px 0px;
bottom: 52px;
border: 1px solid ${({ theme }) => theme.surface3};
color: ${({ theme }) => theme.neutral2};
display: flex;
flex-direction: row;
@ -26,9 +25,24 @@ const Wrapper = styled.div`
justify-content: space-between;
left: 0;
line-height: 20px;
padding: 12px 16px;
position: fixed;
width: 100%;
${({ isInfoTDPEnabled }) =>
isInfoTDPEnabled
? css`
border-radius: 20px;
bottom: 56px;
margin: 8px;
padding: 12px 32px;
width: calc(100vw - 16px);
`
: css`
border-bottom: none;
border-radius: 20px 20px 0px 0px;
bottom: 52px;
padding: 12px 16px;
width: 100%;
`}
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
bottom: 0px;
@ -37,27 +51,29 @@ const Wrapper = styled.div`
display: none;
}
`
const BalanceValue = styled.div`
const BalanceValue = styled.div<{ isInfoTDPEnabled?: boolean }>`
color: ${({ theme }) => theme.neutral1};
font-size: 20px;
line-height: 28px;
line-height: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '20px' : '28px')};
display: flex;
gap: 8px;
`
const Balance = styled.div`
align-items: center;
const Balance = styled.div<{ isInfoTDPEnabled?: boolean }>`
align-items: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? 'flex-end' : 'center')};
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
`
const BalanceInfo = styled.div`
const BalanceInfo = styled.div<{ isInfoTDPEnabled?: boolean }>`
display: flex;
flex: 10 1 auto;
flex-direction: column;
justify-content: flex-start;
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'gap: 6px;'}
`
const FiatValue = styled.span`
const FiatValue = styled(ThemedText.Caption)<{ isInfoTDPEnabled?: boolean }>`
${({ isInfoTDPEnabled, theme }) => !isInfoTDPEnabled && `color: ${theme.neutral2};`}
font-size: 12px;
line-height: 16px;
@ -65,15 +81,15 @@ const FiatValue = styled.span`
line-height: 24px;
}
`
const SwapButton = styled(StyledInternalLink)`
const SwapButton = styled(StyledInternalLink)<{ isInfoTDPEnabled?: boolean }>`
background-color: ${({ theme }) => theme.accent1};
border: none;
border-radius: 12px;
border-radius: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '22px' : '12px')};
color: ${({ theme }) => theme.deprecated_accentTextLightPrimary};
display: flex;
flex: 1 1 auto;
padding: 12px 16px;
font-size: 1em;
font-size: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '16px' : '1em')};
font-weight: 535;
height: 44px;
justify-content: center;
@ -81,10 +97,18 @@ const SwapButton = styled(StyledInternalLink)`
max-width: 100vw;
`
export default function MobileBalanceSummaryFooter({ token }: { token: Currency }) {
export default function MobileBalanceSummaryFooter({
currency,
pageChainBalance,
}: {
currency: Currency
pageChainBalance?: PortfolioTokenBalancePartsFragment
}) {
const isInfoTDPEnabled = useInfoTDPEnabled()
const { account } = useWeb3React()
const balance = useCurrencyBalance(account, token)
const { formatCurrencyAmount } = useFormatter()
const balance = useCurrencyBalance(account, currency)
const { formatCurrencyAmount, formatNumber } = useFormatter()
const formattedBalance = formatCurrencyAmount({
amount: balance,
type: NumberType.TokenNonTx,
@ -93,22 +117,35 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
amount: useStablecoinValue(balance),
type: NumberType.FiatTokenStats,
})
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
const formattedGqlBalance = formatNumber({
input: pageChainBalance?.quantity,
type: NumberType.TokenNonTx,
})
const formattedUsdGqlValue = formatNumber({
input: pageChainBalance?.denominatedValue?.value,
type: NumberType.PortfolioBalance,
})
const chain = CHAIN_ID_TO_BACKEND_NAME[currency.chainId].toLowerCase()
return (
<Wrapper>
{Boolean(account && balance) && (
<BalanceInfo>
<Trans>Your {token.symbol} balance</Trans>
<Balance>
<BalanceValue>
{formattedBalance} {token.symbol}
<Wrapper isInfoTDPEnabled={isInfoTDPEnabled}>
{Boolean(account && (isInfoTDPEnabled ? pageChainBalance : balance)) && (
<BalanceInfo isInfoTDPEnabled={isInfoTDPEnabled}>
{isInfoTDPEnabled ? <Trans>Your balance</Trans> : <Trans>Your {currency.symbol} balance</Trans>}
<Balance isInfoTDPEnabled={isInfoTDPEnabled}>
<BalanceValue isInfoTDPEnabled={isInfoTDPEnabled}>
{isInfoTDPEnabled ? formattedGqlBalance : formattedBalance} {currency.symbol}
</BalanceValue>
<FiatValue>{formattedUsdValue}</FiatValue>
<FiatValue isInfoTDPEnabled={isInfoTDPEnabled}>
{isInfoTDPEnabled ? `(${formattedUsdGqlValue})` : formattedUsdValue}
</FiatValue>
</Balance>
</BalanceInfo>
)}
<SwapButton to={`/swap?chainName=${chain}&outputCurrency=${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
<SwapButton
isInfoTDPEnabled={isInfoTDPEnabled}
to={`/swap?chainName=${chain}&outputCurrency=${currency.isNative ? NATIVE_CHAIN_ID : currency.address}`}
>
<Trans>Swap</Trans>
</SwapButton>
</Wrapper>

@ -3,12 +3,11 @@ import { InterfacePageName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { Trace } from 'analytics'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { AboutSection } from 'components/Tokens/TokenDetails/About'
import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
import { BreadcrumbNav, BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
import TokenDetailsSkeleton, {
Hr,
@ -25,8 +24,13 @@ import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
import {
Chain,
PortfolioTokenBalancePartsFragment,
TokenPriceQuery,
TokenQuery,
} from 'graphql/data/__generated__/types-and-hooks'
import { TokenQueryData } from 'graphql/data/Token'
import { getTokenDetailsURL, gqlToCurrency, InterfaceGqlChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
@ -41,7 +45,9 @@ import { CopyContractAddress } from 'theme/components'
import { isAddress, shortenAddress } from 'utils'
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
import BalanceSummary from './BalanceSummary'
import InvalidTokenDetails from './InvalidTokenDetails'
import MobileBalanceSummaryFooter from './MobileBalanceSummaryFooter'
import { TokenDescription } from './TokenDescription'
const TokenSymbol = styled.span`
@ -94,7 +100,7 @@ function useRelevantToken(
[onChainToken, queryToken]
)
}
export type MultiChainMap = { [chain: string]: { address?: string; balance?: PortfolioTokenBalancePartsFragment } }
type TokenDetailsProps = {
urlAddress?: string
inputTokenAddress?: string
@ -117,17 +123,25 @@ export default function TokenDetails({
[urlAddress]
)
const { chainId: connectedChainId } = useWeb3React()
const { account, chainId: connectedChainId } = useWeb3React()
const pageChainId = supportedChainIdFromGQLChain(chain)
const tokenQueryData = tokenQuery.token
const crossChainMap = useMemo(
() =>
tokenQueryData?.project?.tokens.reduce((map, current) => {
if (current) map[current.chain] = current.address
return map
}, {} as { [key: string]: string | undefined }) ?? {},
[tokenQueryData]
)
const { data: balanceQuery } = useCachedPortfolioBalancesQuery({ account })
const multiChainMap = useMemo(() => {
const tokenBalances = balanceQuery?.portfolios?.[0].tokenBalances
const tokensAcrossChains = tokenQueryData?.project?.tokens
if (!tokensAcrossChains) return {}
return tokensAcrossChains.reduce((map, current) => {
if (current) {
if (!map[current.chain]) {
map[current.chain] = {}
}
map[current.chain].address = current.address
map[current.chain].balance = tokenBalances?.find((tokenBalance) => tokenBalance.token?.id === current.id)
}
return map
}, {} as MultiChainMap)
}, [balanceQuery?.portfolios, tokenQueryData?.project?.tokens])
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
@ -143,7 +157,7 @@ export default function TokenDetails({
const navigateToTokenForChain = useCallback(
(update: Chain) => {
if (!address) return
const bridgedAddress = crossChainMap[update]
const bridgedAddress = multiChainMap[update].address
if (bridgedAddress) {
startTokenTransition(() =>
navigate(
@ -158,7 +172,7 @@ export default function TokenDetails({
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled })))
}
},
[address, crossChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
[address, multiChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
)
useOnGlobalChainSwitch(navigateToTokenForChain)
@ -283,7 +297,7 @@ export default function TokenDetails({
/>
</div>
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
{!isInfoTDPEnabled && detailedToken && <BalanceSummary token={detailedToken} />}
{detailedToken && <BalanceSummary currency={detailedToken} chain={chain} multiChainMap={multiChainMap} />}
{isInfoTDPEnabled && (
<TokenDescription
tokenAddress={address}
@ -293,7 +307,9 @@ export default function TokenDetails({
/>
)}
</RightPanel>
{!isInfoTDPEnabled && detailedToken && <MobileBalanceSummaryFooter token={detailedToken} />}
{detailedToken && (
<MobileBalanceSummaryFooter currency={detailedToken} pageChainBalance={multiChainMap[chain].balance} />
)}
<TokenSafetyModal
isOpen={openTokenSafetyModal || !!continueSwap}

@ -97,7 +97,4 @@ gql`
}
}
`
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
export type TokenQueryData = TokenQuery['token']

@ -49,6 +49,22 @@ export const apolloClient = new ApolloClient({
},
},
},
TokenProject: {
fields: {
tokens: {
// cache data may be lost when replacing the tokens array
merge(existing, incoming) {
if (!existing) {
return incoming
} else if (Array.isArray(existing)) {
return [...existing, ...incoming]
} else {
return [existing, ...incoming]
}
},
},
},
},
},
}),
defaultOptions: {

@ -1,6 +1,39 @@
import gql from 'graphql-tag'
import { PortfolioTokenBalancePartsFragment } from './__generated__/types-and-hooks'
gql`
fragment PortfolioTokenBalanceParts on TokenBalance {
id
quantity
denominatedValue {
id
currency
value
}
token {
id
chain
address
name
symbol
standard
decimals
}
tokenProjectMarket {
id
pricePercentChange(duration: DAY) {
id
value
}
tokenProject {
id
logoUrl
isSpam
}
}
}
query PortfolioBalances($ownerAddress: String!, $chains: [Chain!]!) {
portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
id
@ -19,35 +52,10 @@ gql`
}
}
tokenBalances {
id
quantity
denominatedValue {
id
currency
value
}
tokenProjectMarket {
id
pricePercentChange(duration: DAY) {
id
value
}
tokenProject {
id
logoUrl
isSpam
}
}
token {
id
chain
address
name
symbol
standard
decimals
}
...PortfolioTokenBalanceParts
}
}
}
`
export type PortfolioToken = NonNullable<PortfolioTokenBalancePartsFragment['token']>

@ -1,5 +1,5 @@
import { useWeb3React } from '@web3-react/core'
import { Chain } from 'graphql/data/Token'
import { Chain } from 'graphql/data/__generated__/types-and-hooks'
import { chainIdToBackendName } from 'graphql/data/util'
import { useEffect, useRef } from 'react'

@ -108,6 +108,10 @@ export function useTokenBalance(account?: string, token?: Token): CurrencyAmount
return tokenBalances[token.address]
}
/**
* Returns balances for tokens on currently-connected chainId via RPC.
* See useCachedPortfolioBalancesQuery for multichain portfolio balances via GQL.
*/
export function useCurrencyBalances(
account?: string,
currencies?: (Currency | undefined)[]

@ -1,3 +1,4 @@
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import TokenDetails from 'components/Tokens/TokenDetails'
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
@ -7,10 +8,15 @@ import useParsedQueryString from 'hooks/useParsedQueryString'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import styled from 'styled-components'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
const StyledPrefetchBalancesWrapper = styled(PrefetchBalancesWrapper)`
display: contents;
`
export default function TokenDetailsPage() {
const { tokenAddress, chainName } = useParams<{
tokenAddress: string
@ -58,12 +64,14 @@ export default function TokenDetailsPage() {
if (!tokenQuery) return <TokenDetailsPageSkeleton />
return (
<TokenDetails
urlAddress={tokenAddress}
chain={chain}
tokenQuery={tokenQuery}
tokenPriceQuery={currentPriceQuery}
inputTokenAddress={parsedInputTokenAddress}
/>
<StyledPrefetchBalancesWrapper shouldFetchOnAccountUpdate={true} shouldFetchOnHover={false}>
<TokenDetails
urlAddress={tokenAddress}
chain={chain}
tokenQuery={tokenQuery}
tokenPriceQuery={currentPriceQuery}
inputTokenAddress={parsedInputTokenAddress}
/>
</StyledPrefetchBalancesWrapper>
)
}

@ -1,7 +1,6 @@
import { ChainId, Currency } from '@uniswap/sdk-core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Chain } from 'graphql/data/Token'
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
export type CurrencyKey = string

@ -1,15 +1,15 @@
import { TokenBalance, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { PortfolioTokenBalancePartsFragment, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
export function splitHiddenTokens(
tokenBalances: TokenBalance[],
tokenBalances: readonly PortfolioTokenBalancePartsFragment[],
options: {
hideSmallBalances?: boolean
} = { hideSmallBalances: true }
) {
const visibleTokens: TokenBalance[] = []
const hiddenTokens: TokenBalance[] = []
const visibleTokens: PortfolioTokenBalancePartsFragment[] = []
const hiddenTokens: PortfolioTokenBalancePartsFragment[] = []
for (const tokenBalance of tokenBalances) {
// 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)
@ -29,7 +29,7 @@ export function splitHiddenTokens(
return { visibleTokens, hiddenTokens }
}
function meetsThreshold(tokenBalance: TokenBalance) {
function meetsThreshold(tokenBalance: PortfolioTokenBalancePartsFragment) {
const value = tokenBalance.denominatedValue?.value ?? 0
return value > HIDE_SMALL_USD_BALANCES_THRESHOLD
}