Skip to content

Commit fc7ecc7

Browse files
kristiehuangcartcrom
andauthoredNov 27, 2023
feat: [info] add multi-chain balances on TDP (Uniswap#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>
1 parent 4a5a41c commit fc7ecc7

File tree

15 files changed

+395
-146
lines changed

15 files changed

+395
-146
lines changed
 

‎src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>`
110110
margin-top: 1px;
111111
`
112112

113-
function calculcateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
113+
function calculateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
114114
if (!price0 || !price1) return undefined
115115

116116
const value0 = parseFloat(position.amount0.toExact()) * price0
@@ -124,7 +124,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
124124
const { chainId, position, pool, details, inRange, closed } = positionInfo
125125

126126
const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
127-
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
127+
const liquidityValue = calculateLiquidityValue(priceA, priceB, position)
128128

129129
const navigate = useNavigate()
130130
const toggleWalletDrawer = useToggleAccountDrawer()

‎src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrap
44
import Row from 'components/Row'
55
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
66
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
7-
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
7+
import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
8+
import { PortfolioToken } from 'graphql/data/portfolios'
89
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
910
import { useAtomValue } from 'jotai/utils'
1011
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
@@ -28,7 +29,7 @@ export default function Tokens({ account }: { account: string }) {
2829

2930
const { data } = useCachedPortfolioBalancesQuery({ account })
3031

31-
const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
32+
const tokenBalances = data?.portfolios?.[0].tokenBalances
3233

3334
const { visibleTokens, hiddenTokens } = useMemo(
3435
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
@@ -69,9 +70,12 @@ const TokenNameText = styled(ThemedText.SubHeader)`
6970
${EllipsisStyle}
7071
`
7172

72-
type PortfolioToken = NonNullable<TokenBalance['token']>
73-
74-
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
73+
function TokenRow({
74+
token,
75+
quantity,
76+
denominatedValue,
77+
tokenProjectMarket,
78+
}: PortfolioTokenBalancePartsFragment & { token: PortfolioToken }) {
7579
const { formatDelta } = useFormatter()
7680
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
7781

‎src/components/PrefetchBalancesWrapper/PrefetchBalancesWrapper.tsx

+17-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphq
33
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
44
import usePrevious from 'hooks/usePrevious'
55
import { atom, useAtom } from 'jotai'
6+
import ms from 'ms'
67
import { PropsWithChildren, useCallback, useEffect } from 'react'
78

89
import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks'
@@ -31,17 +32,23 @@ const hasUnfetchedBalancesAtom = atom<boolean>(true)
3132
export default function PrefetchBalancesWrapper({
3233
children,
3334
shouldFetchOnAccountUpdate,
35+
shouldFetchOnHover = true,
3436
className,
35-
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; className?: string }>) {
37+
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; shouldFetchOnHover?: boolean; className?: string }>) {
3638
const { account } = useWeb3React()
3739
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
3840

3941
// Use an atom to track unfetched state to avoid duplicating fetches if this component appears multiple times on the page.
4042
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
4143
const fetchBalances = useCallback(() => {
4244
if (account) {
43-
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
44-
setHasUnfetchedBalances(false)
45+
// Backend takes <2sec to get the updated portfolio value after a transaction
46+
// This timeout is an interim solution while we're working on a websocket that'll ping the client when connected account gets changes
47+
// TODO(WEB-3131): remove this timeout after websocket is implemented
48+
setTimeout(() => {
49+
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
50+
setHasUnfetchedBalances(false)
51+
}, ms('3.5s'))
4552
}
4653
}, [account, prefetchPortfolioBalances, setHasUnfetchedBalances])
4754

@@ -62,12 +69,18 @@ export default function PrefetchBalancesWrapper({
6269
}
6370
}, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances])
6471

72+
// Temporary workaround to fix balances on TDP - this fetches balances if shouldFetchOnAccountUpdate becomes true while hasUnfetchedBalances is true
73+
// TODO(WEB-3071) remove this logic once balance provider refactor is done
74+
useEffect(() => {
75+
if (hasUnfetchedBalances && shouldFetchOnAccountUpdate) fetchBalances()
76+
}, [fetchBalances, hasUnfetchedBalances, shouldFetchOnAccountUpdate])
77+
6578
const onHover = useCallback(() => {
6679
if (hasUnfetchedBalances) fetchBalances()
6780
}, [fetchBalances, hasUnfetchedBalances])
6881

6982
return (
70-
<div onMouseEnter={onHover} className={className}>
83+
<div onMouseEnter={shouldFetchOnHover ? onHover : undefined} className={className}>
7184
{children}
7285
</div>
7386
)

‎src/components/SearchModal/CurrencySearch.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ChainId, Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
55
import { useWeb3React } from '@web3-react/core'
66
import { Trace } from 'analytics'
77
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
8-
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
98
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
109
import useDebounce from 'hooks/useDebounce'
1110
import { useOnClickOutside } from 'hooks/useOnClickOutside'
@@ -101,7 +100,7 @@ export function CurrencySearch({
101100
}, [chainId, data?.portfolios])
102101

103102
const sortedTokens: Token[] = useMemo(() => {
104-
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
103+
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances
105104
const portfolioTokens = splitHiddenTokens(portfolioTokenBalances ?? [])
106105
.visibleTokens.map((tokenBalance) => {
107106
if (!tokenBalance?.token?.chain || !tokenBalance.token?.address || !tokenBalance.token?.decimals) {
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
import { Trans } from '@lingui/macro'
2-
import { ChainId, Currency } from '@uniswap/sdk-core'
2+
import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core'
33
import { useWeb3React } from '@web3-react/core'
44
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
55
import { getChainInfo } from 'constants/chainInfo'
66
import { asSupportedChain } from 'constants/chains'
7+
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
8+
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
9+
import { Chain, PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
10+
import { getTokenDetailsURL, gqlToCurrency, supportedChainIdFromGQLChain } from 'graphql/data/util'
711
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
812
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
913
import { useMemo } from 'react'
10-
import styled, { useTheme } from 'styled-components'
14+
import { useNavigate } from 'react-router-dom'
15+
import styled from 'styled-components'
1116
import { ThemedText } from 'theme/components'
1217
import { NumberType, useFormatter } from 'utils/formatNumbers'
1318

14-
const BalancesCard = styled.div`
15-
border-radius: 16px;
19+
import { MultiChainMap } from '.'
20+
21+
const BalancesCard = styled.div<{ isInfoTDPEnabled?: boolean }>`
1622
color: ${({ theme }) => theme.neutral1};
17-
display: none;
23+
display: flex;
24+
flex-direction: column;
25+
gap: 24px;
1826
height: fit-content;
19-
padding: 16px;
27+
${({ isInfoTDPEnabled }) => !isInfoTDPEnabled && 'padding: 16px;'}
2028
width: 100%;
2129
2230
// 768 hardcoded to match NFT-redesign navbar breakpoints
@@ -48,11 +56,13 @@ const BalanceContainer = styled.div`
4856
flex: 1;
4957
`
5058

51-
const BalanceAmountsContainer = styled.div`
59+
const BalanceAmountsContainer = styled.div<{ isInfoTDPEnabled?: boolean }>`
5260
display: flex;
5361
flex-direction: row;
5462
justify-content: space-between;
5563
align-items: center;
64+
width: 100%;
65+
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'margin-left: 12px;'}
5666
`
5767

5868
const StyledNetworkLabel = styled.div`
@@ -61,49 +71,187 @@ const StyledNetworkLabel = styled.div`
6171
line-height: 16px;
6272
`
6373

64-
export default function BalanceSummary({ token }: { token: Currency }) {
65-
const { account, chainId } = useWeb3React()
66-
const theme = useTheme()
67-
const { label, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
68-
const balance = useCurrencyBalance(account, token)
69-
const { formatCurrencyAmount } = useFormatter()
74+
interface BalanceProps {
75+
currency?: Currency
76+
chainId?: ChainId
77+
balance?: CurrencyAmount<Currency> // TODO(WEB-3026): only used for pre-Info-project calculations, should remove after project goes live
78+
gqlBalance?: PortfolioTokenBalancePartsFragment
79+
onClick?: () => void
80+
}
81+
const Balance = ({ currency, chainId = ChainId.MAINNET, balance, gqlBalance, onClick }: BalanceProps) => {
82+
const { formatCurrencyAmount, formatNumber } = useFormatter()
83+
const { label: chainName, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
84+
const currencies = useMemo(() => [currency], [currency])
85+
const isInfoTDPEnabled = useInfoExplorePageEnabled()
86+
7087
const formattedBalance = formatCurrencyAmount({
7188
amount: balance,
7289
type: NumberType.TokenNonTx,
7390
})
7491
const formattedUsdValue = formatCurrencyAmount({
7592
amount: useStablecoinValue(balance),
76-
type: NumberType.FiatTokenStats,
93+
type: NumberType.PortfolioBalance,
94+
})
95+
const formattedGqlBalance = formatNumber({
96+
input: gqlBalance?.quantity,
97+
type: NumberType.TokenNonTx,
7798
})
99+
const formattedUsdGqlValue = formatNumber({
100+
input: gqlBalance?.denominatedValue?.value,
101+
type: NumberType.PortfolioBalance,
102+
})
103+
104+
if (isInfoTDPEnabled) {
105+
return (
106+
<BalanceRow onClick={onClick}>
107+
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
108+
<BalanceAmountsContainer isInfoTDPEnabled>
109+
<BalanceItem>
110+
<ThemedText.BodyPrimary>{formattedUsdGqlValue}</ThemedText.BodyPrimary>
111+
</BalanceItem>
112+
<BalanceItem>
113+
<ThemedText.BodySecondary>{formattedGqlBalance}</ThemedText.BodySecondary>
114+
</BalanceItem>
115+
</BalanceAmountsContainer>
116+
</BalanceRow>
117+
)
118+
} else {
119+
return (
120+
<BalanceRow>
121+
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
122+
<BalanceContainer>
123+
<BalanceAmountsContainer>
124+
<BalanceItem>
125+
<ThemedText.SubHeader>
126+
{formattedBalance} {currency?.symbol}
127+
</ThemedText.SubHeader>
128+
</BalanceItem>
129+
<BalanceItem>
130+
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
131+
</BalanceItem>
132+
</BalanceAmountsContainer>
133+
<StyledNetworkLabel color={color}>{chainName}</StyledNetworkLabel>
134+
</BalanceContainer>
135+
</BalanceRow>
136+
)
137+
}
138+
}
78139

79-
const currencies = useMemo(() => [token], [token])
140+
const ConnectedChainBalanceSummary = ({
141+
connectedChainBalance,
142+
}: {
143+
connectedChainBalance?: CurrencyAmount<Currency>
144+
}) => {
145+
const { chainId: connectedChainId } = useWeb3React()
146+
if (!connectedChainId || !connectedChainBalance || !connectedChainBalance.greaterThan(0)) return null
147+
const token = connectedChainBalance.currency
148+
const { label: chainName } = getChainInfo(asSupportedChain(connectedChainId) ?? ChainId.MAINNET)
149+
return (
150+
<BalanceSection>
151+
<ThemedText.SubHeaderSmall color="neutral1">
152+
<Trans>Your balance on {chainName}</Trans>
153+
</ThemedText.SubHeaderSmall>
154+
<Balance currency={token} chainId={connectedChainId} balance={connectedChainBalance} />
155+
</BalanceSection>
156+
)
157+
}
80158

81-
if (!account || !balance) {
159+
const PageChainBalanceSummary = ({ pageChainBalance }: { pageChainBalance?: PortfolioTokenBalancePartsFragment }) => {
160+
if (!pageChainBalance || !pageChainBalance.token) return null
161+
const currency = gqlToCurrency(pageChainBalance.token)
162+
return (
163+
<BalanceSection>
164+
<ThemedText.HeadlineSmall color="neutral1">
165+
<Trans>Your balance</Trans>
166+
</ThemedText.HeadlineSmall>
167+
<Balance currency={currency} chainId={currency?.chainId} gqlBalance={pageChainBalance} />
168+
</BalanceSection>
169+
)
170+
}
171+
172+
const OtherChainsBalanceSummary = ({
173+
otherChainBalances,
174+
hasPageChainBalance,
175+
}: {
176+
otherChainBalances: readonly PortfolioTokenBalancePartsFragment[]
177+
hasPageChainBalance: boolean
178+
}) => {
179+
const navigate = useNavigate()
180+
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
181+
182+
if (!otherChainBalances.length) return null
183+
return (
184+
<BalanceSection>
185+
{hasPageChainBalance ? (
186+
<ThemedText.SubHeaderSmall>
187+
<Trans>On other networks</Trans>
188+
</ThemedText.SubHeaderSmall>
189+
) : (
190+
<ThemedText.HeadlineSmall>
191+
<Trans>Balance on other networks</Trans>
192+
</ThemedText.HeadlineSmall>
193+
)}
194+
{otherChainBalances.map((balance) => {
195+
const currency = balance.token && gqlToCurrency(balance.token)
196+
const chainId = (balance.token && supportedChainIdFromGQLChain(balance.token.chain)) ?? ChainId.MAINNET
197+
return (
198+
<Balance
199+
key={balance.id}
200+
currency={currency}
201+
chainId={chainId}
202+
gqlBalance={balance}
203+
onClick={() =>
204+
navigate(
205+
getTokenDetailsURL({
206+
address: balance.token?.address,
207+
chain: balance.token?.chain ?? Chain.Ethereum,
208+
isInfoExplorePageEnabled,
209+
})
210+
)
211+
}
212+
/>
213+
)
214+
})}
215+
</BalanceSection>
216+
)
217+
}
218+
219+
export default function BalanceSummary({
220+
currency,
221+
chain,
222+
multiChainMap,
223+
}: {
224+
currency: Currency
225+
chain: Chain
226+
multiChainMap: MultiChainMap
227+
}) {
228+
const { account } = useWeb3React()
229+
230+
const isInfoTDPEnabled = useInfoTDPEnabled()
231+
232+
const connectedChainBalance = useCurrencyBalance(account, currency)
233+
234+
const pageChainBalance = multiChainMap[chain].balance
235+
const otherChainBalances: PortfolioTokenBalancePartsFragment[] = []
236+
for (const [key, value] of Object.entries(multiChainMap)) {
237+
if (key !== chain && value.balance !== undefined) {
238+
otherChainBalances.push(value.balance)
239+
}
240+
}
241+
const hasBalances = pageChainBalance || Boolean(otherChainBalances.length)
242+
243+
if (!account || !hasBalances) {
82244
return null
83245
}
84246
return (
85-
<BalancesCard>
86-
<BalanceSection>
87-
<ThemedText.SubHeaderSmall color={theme.neutral1}>
88-
<Trans>Your balance on {label}</Trans>
89-
</ThemedText.SubHeaderSmall>
90-
<BalanceRow>
91-
<PortfolioLogo currencies={currencies} chainId={token.chainId} size="2rem" />
92-
<BalanceContainer>
93-
<BalanceAmountsContainer>
94-
<BalanceItem>
95-
<ThemedText.SubHeader>
96-
{formattedBalance} {token.symbol}
97-
</ThemedText.SubHeader>
98-
</BalanceItem>
99-
<BalanceItem>
100-
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
101-
</BalanceItem>
102-
</BalanceAmountsContainer>
103-
<StyledNetworkLabel color={color}>{label}</StyledNetworkLabel>
104-
</BalanceContainer>
105-
</BalanceRow>
106-
</BalanceSection>
247+
<BalancesCard isInfoTDPEnabled={isInfoTDPEnabled}>
248+
{!isInfoTDPEnabled && <ConnectedChainBalanceSummary connectedChainBalance={connectedChainBalance} />}
249+
{isInfoTDPEnabled && (
250+
<>
251+
<PageChainBalanceSummary pageChainBalance={pageChainBalance} />
252+
<OtherChainsBalanceSummary otherChainBalances={otherChainBalances} hasPageChainBalance={!!pageChainBalance} />
253+
</>
254+
)}
107255
</BalancesCard>
108256
)
109257
}

‎src/components/Tokens/TokenDetails/MobileBalanceSummaryFooter.tsx

+68-31
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@ import { Trans } from '@lingui/macro'
22
import { Currency } from '@uniswap/sdk-core'
33
import { useWeb3React } from '@web3-react/core'
44
import { NATIVE_CHAIN_ID } from 'constants/tokens'
5+
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
6+
import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
57
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
68
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
79
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
8-
import styled from 'styled-components'
9-
import { StyledInternalLink } from 'theme/components'
10+
import styled, { css } from 'styled-components'
11+
import { StyledInternalLink, ThemedText } from 'theme/components'
1012
import { NumberType, useFormatter } from 'utils/formatNumbers'
1113

12-
const Wrapper = styled.div`
14+
const Wrapper = styled.div<{ isInfoTDPEnabled?: boolean }>`
1315
align-content: center;
1416
align-items: center;
15-
border: 1px solid ${({ theme }) => theme.surface3};
16-
border-bottom: none;
1717
background-color: ${({ theme }) => theme.surface1};
18-
border-radius: 20px 20px 0px 0px;
19-
bottom: 52px;
18+
border: 1px solid ${({ theme }) => theme.surface3};
2019
color: ${({ theme }) => theme.neutral2};
2120
display: flex;
2221
flex-direction: row;
@@ -26,9 +25,24 @@ const Wrapper = styled.div`
2625
justify-content: space-between;
2726
left: 0;
2827
line-height: 20px;
29-
padding: 12px 16px;
3028
position: fixed;
31-
width: 100%;
29+
30+
${({ isInfoTDPEnabled }) =>
31+
isInfoTDPEnabled
32+
? css`
33+
border-radius: 20px;
34+
bottom: 56px;
35+
margin: 8px;
36+
padding: 12px 32px;
37+
width: calc(100vw - 16px);
38+
`
39+
: css`
40+
border-bottom: none;
41+
border-radius: 20px 20px 0px 0px;
42+
bottom: 52px;
43+
padding: 12px 16px;
44+
width: 100%;
45+
`}
3246
3347
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
3448
bottom: 0px;
@@ -37,54 +51,64 @@ const Wrapper = styled.div`
3751
display: none;
3852
}
3953
`
40-
const BalanceValue = styled.div`
54+
const BalanceValue = styled.div<{ isInfoTDPEnabled?: boolean }>`
4155
color: ${({ theme }) => theme.neutral1};
4256
font-size: 20px;
43-
line-height: 28px;
57+
line-height: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '20px' : '28px')};
4458
display: flex;
4559
gap: 8px;
4660
`
47-
const Balance = styled.div`
48-
align-items: center;
61+
const Balance = styled.div<{ isInfoTDPEnabled?: boolean }>`
62+
align-items: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? 'flex-end' : 'center')};
4963
display: flex;
5064
flex-direction: row;
5165
flex-wrap: wrap;
5266
gap: 8px;
5367
`
54-
const BalanceInfo = styled.div`
68+
const BalanceInfo = styled.div<{ isInfoTDPEnabled?: boolean }>`
5569
display: flex;
5670
flex: 10 1 auto;
5771
flex-direction: column;
5872
justify-content: flex-start;
73+
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'gap: 6px;'}
5974
`
60-
const FiatValue = styled.span`
75+
const FiatValue = styled(ThemedText.Caption)<{ isInfoTDPEnabled?: boolean }>`
76+
${({ isInfoTDPEnabled, theme }) => !isInfoTDPEnabled && `color: ${theme.neutral2};`}
6177
font-size: 12px;
6278
line-height: 16px;
6379
6480
@media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) {
6581
line-height: 24px;
6682
}
6783
`
68-
const SwapButton = styled(StyledInternalLink)`
84+
const SwapButton = styled(StyledInternalLink)<{ isInfoTDPEnabled?: boolean }>`
6985
background-color: ${({ theme }) => theme.accent1};
7086
border: none;
71-
border-radius: 12px;
87+
border-radius: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '22px' : '12px')};
7288
color: ${({ theme }) => theme.deprecated_accentTextLightPrimary};
7389
display: flex;
7490
flex: 1 1 auto;
7591
padding: 12px 16px;
76-
font-size: 1em;
92+
font-size: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '16px' : '1em')};
7793
font-weight: 535;
7894
height: 44px;
7995
justify-content: center;
8096
margin: auto;
8197
max-width: 100vw;
8298
`
8399

84-
export default function MobileBalanceSummaryFooter({ token }: { token: Currency }) {
100+
export default function MobileBalanceSummaryFooter({
101+
currency,
102+
pageChainBalance,
103+
}: {
104+
currency: Currency
105+
pageChainBalance?: PortfolioTokenBalancePartsFragment
106+
}) {
107+
const isInfoTDPEnabled = useInfoTDPEnabled()
108+
85109
const { account } = useWeb3React()
86-
const balance = useCurrencyBalance(account, token)
87-
const { formatCurrencyAmount } = useFormatter()
110+
const balance = useCurrencyBalance(account, currency)
111+
const { formatCurrencyAmount, formatNumber } = useFormatter()
88112
const formattedBalance = formatCurrencyAmount({
89113
amount: balance,
90114
type: NumberType.TokenNonTx,
@@ -93,22 +117,35 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
93117
amount: useStablecoinValue(balance),
94118
type: NumberType.FiatTokenStats,
95119
})
96-
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
120+
const formattedGqlBalance = formatNumber({
121+
input: pageChainBalance?.quantity,
122+
type: NumberType.TokenNonTx,
123+
})
124+
const formattedUsdGqlValue = formatNumber({
125+
input: pageChainBalance?.denominatedValue?.value,
126+
type: NumberType.PortfolioBalance,
127+
})
128+
const chain = CHAIN_ID_TO_BACKEND_NAME[currency.chainId].toLowerCase()
97129

98130
return (
99-
<Wrapper>
100-
{Boolean(account && balance) && (
101-
<BalanceInfo>
102-
<Trans>Your {token.symbol} balance</Trans>
103-
<Balance>
104-
<BalanceValue>
105-
{formattedBalance} {token.symbol}
131+
<Wrapper isInfoTDPEnabled={isInfoTDPEnabled}>
132+
{Boolean(account && (isInfoTDPEnabled ? pageChainBalance : balance)) && (
133+
<BalanceInfo isInfoTDPEnabled={isInfoTDPEnabled}>
134+
{isInfoTDPEnabled ? <Trans>Your balance</Trans> : <Trans>Your {currency.symbol} balance</Trans>}
135+
<Balance isInfoTDPEnabled={isInfoTDPEnabled}>
136+
<BalanceValue isInfoTDPEnabled={isInfoTDPEnabled}>
137+
{isInfoTDPEnabled ? formattedGqlBalance : formattedBalance} {currency.symbol}
106138
</BalanceValue>
107-
<FiatValue>{formattedUsdValue}</FiatValue>
139+
<FiatValue isInfoTDPEnabled={isInfoTDPEnabled}>
140+
{isInfoTDPEnabled ? `(${formattedUsdGqlValue})` : formattedUsdValue}
141+
</FiatValue>
108142
</Balance>
109143
</BalanceInfo>
110144
)}
111-
<SwapButton to={`/swap?chainName=${chain}&outputCurrency=${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
145+
<SwapButton
146+
isInfoTDPEnabled={isInfoTDPEnabled}
147+
to={`/swap?chainName=${chain}&outputCurrency=${currency.isNative ? NATIVE_CHAIN_ID : currency.address}`}
148+
>
112149
<Trans>Swap</Trans>
113150
</SwapButton>
114151
</Wrapper>

‎src/components/Tokens/TokenDetails/index.tsx

+34-18
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import { InterfacePageName } from '@uniswap/analytics-events'
33
import { useWeb3React } from '@web3-react/core'
44
import { Trace } from 'analytics'
55
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
6+
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
67
import { AboutSection } from 'components/Tokens/TokenDetails/About'
78
import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
8-
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
99
import { BreadcrumbNav, BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
1010
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
11-
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
1211
import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
1312
import TokenDetailsSkeleton, {
1413
Hr,
@@ -25,8 +24,13 @@ import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
2524
import { checkWarning } from 'constants/tokenSafety'
2625
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
2726
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
28-
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
29-
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
27+
import {
28+
Chain,
29+
PortfolioTokenBalancePartsFragment,
30+
TokenPriceQuery,
31+
TokenQuery,
32+
} from 'graphql/data/__generated__/types-and-hooks'
33+
import { TokenQueryData } from 'graphql/data/Token'
3034
import { getTokenDetailsURL, gqlToCurrency, InterfaceGqlChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
3135
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
3236
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
@@ -41,7 +45,9 @@ import { CopyContractAddress } from 'theme/components'
4145
import { isAddress, shortenAddress } from 'utils'
4246
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
4347

48+
import BalanceSummary from './BalanceSummary'
4449
import InvalidTokenDetails from './InvalidTokenDetails'
50+
import MobileBalanceSummaryFooter from './MobileBalanceSummaryFooter'
4551
import { TokenDescription } from './TokenDescription'
4652

4753
const TokenSymbol = styled.span`
@@ -94,7 +100,7 @@ function useRelevantToken(
94100
[onChainToken, queryToken]
95101
)
96102
}
97-
103+
export type MultiChainMap = { [chain: string]: { address?: string; balance?: PortfolioTokenBalancePartsFragment } }
98104
type TokenDetailsProps = {
99105
urlAddress?: string
100106
inputTokenAddress?: string
@@ -117,17 +123,25 @@ export default function TokenDetails({
117123
[urlAddress]
118124
)
119125

120-
const { chainId: connectedChainId } = useWeb3React()
126+
const { account, chainId: connectedChainId } = useWeb3React()
121127
const pageChainId = supportedChainIdFromGQLChain(chain)
122128
const tokenQueryData = tokenQuery.token
123-
const crossChainMap = useMemo(
124-
() =>
125-
tokenQueryData?.project?.tokens.reduce((map, current) => {
126-
if (current) map[current.chain] = current.address
127-
return map
128-
}, {} as { [key: string]: string | undefined }) ?? {},
129-
[tokenQueryData]
130-
)
129+
const { data: balanceQuery } = useCachedPortfolioBalancesQuery({ account })
130+
const multiChainMap = useMemo(() => {
131+
const tokenBalances = balanceQuery?.portfolios?.[0].tokenBalances
132+
const tokensAcrossChains = tokenQueryData?.project?.tokens
133+
if (!tokensAcrossChains) return {}
134+
return tokensAcrossChains.reduce((map, current) => {
135+
if (current) {
136+
if (!map[current.chain]) {
137+
map[current.chain] = {}
138+
}
139+
map[current.chain].address = current.address
140+
map[current.chain].balance = tokenBalances?.find((tokenBalance) => tokenBalance.token?.id === current.id)
141+
}
142+
return map
143+
}, {} as MultiChainMap)
144+
}, [balanceQuery?.portfolios, tokenQueryData?.project?.tokens])
131145

132146
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
133147

@@ -143,7 +157,7 @@ export default function TokenDetails({
143157
const navigateToTokenForChain = useCallback(
144158
(update: Chain) => {
145159
if (!address) return
146-
const bridgedAddress = crossChainMap[update]
160+
const bridgedAddress = multiChainMap[update].address
147161
if (bridgedAddress) {
148162
startTokenTransition(() =>
149163
navigate(
@@ -158,7 +172,7 @@ export default function TokenDetails({
158172
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled })))
159173
}
160174
},
161-
[address, crossChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
175+
[address, multiChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
162176
)
163177
useOnGlobalChainSwitch(navigateToTokenForChain)
164178

@@ -283,7 +297,7 @@ export default function TokenDetails({
283297
/>
284298
</div>
285299
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
286-
{!isInfoTDPEnabled && detailedToken && <BalanceSummary token={detailedToken} />}
300+
{detailedToken && <BalanceSummary currency={detailedToken} chain={chain} multiChainMap={multiChainMap} />}
287301
{isInfoTDPEnabled && (
288302
<TokenDescription
289303
tokenAddress={address}
@@ -293,7 +307,9 @@ export default function TokenDetails({
293307
/>
294308
)}
295309
</RightPanel>
296-
{!isInfoTDPEnabled && detailedToken && <MobileBalanceSummaryFooter token={detailedToken} />}
310+
{detailedToken && (
311+
<MobileBalanceSummaryFooter currency={detailedToken} pageChainBalance={multiChainMap[chain].balance} />
312+
)}
297313

298314
<TokenSafetyModal
299315
isOpen={openTokenSafetyModal || !!continueSwap}

‎src/graphql/data/Token.ts

-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,4 @@ gql`
9797
}
9898
}
9999
`
100-
101-
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
102-
103100
export type TokenQueryData = TokenQuery['token']

‎src/graphql/data/apollo.ts

+16
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ export const apolloClient = new ApolloClient({
4949
},
5050
},
5151
},
52+
TokenProject: {
53+
fields: {
54+
tokens: {
55+
// cache data may be lost when replacing the tokens array
56+
merge(existing, incoming) {
57+
if (!existing) {
58+
return incoming
59+
} else if (Array.isArray(existing)) {
60+
return [...existing, ...incoming]
61+
} else {
62+
return [existing, ...incoming]
63+
}
64+
},
65+
},
66+
},
67+
},
5268
},
5369
}),
5470
defaultOptions: {

‎src/graphql/data/portfolios.ts

+36-28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
import gql from 'graphql-tag'
22

3+
import { PortfolioTokenBalancePartsFragment } from './__generated__/types-and-hooks'
4+
35
gql`
6+
fragment PortfolioTokenBalanceParts on TokenBalance {
7+
id
8+
quantity
9+
denominatedValue {
10+
id
11+
currency
12+
value
13+
}
14+
token {
15+
id
16+
chain
17+
address
18+
name
19+
symbol
20+
standard
21+
decimals
22+
}
23+
tokenProjectMarket {
24+
id
25+
pricePercentChange(duration: DAY) {
26+
id
27+
value
28+
}
29+
tokenProject {
30+
id
31+
logoUrl
32+
isSpam
33+
}
34+
}
35+
}
36+
437
query PortfolioBalances($ownerAddress: String!, $chains: [Chain!]!) {
538
portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
639
id
@@ -19,35 +52,10 @@ gql`
1952
}
2053
}
2154
tokenBalances {
22-
id
23-
quantity
24-
denominatedValue {
25-
id
26-
currency
27-
value
28-
}
29-
tokenProjectMarket {
30-
id
31-
pricePercentChange(duration: DAY) {
32-
id
33-
value
34-
}
35-
tokenProject {
36-
id
37-
logoUrl
38-
isSpam
39-
}
40-
}
41-
token {
42-
id
43-
chain
44-
address
45-
name
46-
symbol
47-
standard
48-
decimals
49-
}
55+
...PortfolioTokenBalanceParts
5056
}
5157
}
5258
}
5359
`
60+
61+
export type PortfolioToken = NonNullable<PortfolioTokenBalancePartsFragment['token']>

‎src/hooks/useGlobalChainSwitch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useWeb3React } from '@web3-react/core'
2-
import { Chain } from 'graphql/data/Token'
2+
import { Chain } from 'graphql/data/__generated__/types-and-hooks'
33
import { chainIdToBackendName } from 'graphql/data/util'
44
import { useEffect, useRef } from 'react'
55

‎src/lib/hooks/useCurrencyBalance.ts

+4
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ export function useTokenBalance(account?: string, token?: Token): CurrencyAmount
108108
return tokenBalances[token.address]
109109
}
110110

111+
/**
112+
* Returns balances for tokens on currently-connected chainId via RPC.
113+
* See useCachedPortfolioBalancesQuery for multichain portfolio balances via GQL.
114+
*/
111115
export function useCurrencyBalances(
112116
account?: string,
113117
currencies?: (Currency | undefined)[]

‎src/pages/TokenDetails/index.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
12
import TokenDetails from 'components/Tokens/TokenDetails'
23
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
34
import { NATIVE_CHAIN_ID } from 'constants/tokens'
@@ -7,10 +8,15 @@ import useParsedQueryString from 'hooks/useParsedQueryString'
78
import { atomWithStorage, useAtomValue } from 'jotai/utils'
89
import { useEffect, useMemo, useState } from 'react'
910
import { useParams } from 'react-router-dom'
11+
import styled from 'styled-components'
1012
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
1113

1214
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
1315

16+
const StyledPrefetchBalancesWrapper = styled(PrefetchBalancesWrapper)`
17+
display: contents;
18+
`
19+
1420
export default function TokenDetailsPage() {
1521
const { tokenAddress, chainName } = useParams<{
1622
tokenAddress: string
@@ -58,12 +64,14 @@ export default function TokenDetailsPage() {
5864
if (!tokenQuery) return <TokenDetailsPageSkeleton />
5965

6066
return (
61-
<TokenDetails
62-
urlAddress={tokenAddress}
63-
chain={chain}
64-
tokenQuery={tokenQuery}
65-
tokenPriceQuery={currentPriceQuery}
66-
inputTokenAddress={parsedInputTokenAddress}
67-
/>
67+
<StyledPrefetchBalancesWrapper shouldFetchOnAccountUpdate={true} shouldFetchOnHover={false}>
68+
<TokenDetails
69+
urlAddress={tokenAddress}
70+
chain={chain}
71+
tokenQuery={tokenQuery}
72+
tokenPriceQuery={currentPriceQuery}
73+
inputTokenAddress={parsedInputTokenAddress}
74+
/>
75+
</StyledPrefetchBalancesWrapper>
6876
)
6977
}

‎src/utils/currencyKey.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ChainId, Currency } from '@uniswap/sdk-core'
22
import { NATIVE_CHAIN_ID } from 'constants/tokens'
3-
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
4-
import { Chain } from 'graphql/data/Token'
3+
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
54
import { supportedChainIdFromGQLChain } from 'graphql/data/util'
65

76
export type CurrencyKey = string

‎src/utils/splitHiddenTokens.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { TokenBalance, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
1+
import { PortfolioTokenBalancePartsFragment, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
22

33
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
44

55
export function splitHiddenTokens(
6-
tokenBalances: TokenBalance[],
6+
tokenBalances: readonly PortfolioTokenBalancePartsFragment[],
77
options: {
88
hideSmallBalances?: boolean
99
} = { hideSmallBalances: true }
1010
) {
11-
const visibleTokens: TokenBalance[] = []
12-
const hiddenTokens: TokenBalance[] = []
11+
const visibleTokens: PortfolioTokenBalancePartsFragment[] = []
12+
const hiddenTokens: PortfolioTokenBalancePartsFragment[] = []
1313

1414
for (const tokenBalance of tokenBalances) {
1515
// 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(
2929
return { visibleTokens, hiddenTokens }
3030
}
3131

32-
function meetsThreshold(tokenBalance: TokenBalance) {
32+
function meetsThreshold(tokenBalance: PortfolioTokenBalancePartsFragment) {
3333
const value = tokenBalance.denominatedValue?.value ?? 0
3434
return value > HIDE_SMALL_USD_BALANCES_THRESHOLD
3535
}

0 commit comments

Comments
 (0)
Please sign in to comment.