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;
|
||||
`
|
||||
|
||||
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) {
|
||||
// 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 (!account || !balance) {
|
||||
return null
|
||||
}
|
||||
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 (
|
||||
<BalancesCard>
|
||||
<BalanceSection>
|
||||
<ThemedText.SubHeaderSmall color={theme.neutral1}>
|
||||
<Trans>Your balance on {label}</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<BalanceRow>
|
||||
<PortfolioLogo currencies={currencies} chainId={token.chainId} size="2rem" />
|
||||
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
|
||||
<BalanceContainer>
|
||||
<BalanceAmountsContainer>
|
||||
<BalanceItem>
|
||||
<ThemedText.SubHeader>
|
||||
{formattedBalance} {token.symbol}
|
||||
{formattedBalance} {currency?.symbol}
|
||||
</ThemedText.SubHeader>
|
||||
</BalanceItem>
|
||||
<BalanceItem>
|
||||
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
|
||||
</BalanceItem>
|
||||
</BalanceAmountsContainer>
|
||||
<StyledNetworkLabel color={color}>{label}</StyledNetworkLabel>
|
||||
<StyledNetworkLabel color={color}>{chainName}</StyledNetworkLabel>
|
||||
</BalanceContainer>
|
||||
</BalanceRow>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
${({ 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
|
||||
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 { [key: string]: string | undefined }) ?? {},
|
||||
[tokenQueryData]
|
||||
)
|
||||
}, {} 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,6 +64,7 @@ export default function TokenDetailsPage() {
|
||||
if (!tokenQuery) return <TokenDetailsPageSkeleton />
|
||||
|
||||
return (
|
||||
<StyledPrefetchBalancesWrapper shouldFetchOnAccountUpdate={true} shouldFetchOnHover={false}>
|
||||
<TokenDetails
|
||||
urlAddress={tokenAddress}
|
||||
chain={chain}
|
||||
@ -65,5 +72,6 @@ export default function TokenDetailsPage() {
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user