diff --git a/common/store.ts b/common/store.ts index bbc1a92b..6bc9a7e9 100644 --- a/common/store.ts +++ b/common/store.ts @@ -19,6 +19,7 @@ import RootReducer from './reducers'; import promiseMiddleware from 'redux-promise-middleware'; import { getNodeConfigFromId } from 'utils/node'; import { getNetworkConfigFromId } from 'utils/network'; +import { dedupeCustomTokens } from 'utils/tokens'; import sagas from './sagas'; import { gasPricetoBase } from 'libs/units'; @@ -59,7 +60,6 @@ const configureStore = () => { } : { ...swapInitialState }; - const localCustomTokens = loadStatePropertyOrEmptyObject('customTokens'); const savedTransactionState = loadStatePropertyOrEmptyObject('transaction'); const savedConfigState = loadStatePropertyOrEmptyObject('config'); @@ -82,6 +82,13 @@ const configureStore = () => { } } + // Dedupe custom tokens initially + const savedCustomTokensState = + loadStatePropertyOrEmptyObject('customTokens') || customTokensInitialState; + const initialNetwork = + (savedConfigState && savedConfigState.network) || configInitialState.network; + const customTokens = dedupeCustomTokens(initialNetwork.tokens, savedCustomTokensState); + const persistedInitialState = { config: { ...configInitialState, @@ -100,7 +107,7 @@ const configureStore = () => { : transactionInitialState.fields.gasPrice } }, - customTokens: localCustomTokens || customTokensInitialState, + customTokens, // ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3 swap: swapState }; diff --git a/common/utils/tokens.ts b/common/utils/tokens.ts new file mode 100644 index 00000000..23d44baf --- /dev/null +++ b/common/utils/tokens.ts @@ -0,0 +1,18 @@ +import { Token } from 'config/data'; + +export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]): Token[] { + if (!customTokens.length) { + return []; + } + + // If any tokens have the same symbol or contract address, remove them + const tokenCollisionMap = networkTokens.reduce((prev, token) => { + prev[token.symbol] = true; + prev[token.address] = true; + return prev; + }, {}); + + return customTokens.filter(token => { + return !tokenCollisionMap[token.address] && !tokenCollisionMap[token.symbol]; + }); +} diff --git a/spec/utils/tokens.spec.ts b/spec/utils/tokens.spec.ts new file mode 100644 index 00000000..dcf96822 --- /dev/null +++ b/spec/utils/tokens.spec.ts @@ -0,0 +1,47 @@ +import { dedupeCustomTokens } from 'utils/tokens'; + +describe('dedupeCustomTokens', () => { + const networkTokens = [ + { + address: '0x48c80F1f4D53D5951e5D5438B54Cba84f29F32a5', + symbol: 'REP', + decimal: 18 + }, + { + address: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', + symbol: 'GNT', + decimal: 18 + } + ]; + + const DUPLICATE_ADDRESS = { + address: networkTokens[0].address, + symbol: 'REP2', + decimal: 18 + }; + const DUPLICATE_SYMBOL = { + address: '0x0', + symbol: networkTokens[1].symbol, + decimal: 18 + }; + const NONDUPLICATE_CUSTOM = { + address: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8', + symbol: 'MEW', + decimal: 0 + }; + + const customTokens = [DUPLICATE_ADDRESS, DUPLICATE_SYMBOL, NONDUPLICATE_CUSTOM]; + const dedupedTokens = dedupeCustomTokens(networkTokens, customTokens); + + it('Should remove duplicate address custom tokens', () => { + expect(dedupedTokens.includes(DUPLICATE_ADDRESS)).toBeFalsy(); + }); + + it('Should remove duplicate symbol custom tokens', () => { + expect(dedupedTokens.includes(DUPLICATE_SYMBOL)).toBeFalsy(); + }); + + it('Should not remove custom tokens that aren’t duplicates', () => { + expect(dedupedTokens.includes(NONDUPLICATE_CUSTOM)).toBeTruthy(); + }); +});