feat(token-details): native/wrapped-native balance UI (#4814)
* feat(token-details): balance hook * mobile balance UI * feat(token-details): sidebar balance summary * pr feedback
This commit is contained in:
parent
53b57879a3
commit
22b26de78d
@ -1,83 +1,110 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { useToken } from 'hooks/Tokens'
|
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
import { formatToDecimal } from 'analytics/utils'
|
||||||
import { AlertTriangle } from 'react-feather'
|
import CurrencyLogo from 'components/CurrencyLogo'
|
||||||
|
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||||
|
|
||||||
const BalancesCard = styled.div`
|
const BalancesCard = styled.div`
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
color: ${({ theme }) => theme.textPrimary};
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: ${({ theme }) => theme.shallowShadow};
|
box-shadow: ${({ theme }) => theme.shallowShadow};
|
||||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
`
|
color: ${({ theme }) => theme.textPrimary};
|
||||||
const ErrorState = styled.div`
|
display: none;
|
||||||
display: flex;
|
font-size: 12px;
|
||||||
align-items: center;
|
height: fit-content;
|
||||||
gap: 12px;
|
line-height: 16px;
|
||||||
color: ${({ theme }) => theme.textSecondary};
|
padding: 20px;
|
||||||
font-weight: 500;
|
width: 100%;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
`
|
|
||||||
const ErrorText = styled.span`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
`
|
|
||||||
|
|
||||||
|
// 768 hardcoded to match NFT-redesign navbar breakpoints
|
||||||
|
// src/nft/css/sprinkles.css.ts
|
||||||
|
// change to match theme breakpoints when this navbar is updated
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`
|
||||||
const TotalBalanceSection = styled.div`
|
const TotalBalanceSection = styled.div`
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
width: 100%;
|
||||||
`
|
`
|
||||||
const TotalBalance = styled.div`
|
const TotalBalance = styled.div`
|
||||||
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: row;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
align-items: center;
|
|
||||||
`
|
`
|
||||||
const TotalBalanceItem = styled.div`
|
const TotalBalanceItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function BalanceSummary({
|
export interface BalanceSummaryProps {
|
||||||
address,
|
tokenAmount: CurrencyAmount<Token> | undefined
|
||||||
balance,
|
nativeCurrencyAmount: CurrencyAmount<Currency> | undefined
|
||||||
balanceUsd,
|
isNative: boolean
|
||||||
}: {
|
}
|
||||||
address: string
|
|
||||||
balance?: number
|
export default function BalanceSummary({ tokenAmount, nativeCurrencyAmount, isNative }: BalanceSummaryProps) {
|
||||||
balanceUsd?: number
|
const balanceUsdValue = useStablecoinValue(tokenAmount)?.toFixed(2)
|
||||||
}) {
|
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)?.toFixed(2)
|
||||||
const token = useToken(address)
|
|
||||||
const { loading, error } = useNetworkTokenBalances({ address })
|
const tokenIsWrappedNative =
|
||||||
|
tokenAmount &&
|
||||||
|
nativeCurrencyAmount &&
|
||||||
|
tokenAmount.currency.address.toLowerCase() === nativeCurrencyAmount.currency.wrapped.address.toLowerCase()
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!tokenAmount && !tokenIsWrappedNative && !isNative) ||
|
||||||
|
(!isNative && !tokenIsWrappedNative && tokenAmount?.equalTo(0)) ||
|
||||||
|
(isNative && tokenAmount?.equalTo(0) && nativeCurrencyAmount?.equalTo(0))
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const showNative = tokenIsWrappedNative || isNative
|
||||||
|
|
||||||
|
const currencies = []
|
||||||
|
|
||||||
|
if (tokenAmount) {
|
||||||
|
currencies.push({
|
||||||
|
currency: tokenAmount.currency,
|
||||||
|
formattedBalance: formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2)),
|
||||||
|
formattedUSDValue: balanceUsdValue ? parseFloat(balanceUsdValue) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (showNative && nativeCurrencyAmount) {
|
||||||
|
const nativeData = {
|
||||||
|
currency: nativeCurrencyAmount.currency,
|
||||||
|
formattedBalance: formatToDecimal(nativeCurrencyAmount, Math.min(nativeCurrencyAmount.currency.decimals, 2)),
|
||||||
|
formattedUSDValue: nativeBalanceUsdValue ? parseFloat(nativeBalanceUsdValue) : undefined,
|
||||||
|
}
|
||||||
|
if (isNative) {
|
||||||
|
currencies.unshift(nativeData)
|
||||||
|
} else {
|
||||||
|
currencies.push(nativeData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading || (!error && !balance && !balanceUsd)) return null
|
|
||||||
return (
|
return (
|
||||||
<BalancesCard>
|
<BalancesCard>
|
||||||
{error ? (
|
<TotalBalanceSection>
|
||||||
<ErrorState>
|
<Trans>Your balance</Trans>
|
||||||
<AlertTriangle size={24} />
|
{currencies.map(({ currency, formattedBalance, formattedUSDValue }) => (
|
||||||
<ErrorText>
|
<TotalBalance key={currency.wrapped.address}>
|
||||||
<Trans>There was an error loading your {token?.symbol} balance</Trans>
|
<TotalBalanceItem>
|
||||||
</ErrorText>
|
<CurrencyLogo currency={currency} />
|
||||||
</ErrorState>
|
{formattedBalance} {currency?.symbol}
|
||||||
) : (
|
</TotalBalanceItem>
|
||||||
<>
|
<TotalBalanceItem>
|
||||||
<TotalBalanceSection>
|
{formatDollarAmount(formattedUSDValue === 0 ? undefined : formattedUSDValue)}
|
||||||
Your balance
|
</TotalBalanceItem>
|
||||||
<TotalBalance>
|
</TotalBalance>
|
||||||
<TotalBalanceItem>{`${balance} ${token?.symbol}`}</TotalBalanceItem>
|
))}
|
||||||
<TotalBalanceItem>{`$${balanceUsd}`}</TotalBalanceItem>
|
</TotalBalanceSection>
|
||||||
</TotalBalance>
|
|
||||||
</TotalBalanceSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</BalancesCard>
|
</BalancesCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
|
||||||
import { useToken } from 'hooks/Tokens'
|
|
||||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { AlertTriangle } from 'react-feather'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import styled from 'styled-components/macro'
|
|
||||||
|
|
||||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
|
||||||
import { LoadingBubble } from '../loading'
|
|
||||||
|
|
||||||
const PLACEHOLDER_NAV_FOOTER_HEIGHT = '56px'
|
|
||||||
const BalanceFooter = styled.div`
|
|
||||||
height: fit-content;
|
|
||||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
|
||||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
|
||||||
border-radius: 20px 20px 0px 0px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
width: 100%;
|
|
||||||
color: ${({ theme }) => theme.textSecondary};
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
bottom: ${PLACEHOLDER_NAV_FOOTER_HEIGHT};
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-content: center;
|
|
||||||
`
|
|
||||||
const BalanceValue = styled.div`
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 28px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
const BalanceTotal = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
color: ${({ theme }) => theme.textPrimary};
|
|
||||||
`
|
|
||||||
const BalanceInfo = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
`
|
|
||||||
const FakeFooterNavBar = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0px;
|
|
||||||
background-color: ${({ theme }) => theme.backgroundBackdrop};
|
|
||||||
height: ${PLACEHOLDER_NAV_FOOTER_HEIGHT};
|
|
||||||
width: 100%;
|
|
||||||
align-items: flex-end;
|
|
||||||
padding: 20px 8px;
|
|
||||||
font-size: 10px;
|
|
||||||
`
|
|
||||||
const FiatValue = styled.span`
|
|
||||||
display: flex;
|
|
||||||
align-self: flex-end;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 24px;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const NetworkBalancesSection = styled.div`
|
|
||||||
height: fit-content;
|
|
||||||
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 16px 0px 8px 0px;
|
|
||||||
margin-top: 16px;
|
|
||||||
color: ${({ theme }) => theme.textPrimary};
|
|
||||||
`
|
|
||||||
const NetworkBalancesLabel = styled.span`
|
|
||||||
color: ${({ theme }) => theme.textSecondary};
|
|
||||||
`
|
|
||||||
const SwapButton = styled.button`
|
|
||||||
background-color: ${({ theme }) => theme.accentAction};
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
|
||||||
padding: 12px 16px;
|
|
||||||
width: 120px;
|
|
||||||
height: 44px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
justify-content: center;
|
|
||||||
`
|
|
||||||
const TotalBalancesSection = styled.div`
|
|
||||||
display: flex;
|
|
||||||
color: ${({ theme }) => theme.textSecondary};
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
const ViewAll = styled.span`
|
|
||||||
display: flex;
|
|
||||||
color: ${({ theme }) => theme.accentAction};
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
`
|
|
||||||
const ErrorState = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
`
|
|
||||||
const LoadingState = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
`
|
|
||||||
const TopBalanceLoadBubble = styled(LoadingBubble)`
|
|
||||||
height: 12px;
|
|
||||||
width: 172px;
|
|
||||||
`
|
|
||||||
const BottomBalanceLoadBubble = styled(LoadingBubble)`
|
|
||||||
height: 16px;
|
|
||||||
width: 188px;
|
|
||||||
`
|
|
||||||
const ErrorText = styled.span`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function FooterBalanceSummary({
|
|
||||||
address,
|
|
||||||
networkBalances,
|
|
||||||
balance,
|
|
||||||
balanceUsd,
|
|
||||||
}: {
|
|
||||||
address: string
|
|
||||||
networkBalances: (JSX.Element | null)[] | null
|
|
||||||
balance?: number
|
|
||||||
balanceUsd?: number
|
|
||||||
}) {
|
|
||||||
const tokenSymbol = useToken(address)?.symbol
|
|
||||||
const [showMultipleBalances, setShowMultipleBalances] = useState(false)
|
|
||||||
const multipleBalances = false // for testing purposes
|
|
||||||
const networkNameIfOneBalance = 'Ethereum' // for testing purposes
|
|
||||||
const { loading, error } = useNetworkTokenBalances({ address })
|
|
||||||
return (
|
|
||||||
<BalanceFooter>
|
|
||||||
<TotalBalancesSection>
|
|
||||||
{loading ? (
|
|
||||||
<LoadingState>
|
|
||||||
<TopBalanceLoadBubble></TopBalanceLoadBubble>
|
|
||||||
<BottomBalanceLoadBubble></BottomBalanceLoadBubble>
|
|
||||||
</LoadingState>
|
|
||||||
) : error ? (
|
|
||||||
<ErrorState>
|
|
||||||
<AlertTriangle size={17} />
|
|
||||||
<ErrorText>
|
|
||||||
<Trans>There was an error fetching your balance</Trans>
|
|
||||||
</ErrorText>
|
|
||||||
</ErrorState>
|
|
||||||
) : (
|
|
||||||
!!balance &&
|
|
||||||
!!balanceUsd && (
|
|
||||||
<BalanceInfo>
|
|
||||||
{multipleBalances ? 'Balance on all networks' : `Your balance on ${networkNameIfOneBalance}`}
|
|
||||||
<BalanceTotal>
|
|
||||||
<BalanceValue>
|
|
||||||
{balance} {tokenSymbol}
|
|
||||||
</BalanceValue>
|
|
||||||
<FiatValue>{`$${balanceUsd}`}</FiatValue>
|
|
||||||
</BalanceTotal>
|
|
||||||
{multipleBalances && (
|
|
||||||
<ViewAll onClick={() => setShowMultipleBalances(!showMultipleBalances)}>
|
|
||||||
<Trans>{showMultipleBalances ? 'Hide' : 'View'} all balances</Trans>
|
|
||||||
</ViewAll>
|
|
||||||
)}
|
|
||||||
</BalanceInfo>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<Link to={`/swap?outputCurrency=${address}`}>
|
|
||||||
<SwapButton>
|
|
||||||
<Trans>Swap</Trans>
|
|
||||||
</SwapButton>
|
|
||||||
</Link>
|
|
||||||
</TotalBalancesSection>
|
|
||||||
{showMultipleBalances && (
|
|
||||||
<NetworkBalancesSection>
|
|
||||||
<NetworkBalancesLabel>
|
|
||||||
<Trans>Your balances by network</Trans>
|
|
||||||
</NetworkBalancesLabel>
|
|
||||||
{networkBalances}
|
|
||||||
</NetworkBalancesSection>
|
|
||||||
)}
|
|
||||||
<FakeFooterNavBar>**leaving space for updated nav footer**</FakeFooterNavBar>
|
|
||||||
</BalanceFooter>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { WidgetSkeleton } from 'components/Widget'
|
import { WidgetSkeleton } from 'components/Widget'
|
||||||
import { Footer, LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
import { LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
||||||
import styled, { useTheme } from 'styled-components/macro'
|
import styled, { useTheme } from 'styled-components/macro'
|
||||||
|
|
||||||
import { LoadingBubble } from '../loading'
|
import { LoadingBubble } from '../loading'
|
||||||
@ -158,7 +158,6 @@ export function LoadingTokenDetails() {
|
|||||||
<RightPanel>
|
<RightPanel>
|
||||||
<WidgetSkeleton />
|
<WidgetSkeleton />
|
||||||
</RightPanel>
|
</RightPanel>
|
||||||
<Footer />
|
|
||||||
</TokenDetailsLayout>
|
</TokenDetailsLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { formatToDecimal } from 'analytics/utils'
|
||||||
|
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components/macro'
|
||||||
|
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||||
|
|
||||||
|
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||||
|
import { BalanceSummaryProps } from './BalanceSummary'
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
height: fit-content;
|
||||||
|
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||||
|
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||||
|
border-radius: 20px 20px 0px 0px;
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 100%;
|
||||||
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 56px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
// 768 hardcoded to match NFT-redesign navbar breakpoints
|
||||||
|
// src/nft/css/sprinkles.css.ts
|
||||||
|
// change to match theme breakpoints when this navbar is updated
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const BalanceValue = styled.div`
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
const BalanceTotal = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
color: ${({ theme }) => theme.textPrimary};
|
||||||
|
`
|
||||||
|
const BalanceInfo = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
const FiatValue = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-end;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const SwapButton = styled.button`
|
||||||
|
background-color: ${({ theme }) => theme.accentAction};
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||||
|
padding: 12px 16px;
|
||||||
|
width: 120px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
const TotalBalancesSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function MobileBalanceSummaryFooter({
|
||||||
|
tokenAmount,
|
||||||
|
nativeCurrencyAmount,
|
||||||
|
isNative,
|
||||||
|
}: BalanceSummaryProps) {
|
||||||
|
const balanceUsdValue = useStablecoinValue(tokenAmount)?.toFixed(2)
|
||||||
|
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)?.toFixed(2)
|
||||||
|
|
||||||
|
const formattedBalance = tokenAmount
|
||||||
|
? formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const balanceUsd = balanceUsdValue ? parseFloat(balanceUsdValue) : undefined
|
||||||
|
|
||||||
|
const formattedNativeBalance = nativeCurrencyAmount
|
||||||
|
? formatToDecimal(nativeCurrencyAmount, Math.min(nativeCurrencyAmount.currency.decimals, 2))
|
||||||
|
: undefined
|
||||||
|
const nativeBalanceUsd = nativeBalanceUsdValue ? parseFloat(nativeBalanceUsdValue) : undefined
|
||||||
|
|
||||||
|
if ((!tokenAmount && !nativeCurrencyAmount) || (nativeCurrencyAmount?.equalTo(0) && tokenAmount?.equalTo(0))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputTokenAddress = tokenAmount?.currency.address ?? nativeCurrencyAmount?.wrapped.currency.address
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TotalBalancesSection>
|
||||||
|
{Boolean(formattedBalance !== undefined && !isNative) && (
|
||||||
|
<BalanceInfo>
|
||||||
|
<Trans>Your {tokenAmount?.currency?.symbol} balance</Trans>
|
||||||
|
<BalanceTotal>
|
||||||
|
<BalanceValue>
|
||||||
|
{formattedBalance} {tokenAmount?.currency?.symbol}
|
||||||
|
</BalanceValue>
|
||||||
|
<FiatValue>{formatDollarAmount(balanceUsd)}</FiatValue>
|
||||||
|
</BalanceTotal>
|
||||||
|
</BalanceInfo>
|
||||||
|
)}
|
||||||
|
{isNative && (
|
||||||
|
<BalanceInfo>
|
||||||
|
<Trans>Your {nativeCurrencyAmount?.currency?.symbol} balance</Trans>
|
||||||
|
<BalanceTotal>
|
||||||
|
<BalanceValue>
|
||||||
|
{formattedNativeBalance} {nativeCurrencyAmount?.currency?.symbol}
|
||||||
|
</BalanceValue>
|
||||||
|
<FiatValue>{formatDollarAmount(nativeBalanceUsd)}</FiatValue>
|
||||||
|
</BalanceTotal>
|
||||||
|
</BalanceInfo>
|
||||||
|
)}
|
||||||
|
<Link to={`/swap?outputCurrency=${outputTokenAddress}`}>
|
||||||
|
<SwapButton>
|
||||||
|
<Trans>Swap</Trans>
|
||||||
|
</SwapButton>
|
||||||
|
</Link>
|
||||||
|
</TotalBalancesSection>
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
@ -1,64 +0,0 @@
|
|||||||
import styled, { useTheme } from 'styled-components/macro'
|
|
||||||
|
|
||||||
const Balance = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
`
|
|
||||||
const BalanceItem = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`
|
|
||||||
const BalanceRow = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
`
|
|
||||||
const Logo = styled.img`
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
margin-right: 8px;
|
|
||||||
`
|
|
||||||
const Network = styled.span<{ color: string }>`
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${({ color }) => color};
|
|
||||||
`
|
|
||||||
const NetworkBalanceContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
padding-top: 16px;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function NetworkBalance({
|
|
||||||
logoUrl,
|
|
||||||
balance,
|
|
||||||
tokenSymbol,
|
|
||||||
fiatValue,
|
|
||||||
label,
|
|
||||||
networkColor,
|
|
||||||
}: {
|
|
||||||
logoUrl: string
|
|
||||||
balance: string
|
|
||||||
tokenSymbol: string
|
|
||||||
fiatValue: string | number
|
|
||||||
label: string
|
|
||||||
networkColor: string | undefined
|
|
||||||
}) {
|
|
||||||
const theme = useTheme()
|
|
||||||
return (
|
|
||||||
<NetworkBalanceContainer>
|
|
||||||
<Logo src={logoUrl} />
|
|
||||||
<Balance>
|
|
||||||
<BalanceRow>
|
|
||||||
<BalanceItem>
|
|
||||||
{balance} {tokenSymbol}
|
|
||||||
</BalanceItem>
|
|
||||||
<BalanceItem>${fiatValue}</BalanceItem>
|
|
||||||
</BalanceRow>
|
|
||||||
<Network color={networkColor ?? theme.textPrimary}>{label}</Network>
|
|
||||||
</Balance>
|
|
||||||
</NetworkBalanceContainer>
|
|
||||||
)
|
|
||||||
}
|
|
@ -47,6 +47,9 @@ class TokenSafetyLookupTable {
|
|||||||
if (!this.dict) {
|
if (!this.dict) {
|
||||||
this.dict = this.createMap()
|
this.dict = this.createMap()
|
||||||
}
|
}
|
||||||
|
if (address === 'native') {
|
||||||
|
return TOKEN_LIST_TYPES.UNI_DEFAULT
|
||||||
|
}
|
||||||
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
|
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,5 @@ export enum FeatureFlag {
|
|||||||
tokens = 'tokens',
|
tokens = 'tokens',
|
||||||
tokenSafety = 'tokenSafety',
|
tokenSafety = 'tokenSafety',
|
||||||
traceJsonRpc = 'traceJsonRpc',
|
traceJsonRpc = 'traceJsonRpc',
|
||||||
|
multiNetworkBalances = 'multiNetworkBalances',
|
||||||
}
|
}
|
||||||
|
123
src/hooks/useMultiNetworkAddressBalances.ts
Normal file
123
src/hooks/useMultiNetworkAddressBalances.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { Currency, CurrencyAmount, NativeCurrency, Token } from '@uniswap/sdk-core'
|
||||||
|
import { useWeb3React } from '@web3-react/core'
|
||||||
|
import { Weth } from 'abis/types'
|
||||||
|
import WETH_ABI from 'abis/weth.json'
|
||||||
|
import { ALL_SUPPORTED_CHAIN_IDS, isSupportedChain, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
|
||||||
|
import { RPC_PROVIDERS } from 'constants/providers'
|
||||||
|
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||||
|
import { BaseVariant, FeatureFlag, useBaseFlag } from 'featureFlags'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { getContract } from 'utils'
|
||||||
|
|
||||||
|
interface useMultiNetworkAddressBalancesArgs {
|
||||||
|
ownerAddress: string | undefined
|
||||||
|
tokenAddress: 'NATIVE' | string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddressNetworkBalanceData = Partial<
|
||||||
|
Record<
|
||||||
|
SupportedChainId,
|
||||||
|
Record<string | 'NATIVE', CurrencyAmount<Token> | CurrencyAmount<NativeCurrency> | undefined>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
interface handleBalanceArg {
|
||||||
|
amount: CurrencyAmount<Currency>
|
||||||
|
chainId: SupportedChainId
|
||||||
|
tokenAddress: string | 'NATIVE'
|
||||||
|
}
|
||||||
|
|
||||||
|
const testnetSet = new Set(TESTNET_CHAIN_IDS) as Set<SupportedChainId>
|
||||||
|
|
||||||
|
export function useMultiNetworkAddressBalances({ ownerAddress, tokenAddress }: useMultiNetworkAddressBalancesArgs) {
|
||||||
|
const [data, setData] = useState<AddressNetworkBalanceData>({})
|
||||||
|
const [error] = useState<string | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const { chainId: connectedChainId } = useWeb3React()
|
||||||
|
const feature_flag_multi_network_balances = useBaseFlag(FeatureFlag.multiNetworkBalances)
|
||||||
|
|
||||||
|
const handleBalance = useCallback(({ amount, chainId, tokenAddress }: handleBalanceArg) => {
|
||||||
|
if (!amount.greaterThan(0) || !tokenAddress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setData((data) => ({
|
||||||
|
...data,
|
||||||
|
[chainId]: {
|
||||||
|
...(data[chainId] ?? {}),
|
||||||
|
[tokenAddress]: amount,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ownerAddress || !tokenAddress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isConnecteToTestnet = connectedChainId ? TESTNET_CHAIN_IDS.includes(connectedChainId) : false
|
||||||
|
setLoading(true)
|
||||||
|
const isNative = tokenAddress === 'NATIVE'
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
|
const isWrappedNative = ALL_SUPPORTED_CHAIN_IDS.some(
|
||||||
|
(chainId) => WRAPPED_NATIVE_CURRENCY[chainId]?.address.toLowerCase() === tokenAddress.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
const chainsToCheck: SupportedChainId[] =
|
||||||
|
feature_flag_multi_network_balances === BaseVariant.Enabled
|
||||||
|
? ALL_SUPPORTED_CHAIN_IDS
|
||||||
|
: isSupportedChain(connectedChainId)
|
||||||
|
? [SupportedChainId.MAINNET, connectedChainId]
|
||||||
|
: [SupportedChainId.MAINNET]
|
||||||
|
|
||||||
|
chainsToCheck.forEach((chainId) => {
|
||||||
|
const isTestnet = testnetSet.has(chainId)
|
||||||
|
if ((isConnecteToTestnet && isTestnet) || !isTestnet) {
|
||||||
|
const provider = RPC_PROVIDERS[chainId]
|
||||||
|
if (isWrappedNative || isNative) {
|
||||||
|
const wrappedNative = WRAPPED_NATIVE_CURRENCY[chainId]
|
||||||
|
if (wrappedNative) {
|
||||||
|
promises.push(
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const wrappedNativeContract = getContract(wrappedNative.address, WETH_ABI, provider) as Weth
|
||||||
|
const balance = await wrappedNativeContract.balanceOf(ownerAddress, { blockTag: 'latest' })
|
||||||
|
const amount = CurrencyAmount.fromRawAmount(wrappedNative, balance.toString())
|
||||||
|
resolve(handleBalance({ amount, chainId, tokenAddress: wrappedNative.address.toLowerCase() }))
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
promises.push(
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const balance = await provider.getBalance(ownerAddress, 'latest')
|
||||||
|
const nativeCurrency = nativeOnChain(chainId)
|
||||||
|
const amount = CurrencyAmount.fromRawAmount(nativeCurrency, balance.toString())
|
||||||
|
resolve(handleBalance({ amount, chainId, tokenAddress: 'NATIVE' }))
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// todo (jordan): support multi-network ERC20 balances
|
||||||
|
// } else {
|
||||||
|
// promises.push(
|
||||||
|
// new Promise(async (resolve) => {
|
||||||
|
// try {
|
||||||
|
// const ERC20Contract = getContract(tokenAddress, ERC20_ABI, provider) as Erc20
|
||||||
|
// const balance = await ERC20Contract.balanceOf(ownerAddress, { blockTag: 'latest' })
|
||||||
|
// const amount = //
|
||||||
|
// resolve(handleBalance({ amount, chainId, tokenAddress }))
|
||||||
|
// } catch (e) {}
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.catch(() => ({}))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [connectedChainId, feature_flag_multi_network_balances, handleBalance, ownerAddress, tokenAddress])
|
||||||
|
|
||||||
|
return { data, error, loading }
|
||||||
|
}
|
@ -1,77 +0,0 @@
|
|||||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
|
||||||
import { SupportedChainId } from 'constants/chains'
|
|
||||||
import { gql } from 'graphql-request'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
type NetworkTokenBalancesMap = Partial<Record<SupportedChainId, CurrencyAmount<Token>>>
|
|
||||||
|
|
||||||
interface useNetworkTokenBalancesResult {
|
|
||||||
data: NetworkTokenBalancesMap | null
|
|
||||||
error: null | string
|
|
||||||
loading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface useNetworkTokenBalancesArgs {
|
|
||||||
address: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNetworkTokenBalances({ address }: useNetworkTokenBalancesArgs): useNetworkTokenBalancesResult {
|
|
||||||
const [data, setData] = useState<NetworkTokenBalancesMap | null>(null)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const query = gql``
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (address) {
|
|
||||||
const FAKE_TOKEN_NETWORK_BALANCES = {
|
|
||||||
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(
|
|
||||||
new Token(SupportedChainId.ARBITRUM_ONE, address, 18),
|
|
||||||
10e18
|
|
||||||
),
|
|
||||||
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(
|
|
||||||
new Token(SupportedChainId.MAINNET, address, 18),
|
|
||||||
1e18
|
|
||||||
),
|
|
||||||
[SupportedChainId.RINKEBY]: CurrencyAmount.fromRawAmount(
|
|
||||||
new Token(SupportedChainId.RINKEBY, address, 9),
|
|
||||||
10e18
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchNetworkTokenBalances = async (address: string): Promise<NetworkTokenBalancesMap | void> => {
|
|
||||||
const waitRandom = (min: number, max: number): Promise<void> =>
|
|
||||||
new Promise((resolve) => setTimeout(resolve, min + Math.round(Math.random() * Math.max(0, max - min))))
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
await waitRandom(250, 2000)
|
|
||||||
if (Math.random() < 0.05) {
|
|
||||||
throw new Error('fake error')
|
|
||||||
}
|
|
||||||
return FAKE_TOKEN_NETWORK_BALANCES
|
|
||||||
} catch (e) {
|
|
||||||
setError('something went wrong')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
fetchNetworkTokenBalances(address)
|
|
||||||
.then((data) => {
|
|
||||||
if (data) setData(data)
|
|
||||||
})
|
|
||||||
.catch((e) => setError(e))
|
|
||||||
.finally(() => setLoading(false))
|
|
||||||
} else {
|
|
||||||
setData(null)
|
|
||||||
}
|
|
||||||
}, [address, query])
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import { NativeCurrency, Token } from '@uniswap/sdk-core'
|
import { Token } from '@uniswap/sdk-core'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { formatToDecimal } from 'analytics/utils'
|
|
||||||
import {
|
import {
|
||||||
LARGE_MEDIA_BREAKPOINT,
|
LARGE_MEDIA_BREAKPOINT,
|
||||||
MAX_WIDTH_MEDIA_BREAKPOINT,
|
MAX_WIDTH_MEDIA_BREAKPOINT,
|
||||||
@ -13,14 +12,11 @@ import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
|
|||||||
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
|
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
|
||||||
import { BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
|
import { BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
|
||||||
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
|
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
|
||||||
import FooterBalanceSummary from 'components/Tokens/TokenDetails/FooterBalanceSummary'
|
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
|
||||||
import NetworkBalance from 'components/Tokens/TokenDetails/NetworkBalance'
|
|
||||||
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||||
import Widget, { WIDGET_WIDTH } from 'components/Widget'
|
import Widget, { WIDGET_WIDTH } from 'components/Widget'
|
||||||
import { getChainInfo } from 'constants/chainInfo'
|
|
||||||
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
|
|
||||||
import { isCelo, nativeOnChain } from 'constants/tokens'
|
import { isCelo, nativeOnChain } from 'constants/tokens'
|
||||||
import { checkWarning } from 'constants/tokenSafety'
|
import { checkWarning } from 'constants/tokenSafety'
|
||||||
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
|
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
|
||||||
@ -28,21 +24,13 @@ import { useTokenQuery } from 'graphql/data/Token'
|
|||||||
import { CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util'
|
import { CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util'
|
||||||
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
|
||||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
|
import useCurrencyBalance, { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
|
||||||
export const Footer = styled.div`
|
|
||||||
display: none;
|
|
||||||
@media only screen and (max-width: ${LARGE_MEDIA_BREAKPOINT}) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
export const TokenDetailsLayout = styled.div`
|
export const TokenDetailsLayout = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 60px;
|
gap: 60px;
|
||||||
@ -91,48 +79,60 @@ export const RightPanel = styled.div`
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
function NetworkBalances(tokenAddress: string | undefined) {
|
|
||||||
return useNetworkTokenBalances({ address: tokenAddress })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TokenDetails() {
|
export default function TokenDetails() {
|
||||||
const { tokenAddress: tokenAddressParam, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
|
const { tokenAddress: tokenAddressParam, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
|
||||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[validateUrlChainParam(chainName)]
|
const { account } = useWeb3React()
|
||||||
let tokenAddress = tokenAddressParam
|
|
||||||
let nativeCurrency: NativeCurrency | Token | undefined
|
|
||||||
if (tokenAddressParam === 'NATIVE') {
|
|
||||||
nativeCurrency = nativeOnChain(chainId)
|
|
||||||
tokenAddress = nativeCurrency.wrapped.address
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
|
|
||||||
const isBlockedToken = tokenWarning?.canProceed === false
|
|
||||||
|
|
||||||
const timePeriod = useAtomValue(filterTimeAtom)
|
|
||||||
const currentChainName = validateUrlChainParam(chainName)
|
const currentChainName = validateUrlChainParam(chainName)
|
||||||
const [token, prices] = useTokenQuery(tokenAddress ?? '', currentChainName, timePeriod)
|
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[validateUrlChainParam(chainName)]
|
||||||
|
const nativeCurrency = nativeOnChain(pageChainId)
|
||||||
|
const timePeriod = useAtomValue(filterTimeAtom)
|
||||||
|
const isNative = tokenAddressParam === 'NATIVE'
|
||||||
|
const tokenQueryAddress = isNative ? nativeCurrency.wrapped.address : tokenAddressParam
|
||||||
|
const [tokenQueryData, prices] = useTokenQuery(tokenQueryAddress ?? '', currentChainName, timePeriod)
|
||||||
|
|
||||||
|
const pageToken = useMemo(
|
||||||
|
() =>
|
||||||
|
tokenQueryData && !isNative
|
||||||
|
? new Token(
|
||||||
|
CHAIN_NAME_TO_CHAIN_ID[currentChainName],
|
||||||
|
tokenAddressParam ?? '',
|
||||||
|
18,
|
||||||
|
tokenQueryData?.symbol ?? '',
|
||||||
|
tokenQueryData?.name ?? ''
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
[currentChainName, isNative, tokenAddressParam, tokenQueryData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const nativeCurrencyBalance = useCurrencyBalance(account, nativeCurrency)
|
||||||
|
|
||||||
|
const tokenBalance = useTokenBalance(account, isNative ? nativeCurrency.wrapped : pageToken)
|
||||||
|
|
||||||
|
const tokenWarning = tokenAddressParam ? checkWarning(tokenAddressParam) : null
|
||||||
|
const isBlockedToken = tokenWarning?.canProceed === false
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const switchChains = useCallback(
|
const switchChains = useCallback(
|
||||||
(newChain: Chain) => {
|
(newChain: Chain) => {
|
||||||
const chainSegment = newChain.toLowerCase()
|
const chainSegment = newChain.toLowerCase()
|
||||||
if (tokenAddressParam === 'NATIVE') {
|
if (isNative) {
|
||||||
navigate(`/tokens/${chainSegment}/NATIVE`)
|
navigate(`/tokens/${chainSegment}/NATIVE`)
|
||||||
} else {
|
} else {
|
||||||
token?.project?.tokens?.forEach((token) => {
|
tokenQueryData?.project?.tokens?.forEach((token) => {
|
||||||
if (token.chain === newChain && token.address) {
|
if (token.chain === newChain && token.address) {
|
||||||
navigate(`/tokens/${chainSegment}/${token.address}`)
|
navigate(`/tokens/${chainSegment}/${token.address}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigate, token?.project?.tokens, tokenAddressParam]
|
[isNative, navigate, tokenQueryData?.project?.tokens]
|
||||||
)
|
)
|
||||||
useOnGlobalChainSwitch(switchChains)
|
useOnGlobalChainSwitch(switchChains)
|
||||||
|
|
||||||
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
||||||
|
|
||||||
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(tokenAddress, chainId) && tokenWarning !== null
|
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(tokenAddressParam, pageChainId) && tokenWarning !== null
|
||||||
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
|
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
|
||||||
const onReviewSwap = useCallback(
|
const onReviewSwap = useCallback(
|
||||||
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
|
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
|
||||||
@ -147,102 +147,63 @@ export default function TokenDetails() {
|
|||||||
[continueSwap, setContinueSwap]
|
[continueSwap, setContinueSwap]
|
||||||
)
|
)
|
||||||
|
|
||||||
/* network balance handling */
|
|
||||||
const { data: networkData } = NetworkBalances(tokenAddress)
|
|
||||||
const { chainId: connectedChainId, account } = useWeb3React()
|
|
||||||
|
|
||||||
// TODO: consider updating useTokenBalance to work with just address/chain to avoid using Token data structure here
|
|
||||||
const balanceValue = useTokenBalance(
|
|
||||||
account,
|
|
||||||
useMemo(() => new Token(chainId, tokenAddress ?? '', 18), [chainId, tokenAddress])
|
|
||||||
)
|
|
||||||
const balance = balanceValue ? formatToDecimal(balanceValue, Math.min(balanceValue.currency.decimals, 6)) : undefined
|
|
||||||
const balanceUsdValue = useStablecoinValue(balanceValue)?.toFixed(2)
|
|
||||||
const balanceUsd = balanceUsdValue ? parseFloat(balanceUsdValue) : undefined
|
|
||||||
|
|
||||||
const chainsToList = useMemo(() => {
|
|
||||||
let chainIds = [...L1_CHAIN_IDS, ...L2_CHAIN_IDS]
|
|
||||||
const userConnectedToATestNetwork = connectedChainId && TESTNET_CHAIN_IDS.includes(connectedChainId)
|
|
||||||
if (!userConnectedToATestNetwork) {
|
|
||||||
chainIds = chainIds.filter((id) => !(TESTNET_CHAIN_IDS as unknown as SupportedChainId[]).includes(id))
|
|
||||||
}
|
|
||||||
return chainIds
|
|
||||||
}, [connectedChainId])
|
|
||||||
|
|
||||||
const balancesByNetwork = networkData
|
|
||||||
? chainsToList.map((chainId) => {
|
|
||||||
const amount = networkData[chainId]
|
|
||||||
const fiatValue = amount // for testing purposes
|
|
||||||
if (!fiatValue || !token?.symbol) return null
|
|
||||||
const chainInfo = getChainInfo(chainId)
|
|
||||||
const networkColor = chainInfo.color
|
|
||||||
if (!chainInfo) return null
|
|
||||||
return (
|
|
||||||
<NetworkBalance
|
|
||||||
key={chainId}
|
|
||||||
logoUrl={chainInfo.logoUrl}
|
|
||||||
balance={'1'}
|
|
||||||
tokenSymbol={token.symbol}
|
|
||||||
fiatValue={fiatValue.toSignificant(2)}
|
|
||||||
label={chainInfo.label}
|
|
||||||
networkColor={networkColor}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
|
|
||||||
const widgetToken = useMemo(() => {
|
const widgetToken = useMemo(() => {
|
||||||
const currentChainId = CHAIN_NAME_TO_CHAIN_ID[currentChainName]
|
if (pageToken) {
|
||||||
// The widget is not yet configured to use Celo.
|
return pageToken
|
||||||
if (isCelo(chainId) || isCelo(currentChainId)) return undefined
|
}
|
||||||
|
if (nativeCurrency) {
|
||||||
return (
|
if (isCelo(pageChainId)) return undefined
|
||||||
nativeCurrency ??
|
return nativeCurrency
|
||||||
(token?.address && token.symbol && token.name
|
}
|
||||||
? new Token(currentChainId, token.address, 18, token.symbol, token.name)
|
return undefined
|
||||||
: undefined)
|
}, [nativeCurrency, pageChainId, pageToken])
|
||||||
)
|
|
||||||
}, [chainId, currentChainName, nativeCurrency, token?.address, token?.name, token?.symbol])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TokenDetailsLayout>
|
<TokenDetailsLayout>
|
||||||
{token && (
|
{tokenQueryData && (
|
||||||
<>
|
<>
|
||||||
<LeftPanel>
|
<LeftPanel>
|
||||||
<BreadcrumbNavLink to={`/tokens/${chainName}`}>
|
<BreadcrumbNavLink to={`/tokens/${chainName}`}>
|
||||||
<ArrowLeft size={14} /> Tokens
|
<ArrowLeft size={14} /> Tokens
|
||||||
</BreadcrumbNavLink>
|
</BreadcrumbNavLink>
|
||||||
<ChartSection token={token} prices={prices} nativeCurrency={nativeCurrency} />
|
<ChartSection
|
||||||
|
token={tokenQueryData}
|
||||||
|
nativeCurrency={isNative ? nativeCurrency : undefined}
|
||||||
|
prices={prices}
|
||||||
|
/>
|
||||||
<StatsSection
|
<StatsSection
|
||||||
TVL={token.market?.totalValueLocked?.value}
|
TVL={tokenQueryData.market?.totalValueLocked?.value}
|
||||||
volume24H={token.market?.volume24H?.value}
|
volume24H={tokenQueryData.market?.volume24H?.value}
|
||||||
priceHigh52W={token.market?.priceHigh52W?.value}
|
priceHigh52W={tokenQueryData.market?.priceHigh52W?.value}
|
||||||
priceLow52W={token.market?.priceLow52W?.value}
|
priceLow52W={tokenQueryData.market?.priceLow52W?.value}
|
||||||
/>
|
/>
|
||||||
<AboutSection
|
<AboutSection
|
||||||
address={token.address ?? ''}
|
address={tokenQueryData.address ?? ''}
|
||||||
description={token.project?.description}
|
description={tokenQueryData.project?.description}
|
||||||
homepageUrl={token.project?.homepageUrl}
|
homepageUrl={tokenQueryData.project?.homepageUrl}
|
||||||
twitterName={token.project?.twitterName}
|
twitterName={tokenQueryData.project?.twitterName}
|
||||||
/>
|
/>
|
||||||
<AddressSection address={token.address ?? ''} />
|
<AddressSection address={tokenQueryData.address ?? ''} />
|
||||||
</LeftPanel>
|
</LeftPanel>
|
||||||
<RightPanel>
|
<RightPanel>
|
||||||
<Widget defaultToken={widgetToken} onReviewSwapClick={onReviewSwap} />
|
<Widget defaultToken={widgetToken} onReviewSwapClick={onReviewSwap} />
|
||||||
{tokenWarning && <TokenSafetyMessage tokenAddress={token.address ?? ''} warning={tokenWarning} />}
|
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenQueryData.address ?? ''} warning={tokenWarning} />}
|
||||||
<BalanceSummary address={token.address ?? ''} balance={balance} balanceUsd={balanceUsd} />
|
<BalanceSummary
|
||||||
</RightPanel>
|
tokenAmount={tokenBalance}
|
||||||
<Footer>
|
nativeCurrencyAmount={nativeCurrencyBalance}
|
||||||
<FooterBalanceSummary
|
isNative={isNative}
|
||||||
address={token.address ?? ''}
|
|
||||||
networkBalances={balancesByNetwork}
|
|
||||||
balance={balance}
|
|
||||||
balanceUsd={balanceUsd}
|
|
||||||
/>
|
/>
|
||||||
</Footer>
|
</RightPanel>
|
||||||
|
|
||||||
|
<MobileBalanceSummaryFooter
|
||||||
|
tokenAmount={tokenBalance}
|
||||||
|
nativeCurrencyAmount={nativeCurrencyBalance}
|
||||||
|
isNative={isNative}
|
||||||
|
/>
|
||||||
|
|
||||||
<TokenSafetyModal
|
<TokenSafetyModal
|
||||||
isOpen={isBlockedToken || !!continueSwap}
|
isOpen={isBlockedToken || !!continueSwap}
|
||||||
tokenAddress={token.address}
|
tokenAddress={tokenQueryData.address}
|
||||||
onContinue={() => onResolveSwap(true)}
|
onContinue={() => onResolveSwap(true)}
|
||||||
onBlocked={() => navigate(-1)}
|
onBlocked={() => navigate(-1)}
|
||||||
onCancel={() => onResolveSwap(false)}
|
onCancel={() => onResolveSwap(false)}
|
||||||
|
Loading…
Reference in New Issue
Block a user