Skip to content

Commit fc7ecc7

Browse files
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 <[email protected]>
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 9 additions & 5 deletions
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

Lines changed: 17 additions & 4 deletions
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

Lines changed: 1 addition & 2 deletions
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) {
Lines changed: 186 additions & 38 deletions
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
}

0 commit comments

Comments
 (0)