Skip to content
This repository has been archived by the owner on Jan 6, 2024. It is now read-only.

Commit

Permalink
feat: [info] add multi-chain balances on TDP (Uniswap#7493)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
kristiehuang and cartcrom authored Nov 27, 2023
1 parent 4a5a41c commit fc7ecc7
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 146 deletions.
4 changes: 2 additions & 2 deletions src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
14 changes: 9 additions & 5 deletions src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 }),
Expand Down Expand Up @@ -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

Expand Down
21 changes: 17 additions & 4 deletions src/components/PrefetchBalancesWrapper/PrefetchBalancesWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -31,17 +32,23 @@ 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()

// Use an atom to track unfetched state to avoid duplicating fetches if this component appears multiple times on the page.
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])

Expand All @@ -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>
)
Expand Down
3 changes: 1 addition & 2 deletions src/components/SearchModal/CurrencySearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
224 changes: 186 additions & 38 deletions src/components/Tokens/TokenDetails/BalanceSummary.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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`
Expand All @@ -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,
})

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>
)
}
}

const currencies = useMemo(() => [token], [token])
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>
)
}

if (!account || !balance) {
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>
)
}
Loading

0 comments on commit fc7ecc7

Please sign in to comment.