diff --git a/src/const.ts b/src/const.ts index 1ecba5dcc..80dd0f5bf 100644 --- a/src/const.ts +++ b/src/const.ts @@ -21,6 +21,7 @@ import { Network } from 'types' import { DisabledTokensMaps, TokenOverride, AddressToOverrideMap } from 'types/config' export const BATCH_TIME_IN_MS = BATCH_TIME * 1000 +export const DEFAULT_TIMEOUT = 5000 export const ZERO_BIG_NUMBER = new BigNumber(0) export const ONE_BIG_NUMBER = new BigNumber(1) diff --git a/src/hooks/useTokenBalances.ts b/src/hooks/useTokenBalances.ts index cd94a2f2b..9bc271521 100644 --- a/src/hooks/useTokenBalances.ts +++ b/src/hooks/useTokenBalances.ts @@ -7,7 +7,7 @@ import { erc20Api, depositApi } from 'api' import useSafeState from './useSafeState' import { useWalletConnection } from './useWalletConnection' -import { formatSmart, logDebug, isTokenEnabled } from 'utils' +import { formatSmart, logDebug, isTokenEnabled, timeout, notEmpty } from 'utils' import { TokenBalanceDetails, TokenDetails } from 'types' import { WalletInfo } from 'api/wallet/WalletApi' import { PendingFlux } from 'api/deposit/DepositApi' @@ -34,6 +34,7 @@ async function fetchBalancesForToken( networkId: number, ): Promise { const tokenAddress = token.address + const [ exchangeBalance, pendingDeposit, @@ -83,29 +84,40 @@ async function _getBalances(walletInfo: WalletInfo, tokens: TokenDetails[]): Pro const contractAddress = depositApi.getContractAddress(networkId) assert(contractAddress, 'No valid contract address found. Stopping.') - const balancePromises: Promise[] = tokens.map((token) => - fetchBalancesForToken(token, userAddress, contractAddress, networkId) - .then((balance) => { - const cacheKey = constructCacheKey({ token, userAddress, contractAddress, networkId }) + const balancePromises: Promise[] = tokens.map((token) => { + const cacheKey = constructCacheKey({ token, userAddress, contractAddress, networkId }) + + // timoutPromise == Promise, correctly determined to always throw + const timeoutPromise = timeout({ + timeoutErrorMsg: 'Timeout fetching balances for ' + token.address, + }) + + const fetchBalancesPromise = fetchBalancesForToken(token, userAddress, contractAddress, networkId).then( + (balance) => { balanceCache[cacheKey] = balance return balance - }) - .catch((e) => { - console.error('[useTokenBalances] Error for', token, userAddress, contractAddress, e) + }, + ) - const cacheKey = constructCacheKey({ token, userAddress, contractAddress, networkId }) + // balancePromise == Promise == Promise or throws + const balancePromise = Promise.race([fetchBalancesPromise, timeoutPromise]) - const cachedValue = balanceCache[cacheKey] - if (cachedValue) { - logDebug('Using cached value for', token, userAddress, contractAddress) - return cachedValue - } + return balancePromise.catch((e) => { + console.error('[useTokenBalances] Error for', token, userAddress, contractAddress, e) + const cachedValue = balanceCache[cacheKey] + if (cachedValue) { + logDebug('Using cached value for', token, userAddress, contractAddress) + return cachedValue + } + + return null + }) + }) - return null - }), - ) const balances = await Promise.all(balancePromises) - return balances.filter(Boolean) as TokenBalanceDetails[] + + // TODO: Would be better to show the errored tokens in error state + return balances.filter(notEmpty) } export const useTokenBalances = (passOnParams: Partial = {}): UseBalanceResult => { diff --git a/src/utils/miscellaneous.ts b/src/utils/miscellaneous.ts index 107001f41..e961fa363 100644 --- a/src/utils/miscellaneous.ts +++ b/src/utils/miscellaneous.ts @@ -5,7 +5,7 @@ import { TokenDetails, Unpromise } from 'types' import { AssertionError } from 'assert' import { AuctionElement, Trade, Order } from 'api/exchange/ExchangeApi' import { batchIdToDate } from './time' -import { ORDER_FILLED_FACTOR, MINIMUM_ALLOWANCE_DECIMALS } from 'const' +import { ORDER_FILLED_FACTOR, MINIMUM_ALLOWANCE_DECIMALS, DEFAULT_TIMEOUT } from 'const' import { TEN, ZERO } from '@gnosis.pm/dex-js' export function assertNonNull(val: T, message: string): asserts val is NonNullable { @@ -72,7 +72,8 @@ export function getToken( return token } -export const delay = (ms = 100, result?: T): Promise => new Promise((resolve) => setTimeout(resolve, ms, result)) +export const delay = (ms = 100, result?: T): Promise => + new Promise((resolve) => setTimeout(resolve, ms, result)) /** * Uses images from https://github.com/trustwallet/tokens @@ -210,3 +211,21 @@ export function notEmpty(value: TValue | null | undefined): value is TVa } export const isNonZeroNumber = (value?: string | number): boolean => !!value && !!+value + +export interface TimeoutParams { + time?: number + result?: T + timeoutErrorMsg?: string +} + +export function timeout(params: TimeoutParams): Promise // never means function throws +export function timeout(params: TimeoutParams): Promise +export async function timeout(params: TimeoutParams): Promise { + const { time = DEFAULT_TIMEOUT, result, timeoutErrorMsg: timeoutMsg = 'Timeout' } = params + + await delay(time) + // provided defined result -- return it + if (result !== undefined) return result + // no defined result -- throw message + throw new Error(timeoutMsg) +}