diff --git a/.eslintrc.js b/.eslintrc.js index 0f39addd6..14952dd17 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { parser: '@typescript-eslint/parser', // Specifies the ESLint parser extends: [ + 'eslint:recommended', 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier @@ -44,4 +45,9 @@ module.exports = { globals: { VERSION: true, }, + env: { + browser: true, + node: true, + jest: true, + }, } diff --git a/package-lock.json b/package-lock.json index 044563564..9e8a86f0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@gnosis.pm/dex-react", - "version": "1.5.0-0", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a282bd1ba..40a6794ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gnosis.pm/dex-react", - "version": "1.5.0-0", + "version": "1.5.0", "description": "", "main": "src/index.js", "sideEffects": false, diff --git a/src/api/index.ts b/src/api/index.ts index 2499e50b5..9802e3eee 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -167,10 +167,11 @@ function createTcrApi(web3: Web3): TcrApi | undefined { case 'none': tcrApi = undefined break - case 'multi-tcr': + case 'multi-tcr': { const multiTcrApiConfig = CONFIG.tcr tcrApi = new MultiTcrApiProxy({ web3, ...multiTcrApiConfig.config }) break + } default: throw new Error('Unknown implementation for DexPriceEstimatorApi: ' + type) diff --git a/src/api/tokenList/TokenListApi.ts b/src/api/tokenList/TokenListApi.ts index 211efb5f4..dacdca5ce 100644 --- a/src/api/tokenList/TokenListApi.ts +++ b/src/api/tokenList/TokenListApi.ts @@ -1,4 +1,4 @@ -import { TokenDetails } from 'types' +import { Network, TokenDetails } from 'types' import { getTokensByNetwork } from './tokenList' import { logDebug } from 'utils' import GenericSubscriptions, { SubscriptionsInterface } from './Subscriptions' @@ -20,6 +20,9 @@ const addOverrideToDisabledTokens = (networkId: number) => (token: TokenDetails) } export interface TokenList extends SubscriptionsInterface { + setListReady: (state: boolean, networkId?: Network) => void + getIsListReady: () => boolean + isListReady: boolean getTokens: (networkId: number) => TokenDetails[] addToken: (params: AddTokenParams) => void addTokens: (params: AddTokensParams) => void @@ -64,6 +67,10 @@ export class TokenListApiImpl extends GenericSubscriptions imple private _tokensByNetwork: { [networkId: number]: TokenDetails[] } private _tokenAddressNetworkSet: Set + // token list flag - prevents stale/incorrect data + // from being presented during token list calculation + public isListReady = true + public constructor({ networkIds, initialTokenList }: TokenListApiParams) { super() @@ -93,6 +100,17 @@ export class TokenListApiImpl extends GenericSubscriptions imple }) } + public setListReady(state: boolean, networkId?: Network): void { + this.isListReady = state + if (this.isListReady && networkId) { + this.triggerSubscriptions(this._tokensByNetwork[networkId]) + } + } + + public getIsListReady(): boolean { + return this.isListReady + } + public hasToken(params: HasTokenParams): boolean { return this._tokenAddressNetworkSet.has(TokenListApiImpl.constructAddressNetworkKey(params)) } diff --git a/src/api/tokenList/TokenListApiMock.ts b/src/api/tokenList/TokenListApiMock.ts index f76f2249a..9de17d3fe 100644 --- a/src/api/tokenList/TokenListApiMock.ts +++ b/src/api/tokenList/TokenListApiMock.ts @@ -5,12 +5,18 @@ import GenericSubscriptions from './Subscriptions' export class TokenListApiMock extends GenericSubscriptions implements TokenList { private _tokenList: TokenDetails[] + public isListReady = false + public constructor(tokenList: TokenDetails[]) { super() this._tokenList = tokenList } + public getIsListReady(): boolean { + return this.isListReady + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars public getTokens(_networkId: number): TokenDetails[] { return this._tokenList @@ -34,6 +40,10 @@ export class TokenListApiMock extends GenericSubscriptions imple this._tokenList = tokenList this.triggerSubscriptions(tokenList) } + + public setListReady(state: boolean): void { + this.isListReady = state + } } export default TokenListApiMock diff --git a/src/api/wallet/WalletApi.ts b/src/api/wallet/WalletApi.ts index 382be51e3..0793ff765 100644 --- a/src/api/wallet/WalletApi.ts +++ b/src/api/wallet/WalletApi.ts @@ -67,7 +67,7 @@ export interface WalletApi { export interface WalletInfo { isConnected: boolean userAddress?: string - networkId?: number + networkId?: Network blockNumber?: number } @@ -338,6 +338,7 @@ export class WalletApiImpl implements WalletApi { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore provider.handleReadRequests = async function (payload: unknown): Promise { + if (payload && typeof payload === 'object' && 'skipCache' in payload) delete payload['skipCache'] // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (!this.http) { diff --git a/src/components/OrderBookBtn.tsx b/src/components/OrderBookBtn.tsx index 4a2abbd4c..49b379eca 100644 --- a/src/components/OrderBookBtn.tsx +++ b/src/components/OrderBookBtn.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import styled from 'styled-components' import Modal, { useModal } from 'components/common/Modal' @@ -115,19 +115,18 @@ export const OrderBookBtn: React.FC = (props: OrderBookBtnPro const { baseToken: baseTokenDefault, quoteToken: quoteTokenDefault, label, className } = props const { networkIdOrDefault: networkId } = useWalletConnection() // get all tokens - const tokenList = useTokenList({ networkId }) + const { tokens: tokenList } = useTokenList({ networkId }) const [baseToken, setBaseToken] = useSafeState(baseTokenDefault) const [quoteToken, setQuoteToken] = useSafeState(quoteTokenDefault) const networkDescription = networkId !== Network.Mainnet ? ` (${getNetworkFromId(networkId)})` : '' - // Update if any of the base tokens change - useEffect(() => { - setBaseToken(baseTokenDefault) - setQuoteToken(quoteTokenDefault) - }, [baseTokenDefault, quoteTokenDefault, setBaseToken, setQuoteToken]) - const [modalHook, toggleModal] = useModal({ ...DEFAULT_MODAL_OPTIONS, + onShow: () => { + // Update if any of the base tokens change + setBaseToken(baseTokenDefault) + setQuoteToken(quoteTokenDefault) + }, onHide: () => { // Reset the selection on close setBaseToken(baseTokenDefault) diff --git a/src/components/PoolingWidget/index.tsx b/src/components/PoolingWidget/index.tsx index 3bac93b90..489929ade 100644 --- a/src/components/PoolingWidget/index.tsx +++ b/src/components/PoolingWidget/index.tsx @@ -151,7 +151,7 @@ const PoolingInterface: React.FC = () => { const { networkId, networkIdOrDefault, userAddress } = useWalletConnection() // Get all the tokens for the current network - const tokenList = useTokenList({ networkId: networkIdOrDefault }) + const { tokens: tokenList } = useTokenList({ networkId: networkIdOrDefault }) const tokens = useMemo(() => { return ( diff --git a/src/components/Settings/WalletConnect.tsx b/src/components/Settings/WalletConnect.tsx index d01bc0244..58270db76 100644 --- a/src/components/Settings/WalletConnect.tsx +++ b/src/components/Settings/WalletConnect.tsx @@ -25,9 +25,11 @@ const WCSettingsSchema = Joi.object({ rpc: Joi.object({ mainnet: RPCSchema, rinkeby: RPCSchema, + xDAI: RPCSchema, }).empty({ mainnet: '', rinkeby: '', + xDAI: '', }), }) .oxor('infuraId', 'rpc') @@ -192,7 +194,7 @@ export const WCSettings: React.FC = ({ register, errors }) => {

Here you can set the InfuraId or RPC URL that will be used for connecting - the WalletConnect provider to Mainnet and/or Rinkeby. It is also possible to set a custom WalletConnect{' '} + the WalletConnect provider to Mainnet, Rinkeby and/or xDAI. It is also possible to set a custom WalletConnect{' '} Bridge URL to use instead of the default one.

@@ -240,6 +242,18 @@ export const WCSettings: React.FC = ({ register, errors }) => { RINKEBY + + + + xDAI + + diff --git a/src/components/TokenOptionItem.tsx b/src/components/TokenOptionItem.tsx index 9810b48f7..c26de0647 100644 --- a/src/components/TokenOptionItem.tsx +++ b/src/components/TokenOptionItem.tsx @@ -176,7 +176,7 @@ const generateMessage = ({ case TokenFromExchange.NOT_ERC20: return <>Not a valid ERC20 token // registered but not in list --> option to add - case TokenFromExchange.NOT_IN_TOKEN_LIST: + case TokenFromExchange.NOT_IN_TOKEN_LIST: { if (!token || !('id' in token)) return <>{defaultText} const handleAddToken: React.MouseEventHandler = async (e) => { @@ -201,6 +201,7 @@ const generateMessage = ({ ) + } default: return <>{defaultText} } diff --git a/src/components/TradeWidget/index.tsx b/src/components/TradeWidget/index.tsx index 63d3fd5b9..385b3bbcd 100644 --- a/src/components/TradeWidget/index.tsx +++ b/src/components/TradeWidget/index.tsx @@ -152,7 +152,7 @@ const TradeWidgetContainer: React.FC = () => { const sellTokenSymbol = decodeSymbol(encodedSellTokenSymbol || '') const receiveTokenSymbol = decodeSymbol(decodeReceiveTokenSymbol || '') - const { sellToken: initialSellTokenDefaultNetwork, buyToken: initialReceiveTokenDefaultNetwork } = + const { sellToken: initialSellTokenDefaultNetwork, receiveToken: initialReceiveTokenDefaultNetwork } = initialTokenSelection.networks[networkIdOrDefault] || {} const sellTokenWithFallback = useMemo( diff --git a/src/components/TradesWidget/TradeRow.tsx b/src/components/TradesWidget/TradeRow.tsx index 1fc3e2d35..e9c253853 100644 --- a/src/components/TradesWidget/TradeRow.tsx +++ b/src/components/TradesWidget/TradeRow.tsx @@ -114,6 +114,7 @@ export const TradeRow: React.FC = (params) => { return `${fillPercentage} matched out of ${orderAmount} ${displayTokenSymbolOrLink(sellToken)}` } } + // falls through case 'liquidity': case 'unknown': default: diff --git a/src/components/UserWallet/UserWallet.styled.ts b/src/components/UserWallet/UserWallet.styled.ts index f73396b20..8fa4f77f4 100644 --- a/src/components/UserWallet/UserWallet.styled.ts +++ b/src/components/UserWallet/UserWallet.styled.ts @@ -254,7 +254,7 @@ export const NetworkTitle = styled.div` font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.3rem; - top: -0.6rem; + top: -0.8rem; white-space: nowrap; ` diff --git a/src/hooks/useManageTokens.tsx b/src/hooks/useManageTokens.tsx index 4dfcf59d8..19818d1d2 100644 --- a/src/hooks/useManageTokens.tsx +++ b/src/hooks/useManageTokens.tsx @@ -148,7 +148,7 @@ const SearchInput: React.FC = (props) => { const ManageTokensContainer: React.FC = () => { const { networkId, networkIdOrDefault } = useWalletConnection() // get all tokens - const tokens = useTokenList({ networkId }) + const { tokens } = useTokenList({ networkId }) const [search, setSearch] = useState('') const { value: debouncedSearch, setImmediate: setDebouncedSearch } = useDebounce(search, 500) diff --git a/src/hooks/useOrders.ts b/src/hooks/useOrders.ts index 85d8c7fe5..32e628221 100644 --- a/src/hooks/useOrders.ts +++ b/src/hooks/useOrders.ts @@ -11,6 +11,7 @@ import useGlobalState from './useGlobalState' import { useWalletConnection } from './useWalletConnection' import usePendingOrders from './usePendingOrders' import { useCheckWhenTimeRemainingInBatch } from './useTimeRemainingInBatch' +import { useTokenList } from './useTokenList' // Constants/Types import { REFRESH_WHEN_SECONDS_LEFT } from 'const' @@ -32,6 +33,12 @@ export function useOrders(): Result { dispatch, ] = useGlobalState() + // TODO: check this - currently in use for subscription + // to change in token list to trigger update of orders + // and the incorrect state shown sometimes when loading app from + // fresh state and seeing incorrect token list Issue #1486 + const { tokens, isListReady } = useTokenList({ networkId }) + // Pending Orders const pendingOrders = usePendingOrders() @@ -41,9 +48,11 @@ export function useOrders(): Result { useEffect(() => { // continue loading new orders // from current offset - setIsLoading(true) + // make sure token list is ready before setting + isListReady && setIsLoading(true) + // whenever new block is mined - }, [blockNumber, setIsLoading]) + }, [blockNumber, isListReady, setIsLoading]) useEffect(() => { let cancelled = false @@ -140,7 +149,8 @@ export function useOrders(): Result { } forceOrdersRefresh() dispatch(overwriteOrders([])) - }, [userAddress, networkId, forceOrdersRefresh, dispatch]) + // Subscribe to tokens change to force orders refresh + }, [tokens, userAddress, networkId, forceOrdersRefresh, dispatch]) return { orders, diff --git a/src/hooks/useTokenBalances.ts b/src/hooks/useTokenBalances.ts index 3a3276ce2..cd94a2f2b 100644 --- a/src/hooks/useTokenBalances.ts +++ b/src/hooks/useTokenBalances.ts @@ -114,7 +114,7 @@ export const useTokenBalances = (passOnParams: Partial = {}) const [error, setError] = useSafeState(false) // get all tokens, maybe without deprecated - const tokens = useTokenList({ networkId: walletInfo.networkId, ...passOnParams }) + const { tokens } = useTokenList({ networkId: walletInfo.networkId, ...passOnParams }) // Get token balances useEffect(() => { diff --git a/src/hooks/useTokenList.ts b/src/hooks/useTokenList.ts index 6e1f2f2c7..f2c0922af 100644 --- a/src/hooks/useTokenList.ts +++ b/src/hooks/useTokenList.ts @@ -3,20 +3,25 @@ import { TokenDetails } from 'types' import useSafeState from './useSafeState' import { getTokens, subscribeToTokenList } from 'services' import { EMPTY_ARRAY } from 'const' +import { tokenListApi } from 'api' export interface UseTokenListParams { networkId?: number excludeDeprecated?: boolean } -export const useTokenList = ({ networkId, excludeDeprecated }: UseTokenListParams = {}): TokenDetails[] => { +export const useTokenList = ({ networkId, excludeDeprecated }: UseTokenListParams = {}): { + tokens: TokenDetails[] + isListReady: boolean +} => { // sync get tokenList const unfilteredTokens = networkId === undefined ? EMPTY_ARRAY : getTokens(networkId) const tokens = useMemo(() => { if (!excludeDeprecated) return unfilteredTokens + const filteredList = unfilteredTokens.filter((token) => !token.disabled) - return unfilteredTokens.filter((token) => !token.disabled) + return filteredList }, [excludeDeprecated, unfilteredTokens]) // force update with a new value each time @@ -26,5 +31,5 @@ export const useTokenList = ({ networkId, excludeDeprecated }: UseTokenListParam return subscribeToTokenList(() => forceUpdate({})) }, [forceUpdate]) - return tokens + return { tokens, isListReady: tokenListApi.getIsListReady() } } diff --git a/src/hooks/useWalletConnection.ts b/src/hooks/useWalletConnection.ts index fdef07e59..8d837ccb7 100644 --- a/src/hooks/useWalletConnection.ts +++ b/src/hooks/useWalletConnection.ts @@ -6,7 +6,7 @@ import { BlockchainUpdatePrompt, WalletInfo } from 'api/wallet/WalletApi' interface PendingStateObject extends WalletInfo { pending: true - networkIdOrDefault: number + networkIdOrDefault: Network } const PendingState: PendingStateObject = { @@ -33,7 +33,7 @@ const constructPendingState = ({ chainId, account, blockHeader }: BlockchainUpda } export const useWalletConnection = (): - | (WalletInfo & { pending: false; networkIdOrDefault: number }) + | (WalletInfo & { pending: false; networkIdOrDefault: Network }) | PendingStateObject => { const [walletInfo, setWalletInfo] = useSafeState(null) diff --git a/src/pages/OrderBook.tsx b/src/pages/OrderBook.tsx index 69fc91f0e..5f367f745 100644 --- a/src/pages/OrderBook.tsx +++ b/src/pages/OrderBook.tsx @@ -81,7 +81,7 @@ const OrderBookWrapper = styled.div` const OrderBook: React.FC = () => { const { networkIdOrDefault } = useWalletConnection() // get all tokens - const tokenList = useTokenList({ networkId: networkIdOrDefault }) + const { tokens: tokenList } = useTokenList({ networkId: networkIdOrDefault }) const [baseToken, setBaseToken] = useSafeState(null) const [quoteToken, setQuoteToken] = useSafeState(null) const [hops, setHops] = useSafeState(ORDER_BOOK_HOPS_DEFAULT) diff --git a/src/reducers-actions/orders.ts b/src/reducers-actions/orders.ts index 7cba8d44a..400fdf293 100644 --- a/src/reducers-actions/orders.ts +++ b/src/reducers-actions/orders.ts @@ -114,12 +114,13 @@ export async function sideEffect(state: OrdersState, action: ReducerActionType): switch (action.type) { case 'OVERWRITE_ORDERS': case 'UPDATE_ORDERS': - case 'APPEND_ORDERS': + case 'APPEND_ORDERS': { const newTokenIdsFromOrders = new Set() // orders can contain many duplicated tokenIds state.orders.forEach(({ sellTokenId, buyTokenId }) => newTokenIdsFromOrders.add(sellTokenId).add(buyTokenId)) addUnlistedTokensToUserTokenListById(Array.from(newTokenIdsFromOrders)) + } } } diff --git a/src/reducers-actions/pendingOrders.ts b/src/reducers-actions/pendingOrders.ts index e5b679008..8756d0dc4 100644 --- a/src/reducers-actions/pendingOrders.ts +++ b/src/reducers-actions/pendingOrders.ts @@ -111,6 +111,7 @@ export const sideEffect = (state: PendingOrdersState, action: ReducerType): Pend case 'SAVE_PENDING_ORDERS': case 'REPLACE_PENDING_ORDERS': setStorageItem(STORAGE_PENDING_ORDER_KEY, state) + // falls through default: return state } diff --git a/src/reducers-actions/trades.ts b/src/reducers-actions/trades.ts index 3b08ef73d..acc78918f 100644 --- a/src/reducers-actions/trades.ts +++ b/src/reducers-actions/trades.ts @@ -134,7 +134,7 @@ function getPendingTrades(tradesByRevertKey: Map): Map() diff --git a/src/services/factories/getTokenFromExchange.ts b/src/services/factories/getTokenFromExchange.ts index fe56d1cac..3c06cb721 100644 --- a/src/services/factories/getTokenFromExchange.ts +++ b/src/services/factories/getTokenFromExchange.ts @@ -120,7 +120,14 @@ function getTokenFromExchangeByIdFactory( const { tokenListApi, exchangeApi, getTokenFromErc20 } = factoryParams return async ({ tokenId, networkId }: GetByIdParams): Promise => { + // Grab token list: either TCR or default token list + // if default token list is used, IDs need to be updated first + // or app will see strange data as wrong token IDs from config token list will be used const tokens = tokenListApi.getTokens(networkId) + // this will always return the initial config list first as it is injected + // early in the TokenListAPI constructor + // render 1: config list with incorrect IDs + // render 2: updated token list with correct IDs // Try from our current list of tokens, by id let token = getToken('id', tokenId, tokens) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 7522b61c4..f622693aa 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -173,57 +173,74 @@ export function getTokensFactory(factoryParams: { tokensConfig: TokenDetails[], ): Promise { const tokensConfigMap = new Map(tokensConfig.map((t) => [t.address, t])) - const tokenDetailsPromises: (Promise | TokenDetails)[] = [] - addressToIdMap.forEach((id, tokenAddress) => { + const tokenDetailsPromises: Promise[] = [] + + async function _fillToken(id: number, tokenAddress: string): Promise { // Resolve the details using the config, otherwise fetch the token - const token: undefined | Promise = tokensConfigMap.has(tokenAddress) - ? Promise.resolve(tokensConfigMap.get(tokenAddress)) - : _fetchToken(networkId, id, tokenAddress) + const token = tokensConfigMap.get(tokenAddress) || (await _fetchToken(networkId, id, tokenAddress)) if (token) { - // Add a label for convenience - token.then((token) => { - if (token) { - token.label = safeTokenName(token) - } - return token - }) - - tokenDetailsPromises.push(token) + token.label = safeTokenName(token) + // TODO: review this + // needs to be here to set correct IDs when using default initialTokenList + // from config as the IDs there are not correct + token.id = + addressToIdMap.get(token.address) !== token.id ? (addressToIdMap.get(token.address) as number) : token.id } + + return token + } + + addressToIdMap.forEach((id, tokenAddress) => { + tokenDetailsPromises.push(_fillToken(id, tokenAddress)) }) return (await Promise.all(tokenDetailsPromises)).filter(notEmpty) } - function _moveTokenToHeadOfArray(tokenAddress: string, tokenList: TokenDetails[]): TokenDetails[] { - const tokenIndex = tokenList.findIndex((t) => t.address === tokenAddress) - if (tokenIndex !== -1) { - const token = tokenList[tokenIndex] - tokenList.splice(tokenIndex, 1) - - return [token, ...tokenList] - } - - return tokenList - } + type TokenComparator = (a: TokenDetails, b: TokenDetails) => number - function _sortTokens(networkId: number, tokens: TokenDetails[]): TokenDetails[] { - // Sort tokens - let tokensSorted = tokens.sort(_tokenComparer) + function _createTokenComparator(networkId: number): TokenComparator { + let comparator: TokenComparator + // allows correct unicode comparison + const compareByLabel: TokenComparator = (a, b) => a.label.localeCompare(b.label) - // Make sure wxDAI and WETH are the first tokens switch (networkId) { case Network.Mainnet: - return _moveTokenToHeadOfArray(WETH_ADDRESS_MAINNET, tokensSorted) + comparator = (a, b): number => { + // WETH first + if (a.address === WETH_ADDRESS_MAINNET) return -1 + if (b.address === WETH_ADDRESS_MAINNET) return 1 + return compareByLabel(a, b) + } + break case Network.Rinkeby: - return _moveTokenToHeadOfArray(WETH_ADDRESS_RINKEBY, tokensSorted) + comparator = (a, b): number => { + // WETH first + if (a.address === WETH_ADDRESS_RINKEBY) return -1 + if (b.address === WETH_ADDRESS_RINKEBY) return 1 + return compareByLabel(a, b) + } + break case Network.xDAI: - tokensSorted = _moveTokenToHeadOfArray(WETH_ADDRESS_XDAI, tokensSorted) - return _moveTokenToHeadOfArray(WXDAI_ADDRESS_XDAI, tokensSorted) + comparator = (a, b): number => { + // WXDAI before WETH + if (a.address === WXDAI_ADDRESS_XDAI && b.address === WETH_ADDRESS_XDAI) return -1 + // WETH after WXDAI + if (a.address === WETH_ADDRESS_XDAI && b.address === WXDAI_ADDRESS_XDAI) return 1 + // WXDAI and WETH first + if (a.address === WXDAI_ADDRESS_XDAI) return -1 + if (b.address === WXDAI_ADDRESS_XDAI) return 1 + if (a.address === WETH_ADDRESS_XDAI) return -1 + if (b.address === WETH_ADDRESS_XDAI) return 1 + return compareByLabel(a, b) + } + break default: - return tokensSorted + comparator = compareByLabel } + + return comparator } async function _fetchToken( @@ -259,29 +276,30 @@ export function getTokensFactory(factoryParams: { const tokenDetails = await _fetchTokenDetails(networkId, filteredAddressesAndIds, tokensConfig) // Sort tokens - const tokenList = _sortTokens(networkId, tokenDetails) + // note that sort mutates tokenDetails + // but it's ok as tokenDetails is a newly created array + tokenDetails.sort(_createTokenComparator(networkId)) // Persist it - tokenListApi.persistTokens({ networkId, tokenList }) - } - - function _tokenComparer(a: TokenDetails, b: TokenDetails): number { - if (a.label < b.label) { - return -1 - } else if (a.label > b.label) { - return 1 - } - return 0 + tokenListApi.persistTokens({ networkId, tokenList: tokenDetails }) } async function updateTokens(networkId: number): Promise { areTokensUpdated.add(networkId) try { + // Set token list readiness to false + // and prevent stale data being presented in app + tokenListApi.setListReady(false) + const numTokens = await retry(() => exchangeApi.getNumTokens(networkId)) const tokens = tokenListApi.getTokens(networkId) logDebug(`[tokenListFactory][${networkId}] Contract has ${numTokens}; local list has ${tokens.length}`) + + // TODO: review this logic + // why update tokenDetails when token list lengths don't match? + // why would token IDs not also need to be updated? if (numTokens > tokens.length) { // When there are more tokens in the contract than locally, fetch the new tokens await updateTokenDetails(networkId, numTokens) @@ -294,14 +312,18 @@ export function getTokensFactory(factoryParams: { logDebug(`[tokenListFactory][${networkId}] Failed to update tokens: ${e.message}`) // Clear flag so on next query we try again. areTokensUpdated.delete(networkId) + } finally { + tokenListApi.setListReady(true, networkId) } } return function (networkId: number): TokenDetails[] { if (!areTokensUpdated.has(networkId)) { logDebug(`[tokenListFactory][${networkId}] Will update tokens for network`) + updateTokens(networkId) } + return tokenListApi.getTokens(networkId) } } diff --git a/src/utils/walletconnectOptions.ts b/src/utils/walletconnectOptions.ts index 3bb0572be..77b749e88 100644 --- a/src/utils/walletconnectOptions.ts +++ b/src/utils/walletconnectOptions.ts @@ -8,9 +8,14 @@ export interface WCOptions { rpc?: { mainnet?: string rinkeby?: string + xDAI?: string } } +const defaultRPC = { + [Network.xDAI]: 'https://rpc.xdaichain.com/', +} + export const setCustomWCOptions = (options: WCOptions): boolean => { const optionsStr = JSON.stringify(options) const oldStr = localStorage.getItem(STORAGE_KEY_CUSTOM_WC_OPTIONS) @@ -32,11 +37,12 @@ export const getWCOptionsFromStorage = (): WCOptions => { const mapStoredRpc = (rpc?: WCOptions['rpc']): IRPCMap | undefined => { if (!rpc) return - const { mainnet, rinkeby } = rpc + const { mainnet, rinkeby, xDAI } = rpc const rpcMap = {} if (mainnet) rpcMap[Network.Mainnet] = mainnet if (rinkeby) rpcMap[Network.Rinkeby] = rinkeby + if (xDAI) rpcMap[Network.xDAI] = xDAI return rpcMap } @@ -46,6 +52,9 @@ export const generateWCOptions = (): IWalletConnectProviderOptions => { return { infuraId: infuraId || INFURA_ID, bridge: bridge || WALLET_CONNECT_BRIDGE, - rpc: mapStoredRpc(rpc), + rpc: { + ...defaultRPC, + ...mapStoredRpc(rpc), + }, } }