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:
parent
4a5a41c59e
commit
fc7ecc7e3b
@ -110,7 +110,7 @@ const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>`
|
|||||||
margin-top: 1px;
|
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
|
if (!price0 || !price1) return undefined
|
||||||
|
|
||||||
const value0 = parseFloat(position.amount0.toExact()) * price0
|
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 { chainId, position, pool, details, inRange, closed } = positionInfo
|
||||||
|
|
||||||
const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
|
const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
|
||||||
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
|
const liquidityValue = calculateLiquidityValue(priceA, priceB, position)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||||
|
@ -4,7 +4,8 @@ import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrap
|
|||||||
import Row from 'components/Row'
|
import Row from 'components/Row'
|
||||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
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 { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||||
@ -28,7 +29,7 @@ export default function Tokens({ account }: { account: string }) {
|
|||||||
|
|
||||||
const { data } = useCachedPortfolioBalancesQuery({ account })
|
const { data } = useCachedPortfolioBalancesQuery({ account })
|
||||||
|
|
||||||
const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
|
const tokenBalances = data?.portfolios?.[0].tokenBalances
|
||||||
|
|
||||||
const { visibleTokens, hiddenTokens } = useMemo(
|
const { visibleTokens, hiddenTokens } = useMemo(
|
||||||
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
|
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
|
||||||
@ -69,9 +70,12 @@ const TokenNameText = styled(ThemedText.SubHeader)`
|
|||||||
${EllipsisStyle}
|
${EllipsisStyle}
|
||||||
`
|
`
|
||||||
|
|
||||||
type PortfolioToken = NonNullable<TokenBalance['token']>
|
function TokenRow({
|
||||||
|
token,
|
||||||
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
|
quantity,
|
||||||
|
denominatedValue,
|
||||||
|
tokenProjectMarket,
|
||||||
|
}: PortfolioTokenBalancePartsFragment & { token: PortfolioToken }) {
|
||||||
const { formatDelta } = useFormatter()
|
const { formatDelta } = useFormatter()
|
||||||
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphq
|
|||||||
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
|
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
|
||||||
import usePrevious from 'hooks/usePrevious'
|
import usePrevious from 'hooks/usePrevious'
|
||||||
import { atom, useAtom } from 'jotai'
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import ms from 'ms'
|
||||||
import { PropsWithChildren, useCallback, useEffect } from 'react'
|
import { PropsWithChildren, useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks'
|
import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks'
|
||||||
@ -31,8 +32,9 @@ const hasUnfetchedBalancesAtom = atom<boolean>(true)
|
|||||||
export default function PrefetchBalancesWrapper({
|
export default function PrefetchBalancesWrapper({
|
||||||
children,
|
children,
|
||||||
shouldFetchOnAccountUpdate,
|
shouldFetchOnAccountUpdate,
|
||||||
|
shouldFetchOnHover = true,
|
||||||
className,
|
className,
|
||||||
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; className?: string }>) {
|
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; shouldFetchOnHover?: boolean; className?: string }>) {
|
||||||
const { account } = useWeb3React()
|
const { account } = useWeb3React()
|
||||||
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
|
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
|
||||||
|
|
||||||
@ -40,8 +42,13 @@ export default function PrefetchBalancesWrapper({
|
|||||||
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
|
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
|
||||||
const fetchBalances = useCallback(() => {
|
const fetchBalances = useCallback(() => {
|
||||||
if (account) {
|
if (account) {
|
||||||
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
|
// Backend takes <2sec to get the updated portfolio value after a transaction
|
||||||
setHasUnfetchedBalances(false)
|
// 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])
|
}, [account, prefetchPortfolioBalances, setHasUnfetchedBalances])
|
||||||
|
|
||||||
@ -62,12 +69,18 @@ export default function PrefetchBalancesWrapper({
|
|||||||
}
|
}
|
||||||
}, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances])
|
}, [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(() => {
|
const onHover = useCallback(() => {
|
||||||
if (hasUnfetchedBalances) fetchBalances()
|
if (hasUnfetchedBalances) fetchBalances()
|
||||||
}, [fetchBalances, hasUnfetchedBalances])
|
}, [fetchBalances, hasUnfetchedBalances])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseEnter={onHover} className={className}>
|
<div onMouseEnter={shouldFetchOnHover ? onHover : undefined} className={className}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,6 @@ import { ChainId, Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
|||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { Trace } from 'analytics'
|
import { Trace } from 'analytics'
|
||||||
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||||
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
|
||||||
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
|
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
|
||||||
import useDebounce from 'hooks/useDebounce'
|
import useDebounce from 'hooks/useDebounce'
|
||||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||||
@ -101,7 +100,7 @@ export function CurrencySearch({
|
|||||||
}, [chainId, data?.portfolios])
|
}, [chainId, data?.portfolios])
|
||||||
|
|
||||||
const sortedTokens: Token[] = useMemo(() => {
|
const sortedTokens: Token[] = useMemo(() => {
|
||||||
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
|
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances
|
||||||
const portfolioTokens = splitHiddenTokens(portfolioTokenBalances ?? [])
|
const portfolioTokens = splitHiddenTokens(portfolioTokenBalances ?? [])
|
||||||
.visibleTokens.map((tokenBalance) => {
|
.visibleTokens.map((tokenBalance) => {
|
||||||
if (!tokenBalance?.token?.chain || !tokenBalance.token?.address || !tokenBalance.token?.decimals) {
|
if (!tokenBalance?.token?.chain || !tokenBalance.token?.address || !tokenBalance.token?.decimals) {
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
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 { useWeb3React } from '@web3-react/core'
|
||||||
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
||||||
import { getChainInfo } from 'constants/chainInfo'
|
import { getChainInfo } from 'constants/chainInfo'
|
||||||
import { asSupportedChain } from 'constants/chains'
|
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 { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||||
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
||||||
import { useMemo } from 'react'
|
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 { ThemedText } from 'theme/components'
|
||||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||||
|
|
||||||
const BalancesCard = styled.div`
|
import { MultiChainMap } from '.'
|
||||||
border-radius: 16px;
|
|
||||||
|
const BalancesCard = styled.div<{ isInfoTDPEnabled?: boolean }>`
|
||||||
color: ${({ theme }) => theme.neutral1};
|
color: ${({ theme }) => theme.neutral1};
|
||||||
display: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
padding: 16px;
|
${({ isInfoTDPEnabled }) => !isInfoTDPEnabled && 'padding: 16px;'}
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
// 768 hardcoded to match NFT-redesign navbar breakpoints
|
// 768 hardcoded to match NFT-redesign navbar breakpoints
|
||||||
@ -48,11 +56,13 @@ const BalanceContainer = styled.div`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
const BalanceAmountsContainer = styled.div`
|
const BalanceAmountsContainer = styled.div<{ isInfoTDPEnabled?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'margin-left: 12px;'}
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledNetworkLabel = styled.div`
|
const StyledNetworkLabel = styled.div`
|
||||||
@ -61,49 +71,187 @@ const StyledNetworkLabel = styled.div`
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function BalanceSummary({ token }: { token: Currency }) {
|
interface BalanceProps {
|
||||||
const { account, chainId } = useWeb3React()
|
currency?: Currency
|
||||||
const theme = useTheme()
|
chainId?: ChainId
|
||||||
const { label, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
|
balance?: CurrencyAmount<Currency> // TODO(WEB-3026): only used for pre-Info-project calculations, should remove after project goes live
|
||||||
const balance = useCurrencyBalance(account, token)
|
gqlBalance?: PortfolioTokenBalancePartsFragment
|
||||||
const { formatCurrencyAmount } = useFormatter()
|
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({
|
const formattedBalance = formatCurrencyAmount({
|
||||||
amount: balance,
|
amount: balance,
|
||||||
type: NumberType.TokenNonTx,
|
type: NumberType.TokenNonTx,
|
||||||
})
|
})
|
||||||
const formattedUsdValue = formatCurrencyAmount({
|
const formattedUsdValue = formatCurrencyAmount({
|
||||||
amount: useStablecoinValue(balance),
|
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 null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BalancesCard>
|
<BalancesCard isInfoTDPEnabled={isInfoTDPEnabled}>
|
||||||
<BalanceSection>
|
{!isInfoTDPEnabled && <ConnectedChainBalanceSummary connectedChainBalance={connectedChainBalance} />}
|
||||||
<ThemedText.SubHeaderSmall color={theme.neutral1}>
|
{isInfoTDPEnabled && (
|
||||||
<Trans>Your balance on {label}</Trans>
|
<>
|
||||||
</ThemedText.SubHeaderSmall>
|
<PageChainBalanceSummary pageChainBalance={pageChainBalance} />
|
||||||
<BalanceRow>
|
<OtherChainsBalanceSummary otherChainBalances={otherChainBalances} hasPageChainBalance={!!pageChainBalance} />
|
||||||
<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>
|
</BalancesCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,20 @@ import { Trans } from '@lingui/macro'
|
|||||||
import { Currency } from '@uniswap/sdk-core'
|
import { Currency } from '@uniswap/sdk-core'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
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 { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
|
||||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||||
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
||||||
import styled from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
import { StyledInternalLink } from 'theme/components'
|
import { StyledInternalLink, ThemedText } from 'theme/components'
|
||||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div<{ isInfoTDPEnabled?: boolean }>`
|
||||||
align-content: center;
|
align-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid ${({ theme }) => theme.surface3};
|
|
||||||
border-bottom: none;
|
|
||||||
background-color: ${({ theme }) => theme.surface1};
|
background-color: ${({ theme }) => theme.surface1};
|
||||||
border-radius: 20px 20px 0px 0px;
|
border: 1px solid ${({ theme }) => theme.surface3};
|
||||||
bottom: 52px;
|
|
||||||
color: ${({ theme }) => theme.neutral2};
|
color: ${({ theme }) => theme.neutral2};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -26,9 +25,24 @@ const Wrapper = styled.div`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
left: 0;
|
left: 0;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
padding: 12px 16px;
|
|
||||||
position: fixed;
|
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) {
|
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
@ -37,27 +51,29 @@ const Wrapper = styled.div`
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const BalanceValue = styled.div`
|
const BalanceValue = styled.div<{ isInfoTDPEnabled?: boolean }>`
|
||||||
color: ${({ theme }) => theme.neutral1};
|
color: ${({ theme }) => theme.neutral1};
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 28px;
|
line-height: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '20px' : '28px')};
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
`
|
`
|
||||||
const Balance = styled.div`
|
const Balance = styled.div<{ isInfoTDPEnabled?: boolean }>`
|
||||||
align-items: center;
|
align-items: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? 'flex-end' : 'center')};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
`
|
`
|
||||||
const BalanceInfo = styled.div`
|
const BalanceInfo = styled.div<{ isInfoTDPEnabled?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 10 1 auto;
|
flex: 10 1 auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
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;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
|
||||||
@ -65,15 +81,15 @@ const FiatValue = styled.span`
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const SwapButton = styled(StyledInternalLink)`
|
const SwapButton = styled(StyledInternalLink)<{ isInfoTDPEnabled?: boolean }>`
|
||||||
background-color: ${({ theme }) => theme.accent1};
|
background-color: ${({ theme }) => theme.accent1};
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '22px' : '12px')};
|
||||||
color: ${({ theme }) => theme.deprecated_accentTextLightPrimary};
|
color: ${({ theme }) => theme.deprecated_accentTextLightPrimary};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
font-size: 1em;
|
font-size: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '16px' : '1em')};
|
||||||
font-weight: 535;
|
font-weight: 535;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -81,10 +97,18 @@ const SwapButton = styled(StyledInternalLink)`
|
|||||||
max-width: 100vw;
|
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 { account } = useWeb3React()
|
||||||
const balance = useCurrencyBalance(account, token)
|
const balance = useCurrencyBalance(account, currency)
|
||||||
const { formatCurrencyAmount } = useFormatter()
|
const { formatCurrencyAmount, formatNumber } = useFormatter()
|
||||||
const formattedBalance = formatCurrencyAmount({
|
const formattedBalance = formatCurrencyAmount({
|
||||||
amount: balance,
|
amount: balance,
|
||||||
type: NumberType.TokenNonTx,
|
type: NumberType.TokenNonTx,
|
||||||
@ -93,22 +117,35 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
|
|||||||
amount: useStablecoinValue(balance),
|
amount: useStablecoinValue(balance),
|
||||||
type: NumberType.FiatTokenStats,
|
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 (
|
return (
|
||||||
<Wrapper>
|
<Wrapper isInfoTDPEnabled={isInfoTDPEnabled}>
|
||||||
{Boolean(account && balance) && (
|
{Boolean(account && (isInfoTDPEnabled ? pageChainBalance : balance)) && (
|
||||||
<BalanceInfo>
|
<BalanceInfo isInfoTDPEnabled={isInfoTDPEnabled}>
|
||||||
<Trans>Your {token.symbol} balance</Trans>
|
{isInfoTDPEnabled ? <Trans>Your balance</Trans> : <Trans>Your {currency.symbol} balance</Trans>}
|
||||||
<Balance>
|
<Balance isInfoTDPEnabled={isInfoTDPEnabled}>
|
||||||
<BalanceValue>
|
<BalanceValue isInfoTDPEnabled={isInfoTDPEnabled}>
|
||||||
{formattedBalance} {token.symbol}
|
{isInfoTDPEnabled ? formattedGqlBalance : formattedBalance} {currency.symbol}
|
||||||
</BalanceValue>
|
</BalanceValue>
|
||||||
<FiatValue>{formattedUsdValue}</FiatValue>
|
<FiatValue isInfoTDPEnabled={isInfoTDPEnabled}>
|
||||||
|
{isInfoTDPEnabled ? `(${formattedUsdGqlValue})` : formattedUsdValue}
|
||||||
|
</FiatValue>
|
||||||
</Balance>
|
</Balance>
|
||||||
</BalanceInfo>
|
</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>
|
<Trans>Swap</Trans>
|
||||||
</SwapButton>
|
</SwapButton>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
@ -3,12 +3,11 @@ import { InterfacePageName } from '@uniswap/analytics-events'
|
|||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { Trace } from 'analytics'
|
import { Trace } from 'analytics'
|
||||||
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
||||||
|
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||||
import { AboutSection } from 'components/Tokens/TokenDetails/About'
|
import { AboutSection } from 'components/Tokens/TokenDetails/About'
|
||||||
import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
|
import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
|
||||||
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
|
|
||||||
import { BreadcrumbNav, BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
|
import { BreadcrumbNav, BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
|
||||||
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
|
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
|
||||||
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
|
|
||||||
import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
|
import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
|
||||||
import TokenDetailsSkeleton, {
|
import TokenDetailsSkeleton, {
|
||||||
Hr,
|
Hr,
|
||||||
@ -25,8 +24,13 @@ import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
|||||||
import { checkWarning } from 'constants/tokenSafety'
|
import { checkWarning } from 'constants/tokenSafety'
|
||||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
|
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
|
||||||
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
import {
|
||||||
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
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 { getTokenDetailsURL, gqlToCurrency, InterfaceGqlChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
|
||||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||||
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
|
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
|
||||||
@ -41,7 +45,9 @@ import { CopyContractAddress } from 'theme/components'
|
|||||||
import { isAddress, shortenAddress } from 'utils'
|
import { isAddress, shortenAddress } from 'utils'
|
||||||
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
|
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
|
||||||
|
|
||||||
|
import BalanceSummary from './BalanceSummary'
|
||||||
import InvalidTokenDetails from './InvalidTokenDetails'
|
import InvalidTokenDetails from './InvalidTokenDetails'
|
||||||
|
import MobileBalanceSummaryFooter from './MobileBalanceSummaryFooter'
|
||||||
import { TokenDescription } from './TokenDescription'
|
import { TokenDescription } from './TokenDescription'
|
||||||
|
|
||||||
const TokenSymbol = styled.span`
|
const TokenSymbol = styled.span`
|
||||||
@ -94,7 +100,7 @@ function useRelevantToken(
|
|||||||
[onChainToken, queryToken]
|
[onChainToken, queryToken]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export type MultiChainMap = { [chain: string]: { address?: string; balance?: PortfolioTokenBalancePartsFragment } }
|
||||||
type TokenDetailsProps = {
|
type TokenDetailsProps = {
|
||||||
urlAddress?: string
|
urlAddress?: string
|
||||||
inputTokenAddress?: string
|
inputTokenAddress?: string
|
||||||
@ -117,17 +123,25 @@ export default function TokenDetails({
|
|||||||
[urlAddress]
|
[urlAddress]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { chainId: connectedChainId } = useWeb3React()
|
const { account, chainId: connectedChainId } = useWeb3React()
|
||||||
const pageChainId = supportedChainIdFromGQLChain(chain)
|
const pageChainId = supportedChainIdFromGQLChain(chain)
|
||||||
const tokenQueryData = tokenQuery.token
|
const tokenQueryData = tokenQuery.token
|
||||||
const crossChainMap = useMemo(
|
const { data: balanceQuery } = useCachedPortfolioBalancesQuery({ account })
|
||||||
() =>
|
const multiChainMap = useMemo(() => {
|
||||||
tokenQueryData?.project?.tokens.reduce((map, current) => {
|
const tokenBalances = balanceQuery?.portfolios?.[0].tokenBalances
|
||||||
if (current) map[current.chain] = current.address
|
const tokensAcrossChains = tokenQueryData?.project?.tokens
|
||||||
return map
|
if (!tokensAcrossChains) return {}
|
||||||
}, {} as { [key: string]: string | undefined }) ?? {},
|
return tokensAcrossChains.reduce((map, current) => {
|
||||||
[tokenQueryData]
|
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)
|
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
|
||||||
|
|
||||||
@ -143,7 +157,7 @@ export default function TokenDetails({
|
|||||||
const navigateToTokenForChain = useCallback(
|
const navigateToTokenForChain = useCallback(
|
||||||
(update: Chain) => {
|
(update: Chain) => {
|
||||||
if (!address) return
|
if (!address) return
|
||||||
const bridgedAddress = crossChainMap[update]
|
const bridgedAddress = multiChainMap[update].address
|
||||||
if (bridgedAddress) {
|
if (bridgedAddress) {
|
||||||
startTokenTransition(() =>
|
startTokenTransition(() =>
|
||||||
navigate(
|
navigate(
|
||||||
@ -158,7 +172,7 @@ export default function TokenDetails({
|
|||||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled })))
|
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled })))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[address, crossChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
|
[address, multiChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
|
||||||
)
|
)
|
||||||
useOnGlobalChainSwitch(navigateToTokenForChain)
|
useOnGlobalChainSwitch(navigateToTokenForChain)
|
||||||
|
|
||||||
@ -283,7 +297,7 @@ export default function TokenDetails({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
|
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
|
||||||
{!isInfoTDPEnabled && detailedToken && <BalanceSummary token={detailedToken} />}
|
{detailedToken && <BalanceSummary currency={detailedToken} chain={chain} multiChainMap={multiChainMap} />}
|
||||||
{isInfoTDPEnabled && (
|
{isInfoTDPEnabled && (
|
||||||
<TokenDescription
|
<TokenDescription
|
||||||
tokenAddress={address}
|
tokenAddress={address}
|
||||||
@ -293,7 +307,9 @@ export default function TokenDetails({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RightPanel>
|
</RightPanel>
|
||||||
{!isInfoTDPEnabled && detailedToken && <MobileBalanceSummaryFooter token={detailedToken} />}
|
{detailedToken && (
|
||||||
|
<MobileBalanceSummaryFooter currency={detailedToken} pageChainBalance={multiChainMap[chain].balance} />
|
||||||
|
)}
|
||||||
|
|
||||||
<TokenSafetyModal
|
<TokenSafetyModal
|
||||||
isOpen={openTokenSafetyModal || !!continueSwap}
|
isOpen={openTokenSafetyModal || !!continueSwap}
|
||||||
|
@ -97,7 +97,4 @@ gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
|
||||||
|
|
||||||
export type TokenQueryData = TokenQuery['token']
|
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: {
|
defaultOptions: {
|
||||||
|
@ -1,6 +1,39 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
import { PortfolioTokenBalancePartsFragment } from './__generated__/types-and-hooks'
|
||||||
|
|
||||||
gql`
|
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!]!) {
|
query PortfolioBalances($ownerAddress: String!, $chains: [Chain!]!) {
|
||||||
portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
|
portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
|
||||||
id
|
id
|
||||||
@ -19,35 +52,10 @@ gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokenBalances {
|
tokenBalances {
|
||||||
id
|
...PortfolioTokenBalanceParts
|
||||||
quantity
|
|
||||||
denominatedValue {
|
|
||||||
id
|
|
||||||
currency
|
|
||||||
value
|
|
||||||
}
|
|
||||||
tokenProjectMarket {
|
|
||||||
id
|
|
||||||
pricePercentChange(duration: DAY) {
|
|
||||||
id
|
|
||||||
value
|
|
||||||
}
|
|
||||||
tokenProject {
|
|
||||||
id
|
|
||||||
logoUrl
|
|
||||||
isSpam
|
|
||||||
}
|
|
||||||
}
|
|
||||||
token {
|
|
||||||
id
|
|
||||||
chain
|
|
||||||
address
|
|
||||||
name
|
|
||||||
symbol
|
|
||||||
standard
|
|
||||||
decimals
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export type PortfolioToken = NonNullable<PortfolioTokenBalancePartsFragment['token']>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useWeb3React } from '@web3-react/core'
|
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 { chainIdToBackendName } from 'graphql/data/util'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
@ -108,6 +108,10 @@ export function useTokenBalance(account?: string, token?: Token): CurrencyAmount
|
|||||||
return tokenBalances[token.address]
|
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(
|
export function useCurrencyBalances(
|
||||||
account?: string,
|
account?: string,
|
||||||
currencies?: (Currency | undefined)[]
|
currencies?: (Currency | undefined)[]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||||
import TokenDetails from 'components/Tokens/TokenDetails'
|
import TokenDetails from 'components/Tokens/TokenDetails'
|
||||||
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
|
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
|
||||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||||
@ -7,10 +8,15 @@ import useParsedQueryString from 'hooks/useParsedQueryString'
|
|||||||
import { atomWithStorage, useAtomValue } from 'jotai/utils'
|
import { atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||||
|
|
||||||
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
|
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
|
||||||
|
|
||||||
|
const StyledPrefetchBalancesWrapper = styled(PrefetchBalancesWrapper)`
|
||||||
|
display: contents;
|
||||||
|
`
|
||||||
|
|
||||||
export default function TokenDetailsPage() {
|
export default function TokenDetailsPage() {
|
||||||
const { tokenAddress, chainName } = useParams<{
|
const { tokenAddress, chainName } = useParams<{
|
||||||
tokenAddress: string
|
tokenAddress: string
|
||||||
@ -58,12 +64,14 @@ export default function TokenDetailsPage() {
|
|||||||
if (!tokenQuery) return <TokenDetailsPageSkeleton />
|
if (!tokenQuery) return <TokenDetailsPageSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TokenDetails
|
<StyledPrefetchBalancesWrapper shouldFetchOnAccountUpdate={true} shouldFetchOnHover={false}>
|
||||||
urlAddress={tokenAddress}
|
<TokenDetails
|
||||||
chain={chain}
|
urlAddress={tokenAddress}
|
||||||
tokenQuery={tokenQuery}
|
chain={chain}
|
||||||
tokenPriceQuery={currentPriceQuery}
|
tokenQuery={tokenQuery}
|
||||||
inputTokenAddress={parsedInputTokenAddress}
|
tokenPriceQuery={currentPriceQuery}
|
||||||
/>
|
inputTokenAddress={parsedInputTokenAddress}
|
||||||
|
/>
|
||||||
|
</StyledPrefetchBalancesWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ChainId, Currency } from '@uniswap/sdk-core'
|
import { ChainId, Currency } from '@uniswap/sdk-core'
|
||||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||||
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
import { Chain } from 'graphql/data/Token'
|
|
||||||
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
|
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
|
||||||
|
|
||||||
export type CurrencyKey = string
|
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
|
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
|
||||||
|
|
||||||
export function splitHiddenTokens(
|
export function splitHiddenTokens(
|
||||||
tokenBalances: TokenBalance[],
|
tokenBalances: readonly PortfolioTokenBalancePartsFragment[],
|
||||||
options: {
|
options: {
|
||||||
hideSmallBalances?: boolean
|
hideSmallBalances?: boolean
|
||||||
} = { hideSmallBalances: true }
|
} = { hideSmallBalances: true }
|
||||||
) {
|
) {
|
||||||
const visibleTokens: TokenBalance[] = []
|
const visibleTokens: PortfolioTokenBalancePartsFragment[] = []
|
||||||
const hiddenTokens: TokenBalance[] = []
|
const hiddenTokens: PortfolioTokenBalancePartsFragment[] = []
|
||||||
|
|
||||||
for (const tokenBalance of tokenBalances) {
|
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)
|
// 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 }
|
return { visibleTokens, hiddenTokens }
|
||||||
}
|
}
|
||||||
|
|
||||||
function meetsThreshold(tokenBalance: TokenBalance) {
|
function meetsThreshold(tokenBalance: PortfolioTokenBalancePartsFragment) {
|
||||||
const value = tokenBalance.denominatedValue?.value ?? 0
|
const value = tokenBalance.denominatedValue?.value ?? 0
|
||||||
return value > HIDE_SMALL_USD_BALANCES_THRESHOLD
|
return value > HIDE_SMALL_USD_BALANCES_THRESHOLD
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user