Commit 22b26de7 authored by Jordan Frankfurt's avatar Jordan Frankfurt Committed by GitHub

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
parent 53b57879
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> &nbsp;{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>
) )
} }
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() { ...@@ -158,7 +158,6 @@ export function LoadingTokenDetails() {
<RightPanel> <RightPanel>
<WidgetSkeleton /> <WidgetSkeleton />
</RightPanel> </RightPanel>
<Footer />
</TokenDetailsLayout> </TokenDetailsLayout>
) )
} }
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useToken } from 'hooks/Tokens' import { formatToDecimal } from 'analytics/utils'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances' import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useState } from 'react'
import { AlertTriangle } from 'react-feather'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants' import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
import { LoadingBubble } from '../loading' import { BalanceSummaryProps } from './BalanceSummary'
const PLACEHOLDER_NAV_FOOTER_HEIGHT = '56px' const Wrapper = styled.div`
const BalanceFooter = styled.div`
height: fit-content; height: fit-content;
border: 1px solid ${({ theme }) => theme.backgroundOutline}; border: 1px solid ${({ theme }) => theme.backgroundOutline};
background-color: ${({ theme }) => theme.backgroundSurface}; background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px 20px 0px 0px; border-radius: 20px 20px 0px 0px;
display: flex;
padding: 12px 16px; padding: 12px 16px;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
...@@ -23,10 +22,17 @@ const BalanceFooter = styled.div` ...@@ -23,10 +22,17 @@ const BalanceFooter = styled.div`
color: ${({ theme }) => theme.textSecondary}; color: ${({ theme }) => theme.textSecondary};
position: fixed; position: fixed;
left: 0; left: 0;
bottom: ${PLACEHOLDER_NAV_FOOTER_HEIGHT}; bottom: 56px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-content: center; 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` const BalanceValue = styled.div`
font-size: 20px; font-size: 20px;
...@@ -45,17 +51,6 @@ const BalanceInfo = styled.div` ...@@ -45,17 +51,6 @@ const BalanceInfo = styled.div`
justify-content: flex-start; justify-content: flex-start;
flex-direction: column; 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` const FiatValue = styled.span`
display: flex; display: flex;
align-self: flex-end; align-self: flex-end;
...@@ -66,18 +61,6 @@ const FiatValue = styled.span` ...@@ -66,18 +61,6 @@ const FiatValue = styled.span`
line-height: 16px; 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` const SwapButton = styled.button`
background-color: ${({ theme }) => theme.accentAction}; background-color: ${({ theme }) => theme.accentAction};
border-radius: 12px; border-radius: 12px;
...@@ -98,102 +81,63 @@ const TotalBalancesSection = styled.div` ...@@ -98,102 +81,63 @@ const TotalBalancesSection = styled.div`
justify-content: space-between; justify-content: space-between;
align-items: center; 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({ export default function MobileBalanceSummaryFooter({
address, tokenAmount,
networkBalances, nativeCurrencyAmount,
balance, isNative,
balanceUsd, }: BalanceSummaryProps) {
}: { const balanceUsdValue = useStablecoinValue(tokenAmount)?.toFixed(2)
address: string const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)?.toFixed(2)
networkBalances: (JSX.Element | null)[] | null
balance?: number const formattedBalance = tokenAmount
balanceUsd?: number ? formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2))
}) { : undefined
const tokenSymbol = useToken(address)?.symbol
const [showMultipleBalances, setShowMultipleBalances] = useState(false) const balanceUsd = balanceUsdValue ? parseFloat(balanceUsdValue) : undefined
const multipleBalances = false // for testing purposes
const networkNameIfOneBalance = 'Ethereum' // for testing purposes const formattedNativeBalance = nativeCurrencyAmount
const { loading, error } = useNetworkTokenBalances({ address }) ? 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 ( return (
<BalanceFooter> <Wrapper>
<TotalBalancesSection> <TotalBalancesSection>
{loading ? ( {Boolean(formattedBalance !== undefined && !isNative) && (
<LoadingState> <BalanceInfo>
<TopBalanceLoadBubble></TopBalanceLoadBubble> <Trans>Your {tokenAmount?.currency?.symbol} balance</Trans>
<BottomBalanceLoadBubble></BottomBalanceLoadBubble> <BalanceTotal>
</LoadingState> <BalanceValue>
) : error ? ( {formattedBalance} {tokenAmount?.currency?.symbol}
<ErrorState> </BalanceValue>
<AlertTriangle size={17} /> <FiatValue>{formatDollarAmount(balanceUsd)}</FiatValue>
<ErrorText> </BalanceTotal>
<Trans>There was an error fetching your balance</Trans> </BalanceInfo>
</ErrorText> )}
</ErrorState> {isNative && (
) : ( <BalanceInfo>
!!balance && <Trans>Your {nativeCurrencyAmount?.currency?.symbol} balance</Trans>
!!balanceUsd && ( <BalanceTotal>
<BalanceInfo> <BalanceValue>
{multipleBalances ? 'Balance on all networks' : `Your balance on ${networkNameIfOneBalance}`} {formattedNativeBalance} {nativeCurrencyAmount?.currency?.symbol}
<BalanceTotal> </BalanceValue>
<BalanceValue> <FiatValue>{formatDollarAmount(nativeBalanceUsd)}</FiatValue>
{balance} {tokenSymbol} </BalanceTotal>
</BalanceValue> </BalanceInfo>
<FiatValue>{`$${balanceUsd}`}</FiatValue>
</BalanceTotal>
{multipleBalances && (
<ViewAll onClick={() => setShowMultipleBalances(!showMultipleBalances)}>
<Trans>{showMultipleBalances ? 'Hide' : 'View'} all balances</Trans>
</ViewAll>
)}
</BalanceInfo>
)
)} )}
<Link to={`/swap?outputCurrency=${address}`}> <Link to={`/swap?outputCurrency=${outputTokenAddress}`}>
<SwapButton> <SwapButton>
<Trans>Swap</Trans> <Trans>Swap</Trans>
</SwapButton> </SwapButton>
</Link> </Link>
</TotalBalancesSection> </TotalBalancesSection>
{showMultipleBalances && ( </Wrapper>
<NetworkBalancesSection>
<NetworkBalancesLabel>
<Trans>Your balances by network</Trans>
</NetworkBalancesLabel>
{networkBalances}
</NetworkBalancesSection>
)}
<FakeFooterNavBar>**leaving space for updated nav footer**</FakeFooterNavBar>
</BalanceFooter>
) )
} }
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}&nbsp;{tokenSymbol}
</BalanceItem>
<BalanceItem>${fiatValue}</BalanceItem>
</BalanceRow>
<Network color={networkColor ?? theme.textPrimary}>{label}</Network>
</Balance>
</NetworkBalanceContainer>
)
}
...@@ -47,6 +47,9 @@ class TokenSafetyLookupTable { ...@@ -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 { ...@@ -6,4 +6,5 @@ export enum FeatureFlag {
tokens = 'tokens', tokens = 'tokens',
tokenSafety = 'tokenSafety', tokenSafety = 'tokenSafety',
traceJsonRpc = 'traceJsonRpc', traceJsonRpc = 'traceJsonRpc',
multiNetworkBalances = 'multiNetworkBalances',
} }
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 }
}
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,
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment