Add hidden tokens to store (#9320)

From a behavioral standpoint this PR fixes the issue with tracking, and persisting, tokens that the user hides. Whether we can/should optimize this to prevent duplicates of the accountHiddenTokens and hiddenToken is a point of contention, but it acts similiarly to how we track tokens and accountTokens. 

Also to note, for tokens under a custom network there is no way to distinguish two different custom network sets of hidden tokens, they are all under the `rpc` property, same as accountTokens.
feature/default_network_editable
Patryk Łucka 4 years ago committed by GitHub
parent 3bf94164ac
commit e4a77e1dc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/scripts/controllers/detect-tokens.js
  2. 96
      app/scripts/controllers/preferences.js
  3. 47
      test/unit/app/controllers/detect-tokens-test.js

@ -47,7 +47,8 @@ export default class DetectTokensController {
for (const contractAddress in contracts) { for (const contractAddress in contracts) {
if ( if (
contracts[contractAddress].erc20 && contracts[contractAddress].erc20 &&
!this.tokenAddresses.includes(contractAddress.toLowerCase()) !this.tokenAddresses.includes(contractAddress.toLowerCase()) &&
!this.hiddenTokens.includes(contractAddress.toLowerCase())
) { ) {
tokensToDetect.push(contractAddress) tokensToDetect.push(contractAddress)
} }
@ -130,10 +131,12 @@ export default class DetectTokensController {
this.tokenAddresses = currentTokens this.tokenAddresses = currentTokens
? currentTokens.map((token) => token.address) ? currentTokens.map((token) => token.address)
: [] : []
preferences.store.subscribe(({ tokens = [] }) => { this.hiddenTokens = preferences.store.getState().hiddenTokens
preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => { this.tokenAddresses = tokens.map((token) => {
return token.address return token.address
}) })
this.hiddenTokens = hiddenTokens
}) })
preferences.store.subscribe(({ selectedAddress }) => { preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) { if (this.selectedAddress !== selectedAddress) {

@ -34,8 +34,10 @@ export default class PreferencesController {
const initState = { const initState = {
frequentRpcListDetail: [], frequentRpcListDetail: [],
accountTokens: {}, accountTokens: {},
accountHiddenTokens: {},
assetImages: {}, assetImages: {},
tokens: [], tokens: [],
hiddenTokens: [],
suggestedTokens: {}, suggestedTokens: {},
useBlockie: false, useBlockie: false,
useNonceField: false, useNonceField: false,
@ -191,6 +193,7 @@ export default class PreferencesController {
setAddresses(addresses) { setAddresses(addresses) {
const oldIdentities = this.store.getState().identities const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens const oldAccountTokens = this.store.getState().accountTokens
const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens
const identities = addresses.reduce((ids, address, index) => { const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {} const oldId = oldIdentities[address] || {}
@ -202,7 +205,12 @@ export default class PreferencesController {
tokens[address] = oldTokens tokens[address] = oldTokens
return tokens return tokens
}, {}) }, {})
this.store.updateState({ identities, accountTokens }) const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => {
const oldHiddenTokens = oldAccountHiddenTokens[address] || {}
hiddenTokens[address] = oldHiddenTokens
return hiddenTokens
}, {})
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
} }
/** /**
@ -212,14 +220,19 @@ export default class PreferencesController {
* @returns {string} the address that was removed * @returns {string} the address that was removed
*/ */
removeAddress(address) { removeAddress(address) {
const { identities } = this.store.getState() const {
const { accountTokens } = this.store.getState() identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState()
if (!identities[address]) { if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`) throw new Error(`${address} can't be deleted cause it was not found`)
} }
delete identities[address] delete identities[address]
delete accountTokens[address] delete accountTokens[address]
this.store.updateState({ identities, accountTokens }) delete accountHiddenTokens[address]
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
// If the selected account is no longer valid, // If the selected account is no longer valid,
// select an arbitrary other account: // select an arbitrary other account:
@ -237,7 +250,11 @@ export default class PreferencesController {
* *
*/ */
addAddresses(addresses) { addAddresses(addresses) {
const { identities, accountTokens } = this.store.getState() const {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState()
addresses.forEach((address) => { addresses.forEach((address) => {
// skip if already exists // skip if already exists
if (identities[address]) { if (identities[address]) {
@ -247,9 +264,10 @@ export default class PreferencesController {
const identityCount = Object.keys(identities).length const identityCount = Object.keys(identities).length
accountTokens[address] = {} accountTokens[address] = {}
accountHiddenTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address } identities[address] = { name: `Account ${identityCount + 1}`, address }
}) })
this.store.updateState({ identities, accountTokens }) this.store.updateState({ identities, accountTokens, accountHiddenTokens })
} }
/** /**
@ -346,7 +364,7 @@ export default class PreferencesController {
*/ */
/** /**
* Adds a new token to the token array, or updates the token if passed an address that already exists. * Adds a new token to the token array and removes it from the hiddenToken array, or updates the token if passed an address that already exists.
* Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects. * Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects.
* @see AddedToken {@link AddedToken} * @see AddedToken {@link AddedToken}
* *
@ -359,8 +377,11 @@ export default class PreferencesController {
async addToken(rawAddress, symbol, decimals, image) { async addToken(rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress) const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals } const newEntry = { address, symbol, decimals }
const { tokens } = this.store.getState() const { tokens, hiddenTokens } = this.store.getState()
const assetImages = this.getAssetImages() const assetImages = this.getAssetImages()
const updatedHiddenTokens = hiddenTokens.filter(
(tokenAddress) => tokenAddress !== rawAddress.toLowerCase(),
)
const previousEntry = tokens.find((token) => { const previousEntry = tokens.find((token) => {
return token.address === address return token.address === address
}) })
@ -372,23 +393,24 @@ export default class PreferencesController {
tokens.push(newEntry) tokens.push(newEntry)
} }
assetImages[address] = image assetImages[address] = image
this._updateAccountTokens(tokens, assetImages) this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens)
return Promise.resolve(tokens) return Promise.resolve(tokens)
} }
/** /**
* Removes a specified token from the tokens array. * Removes a specified token from the tokens array and adds it to hiddenTokens array
* *
* @param {string} rawAddress - Hex address of the token contract to remove. * @param {string} rawAddress - Hex address of the token contract to remove.
* @returns {Promise<array>} The new array of AddedToken objects * @returns {Promise<array>} The new array of AddedToken objects
* *
*/ */
removeToken(rawAddress) { removeToken(rawAddress) {
const { tokens } = this.store.getState() const { tokens, hiddenTokens } = this.store.getState()
const assetImages = this.getAssetImages() const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter((token) => token.address !== rawAddress) const updatedTokens = tokens.filter((token) => token.address !== rawAddress)
const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()]
delete assetImages[rawAddress] delete assetImages[rawAddress]
this._updateAccountTokens(updatedTokens, assetImages) this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens)
return Promise.resolve(updatedTokens) return Promise.resolve(updatedTokens)
} }
@ -643,47 +665,59 @@ export default class PreferencesController {
*/ */
_subscribeProviderType() { _subscribeProviderType() {
this.network.providerStore.subscribe(() => { this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates() const { tokens, hiddenTokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens }) this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens)
}) })
} }
/** /**
* Updates `accountTokens` and `tokens` of current account and network according to it. * Updates `accountTokens`, `tokens`, `accountHiddenTokens` and `hiddenTokens` of current account and network according to it.
* *
* @param {Array} tokens - Array of tokens to be updated. * @param {array} tokens - Array of tokens to be updated.
* @param {array} assetImages - Array of assets objects related to assets added
* @param {array} hiddenTokens - Array of tokens hidden by user
* *
*/ */
_updateAccountTokens(tokens, assetImages) { _updateAccountTokens(tokens, assetImages, hiddenTokens) {
const { const {
accountTokens, accountTokens,
providerType, providerType,
selectedAddress, selectedAddress,
accountHiddenTokens,
} = this._getTokenRelatedStates() } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens, assetImages }) accountHiddenTokens[selectedAddress][providerType] = hiddenTokens
this.store.updateState({
accountTokens,
tokens,
assetImages,
accountHiddenTokens,
hiddenTokens,
})
} }
/** /**
* Updates `tokens` of current account and network. * Updates `tokens` and `hiddenTokens` of current account and network.
* *
* @param {string} selectedAddress - Account address to be updated with. * @param {string} selectedAddress - Account address to be updated with.
* *
*/ */
_updateTokens(selectedAddress) { _updateTokens(selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress) const { tokens, hiddenTokens } = this._getTokenRelatedStates(
this.store.updateState({ tokens }) selectedAddress,
)
this.store.updateState({ tokens, hiddenTokens })
} }
/** /**
* A getter for `tokens` and `accountTokens` related states. * A getter for `tokens`, `accountTokens`, `hiddenTokens` and `accountHiddenTokens` related states.
* *
* @param {string} [selectedAddress] - A new hex address for an account * @param {string} [selectedAddress] - A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens` * @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
* *
*/ */
_getTokenRelatedStates(selectedAddress) { _getTokenRelatedStates(selectedAddress) {
const { accountTokens } = this.store.getState() const { accountTokens, accountHiddenTokens } = this.store.getState()
if (!selectedAddress) { if (!selectedAddress) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
selectedAddress = this.store.getState().selectedAddress selectedAddress = this.store.getState().selectedAddress
@ -692,11 +726,25 @@ export default class PreferencesController {
if (!(selectedAddress in accountTokens)) { if (!(selectedAddress in accountTokens)) {
accountTokens[selectedAddress] = {} accountTokens[selectedAddress] = {}
} }
if (!(selectedAddress in accountHiddenTokens)) {
accountHiddenTokens[selectedAddress] = {}
}
if (!(providerType in accountTokens[selectedAddress])) { if (!(providerType in accountTokens[selectedAddress])) {
accountTokens[selectedAddress][providerType] = [] accountTokens[selectedAddress][providerType] = []
} }
if (!(providerType in accountHiddenTokens[selectedAddress])) {
accountHiddenTokens[selectedAddress][providerType] = []
}
const tokens = accountTokens[selectedAddress][providerType] const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress } const hiddenTokens = accountHiddenTokens[selectedAddress][providerType]
return {
tokens,
accountTokens,
hiddenTokens,
accountHiddenTokens,
providerType,
selectedAddress,
}
} }
/** /**

@ -85,6 +85,53 @@ describe('DetectTokensController', function () {
sandbox.assert.notCalled(stub) sandbox.assert.notCalled(stub)
}) })
it('should skip adding tokens listed in hiddenTokens array', async function () {
sandbox.useFakeTimers()
network.setProviderType(MAINNET)
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
})
controller.isOpen = true
controller.isUnlocked = true
const contractAddresses = Object.keys(contracts)
const erc20ContractAddresses = contractAddresses.filter(
(contractAddress) => contracts[contractAddress].erc20 === true,
)
const existingTokenAddress = erc20ContractAddresses[0]
const existingToken = contracts[existingTokenAddress]
await preferences.addToken(
existingTokenAddress,
existingToken.symbol,
existingToken.decimals,
)
const tokenAddressToSkip = erc20ContractAddresses[1]
sandbox
.stub(controller, '_getTokenBalances')
.callsFake((tokensToDetect) =>
tokensToDetect.map((token) =>
token === tokenAddressToSkip ? new BigNumber(10) : 0,
),
)
await preferences.removeToken(tokenAddressToSkip)
await controller.detectNewTokens()
assert.deepEqual(preferences.store.getState().tokens, [
{
address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals,
symbol: existingToken.symbol,
},
])
})
it('should check and add tokens while on main network', async function () { it('should check and add tokens while on main network', async function () {
sandbox.useFakeTimers() sandbox.useFakeTimers()
network.setProviderType(MAINNET) network.setProviderType(MAINNET)

Loading…
Cancel
Save