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 { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { formatToDecimal } from 'analytics/utils'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import styled from 'styled-components/macro'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
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};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
border-radius: 16px;
|
||||
`
|
||||
const ErrorState = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`
|
||||
const ErrorText = styled.span`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
height: fit-content;
|
||||
line-height: 16px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
|
||||
// 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`
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
`
|
||||
const TotalBalance = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
font-size: 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 28px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
`
|
||||
const TotalBalanceItem = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
export default function BalanceSummary({
|
||||
address,
|
||||
balance,
|
||||
balanceUsd,
|
||||
}: {
|
||||
address: string
|
||||
balance?: number
|
||||
balanceUsd?: number
|
||||
}) {
|
||||
const token = useToken(address)
|
||||
const { loading, error } = useNetworkTokenBalances({ address })
|
||||
export interface BalanceSummaryProps {
|
||||
tokenAmount: CurrencyAmount<Token> | undefined
|
||||
nativeCurrencyAmount: CurrencyAmount<Currency> | undefined
|
||||
isNative: boolean
|
||||
}
|
||||
|
||||
export default function BalanceSummary({ tokenAmount, nativeCurrencyAmount, isNative }: BalanceSummaryProps) {
|
||||
const balanceUsdValue = useStablecoinValue(tokenAmount)?.toFixed(2)
|
||||
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)?.toFixed(2)
|
||||
|
||||
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 (
|
||||
<BalancesCard>
|
||||
{error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={24} />
|
||||
<ErrorText>
|
||||
<Trans>There was an error loading your {token?.symbol} balance</Trans>
|
||||
</ErrorText>
|
||||
</ErrorState>
|
||||
) : (
|
||||
<>
|
||||
<TotalBalanceSection>
|
||||
Your balance
|
||||
<TotalBalance>
|
||||
<TotalBalanceItem>{`${balance} ${token?.symbol}`}</TotalBalanceItem>
|
||||
<TotalBalanceItem>{`$${balanceUsd}`}</TotalBalanceItem>
|
||||
</TotalBalance>
|
||||
</TotalBalanceSection>
|
||||
</>
|
||||
)}
|
||||
<TotalBalanceSection>
|
||||
<Trans>Your balance</Trans>
|
||||
{currencies.map(({ currency, formattedBalance, formattedUSDValue }) => (
|
||||
<TotalBalance key={currency.wrapped.address}>
|
||||
<TotalBalanceItem>
|
||||
<CurrencyLogo currency={currency} />
|
||||
{formattedBalance} {currency?.symbol}
|
||||
</TotalBalanceItem>
|
||||
<TotalBalanceItem>
|
||||
{formatDollarAmount(formattedUSDValue === 0 ? undefined : formattedUSDValue)}
|
||||
</TotalBalanceItem>
|
||||
</TotalBalance>
|
||||
))}
|
||||
</TotalBalanceSection>
|
||||
</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 { Footer, LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
||||
import { LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { LoadingBubble } from '../loading'
|
||||
@ -158,7 +158,6 @@ export function LoadingTokenDetails() {
|
||||
<RightPanel>
|
||||
<WidgetSkeleton />
|
||||
</RightPanel>
|
||||
<Footer />
|
||||
</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) {
|
||||
this.dict = this.createMap()
|
||||
}
|
||||
if (address === 'native') {
|
||||
return TOKEN_LIST_TYPES.UNI_DEFAULT
|
||||
}
|
||||
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ export enum FeatureFlag {
|
||||
tokens = 'tokens',
|
||||
tokenSafety = 'tokenSafety',
|
||||
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 { formatToDecimal } from 'analytics/utils'
|
||||
import {
|
||||
LARGE_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 { BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
|
||||
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
|
||||
import FooterBalanceSummary from 'components/Tokens/TokenDetails/FooterBalanceSummary'
|
||||
import NetworkBalance from 'components/Tokens/TokenDetails/NetworkBalance'
|
||||
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
|
||||
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
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 { checkWarning } from 'constants/tokenSafety'
|
||||
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 { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
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 { ArrowLeft } from 'react-feather'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
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`
|
||||
display: flex;
|
||||
gap: 60px;
|
||||
@ -91,48 +79,60 @@ export const RightPanel = styled.div`
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
function NetworkBalances(tokenAddress: string | undefined) {
|
||||
return useNetworkTokenBalances({ address: tokenAddress })
|
||||
}
|
||||
|
||||
export default function TokenDetails() {
|
||||
const { tokenAddress: tokenAddressParam, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[validateUrlChainParam(chainName)]
|
||||
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 { account } = useWeb3React()
|
||||
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 switchChains = useCallback(
|
||||
(newChain: Chain) => {
|
||||
const chainSegment = newChain.toLowerCase()
|
||||
if (tokenAddressParam === 'NATIVE') {
|
||||
if (isNative) {
|
||||
navigate(`/tokens/${chainSegment}/NATIVE`)
|
||||
} else {
|
||||
token?.project?.tokens?.forEach((token) => {
|
||||
tokenQueryData?.project?.tokens?.forEach((token) => {
|
||||
if (token.chain === newChain && token.address) {
|
||||
navigate(`/tokens/${chainSegment}/${token.address}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
[navigate, token?.project?.tokens, tokenAddressParam]
|
||||
[isNative, navigate, tokenQueryData?.project?.tokens]
|
||||
)
|
||||
useOnGlobalChainSwitch(switchChains)
|
||||
|
||||
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
|
||||
const onReviewSwap = useCallback(
|
||||
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
|
||||
@ -147,102 +147,63 @@ export default function TokenDetails() {
|
||||
[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 currentChainId = CHAIN_NAME_TO_CHAIN_ID[currentChainName]
|
||||
// The widget is not yet configured to use Celo.
|
||||
if (isCelo(chainId) || isCelo(currentChainId)) return undefined
|
||||
|
||||
return (
|
||||
nativeCurrency ??
|
||||
(token?.address && token.symbol && token.name
|
||||
? new Token(currentChainId, token.address, 18, token.symbol, token.name)
|
||||
: undefined)
|
||||
)
|
||||
}, [chainId, currentChainName, nativeCurrency, token?.address, token?.name, token?.symbol])
|
||||
if (pageToken) {
|
||||
return pageToken
|
||||
}
|
||||
if (nativeCurrency) {
|
||||
if (isCelo(pageChainId)) return undefined
|
||||
return nativeCurrency
|
||||
}
|
||||
return undefined
|
||||
}, [nativeCurrency, pageChainId, pageToken])
|
||||
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
{token && (
|
||||
{tokenQueryData && (
|
||||
<>
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to={`/tokens/${chainName}`}>
|
||||
<ArrowLeft size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<ChartSection token={token} prices={prices} nativeCurrency={nativeCurrency} />
|
||||
<ChartSection
|
||||
token={tokenQueryData}
|
||||
nativeCurrency={isNative ? nativeCurrency : undefined}
|
||||
prices={prices}
|
||||
/>
|
||||
<StatsSection
|
||||
TVL={token.market?.totalValueLocked?.value}
|
||||
volume24H={token.market?.volume24H?.value}
|
||||
priceHigh52W={token.market?.priceHigh52W?.value}
|
||||
priceLow52W={token.market?.priceLow52W?.value}
|
||||
TVL={tokenQueryData.market?.totalValueLocked?.value}
|
||||
volume24H={tokenQueryData.market?.volume24H?.value}
|
||||
priceHigh52W={tokenQueryData.market?.priceHigh52W?.value}
|
||||
priceLow52W={tokenQueryData.market?.priceLow52W?.value}
|
||||
/>
|
||||
<AboutSection
|
||||
address={token.address ?? ''}
|
||||
description={token.project?.description}
|
||||
homepageUrl={token.project?.homepageUrl}
|
||||
twitterName={token.project?.twitterName}
|
||||
address={tokenQueryData.address ?? ''}
|
||||
description={tokenQueryData.project?.description}
|
||||
homepageUrl={tokenQueryData.project?.homepageUrl}
|
||||
twitterName={tokenQueryData.project?.twitterName}
|
||||
/>
|
||||
<AddressSection address={token.address ?? ''} />
|
||||
<AddressSection address={tokenQueryData.address ?? ''} />
|
||||
</LeftPanel>
|
||||
<RightPanel>
|
||||
<Widget defaultToken={widgetToken} onReviewSwapClick={onReviewSwap} />
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={token.address ?? ''} warning={tokenWarning} />}
|
||||
<BalanceSummary address={token.address ?? ''} balance={balance} balanceUsd={balanceUsd} />
|
||||
</RightPanel>
|
||||
<Footer>
|
||||
<FooterBalanceSummary
|
||||
address={token.address ?? ''}
|
||||
networkBalances={balancesByNetwork}
|
||||
balance={balance}
|
||||
balanceUsd={balanceUsd}
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenQueryData.address ?? ''} warning={tokenWarning} />}
|
||||
<BalanceSummary
|
||||
tokenAmount={tokenBalance}
|
||||
nativeCurrencyAmount={nativeCurrencyBalance}
|
||||
isNative={isNative}
|
||||
/>
|
||||
</Footer>
|
||||
</RightPanel>
|
||||
|
||||
<MobileBalanceSummaryFooter
|
||||
tokenAmount={tokenBalance}
|
||||
nativeCurrencyAmount={nativeCurrencyBalance}
|
||||
isNative={isNative}
|
||||
/>
|
||||
|
||||
<TokenSafetyModal
|
||||
isOpen={isBlockedToken || !!continueSwap}
|
||||
tokenAddress={token.address}
|
||||
tokenAddress={tokenQueryData.address}
|
||||
onContinue={() => onResolveSwap(true)}
|
||||
onBlocked={() => navigate(-1)}
|
||||
onCancel={() => onResolveSwap(false)}
|
||||
|
Loading…
Reference in New Issue
Block a user