diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 687a93679..759872e4b 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -778,7 +778,7 @@ const state = { "0xaD6D458402F60fD3Bd25163575031ACDce07538D": "./sai.svg" }, "hiddenTokens": [], - "suggestedTokens": {}, + "suggestedAssets": [], "useNonceField": false, "usePhishDetect": true, "lostIdentities": {}, diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 1777d28ca..246114657 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -24,12 +24,36 @@ export default class DetectTokensController { network, keyringMemStore, tokenList, + tokensController, } = {}) { + this.tokensController = tokensController; this.preferences = preferences; this.interval = interval; this.network = network; this.keyringMemStore = keyringMemStore; this.tokenList = tokenList; + this.selectedAddress = this.preferences?.store.getState().selectedAddress; + this.tokenAddresses = this.tokensController?.state.tokens.map((token) => { + return token.address; + }); + this.hiddenTokens = this.tokensController?.state.ignoredTokens; + + preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => { + if ( + this.selectedAddress !== selectedAddress || + this.useTokenDetection !== useTokenDetection + ) { + this.selectedAddress = selectedAddress; + this.useTokenDetection = useTokenDetection; + this.restartTokenDetection(); + } + }); + tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => { + this.tokenAddresses = tokens.map((token) => { + return token.address; + }); + this.hiddenTokens = ignoredTokens; + }); } async _getTokenBalances(tokens) { @@ -88,16 +112,19 @@ export default class DetectTokensController { ); return; } + + const tokensWithBalance = tokensSlice.filter((_, index) => { + const balance = result[index]; + return balance && !balance.isZero(); + }); + await Promise.all( - tokensSlice.map(async (tokenAddress, index) => { - const balance = result[index]; - if (balance && !balance.isZero()) { - await this._preferences.addToken( - tokenAddress, - tokenList[tokenAddress].symbol, - tokenList[tokenAddress].decimals, - ); - } + tokensWithBalance.map((tokenAddress) => { + return this.tokensController.addToken( + tokenAddress, + tokenList[tokenAddress].symbol, + tokenList[tokenAddress].decimals, + ); }), ); } @@ -130,38 +157,6 @@ export default class DetectTokensController { }, interval); } - /** - * In setter when selectedAddress is changed, detectNewTokens and restart polling - * @type {Object} - */ - set preferences(preferences) { - if (!preferences) { - return; - } - this._preferences = preferences; - const currentTokens = preferences.store.getState().tokens; - this.tokenAddresses = currentTokens - ? currentTokens.map((token) => token.address) - : []; - this.hiddenTokens = preferences.store.getState().hiddenTokens; - preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => { - this.tokenAddresses = tokens.map((token) => { - return token.address; - }); - this.hiddenTokens = hiddenTokens; - }); - preferences.store.subscribe(({ selectedAddress, useTokenDetection }) => { - if ( - this.selectedAddress !== selectedAddress || - this.useTokenDetection !== useTokenDetection - ) { - this.selectedAddress = selectedAddress; - this.useTokenDetection = useTokenDetection; - this.restartTokenDetection(); - } - }); - } - /** * @type {Object} */ diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index 65f9af298..4a739aff1 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -6,8 +6,10 @@ import BigNumber from 'bignumber.js'; import { ControllerMessenger, TokenListController, + TokensController, } from '@metamask/controllers'; import { MAINNET, ROPSTEN } from '../../../shared/constants/network'; +import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; import NetworkController from './network'; import PreferencesController from './preferences'; @@ -15,7 +17,7 @@ import PreferencesController from './preferences'; describe('DetectTokensController', function () { let tokenListController; const sandbox = sinon.createSandbox(); - let keyringMemStore, network, preferences, provider; + let keyringMemStore, network, preferences, provider, tokensController; const noop = () => undefined; @@ -30,6 +32,12 @@ describe('DetectTokensController', function () { network.initializeProvider(networkControllerProviderConfig); provider = network.getProviderAndBlockTracker().provider; preferences = new PreferencesController({ network, provider }); + tokensController = new TokensController({ + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: network.store.subscribe.bind(network.store), + }); preferences.setAddresses([ '0x7e57e2', '0xbc86727e770de68b1060c91f6bb6945c73e10388', @@ -38,7 +46,10 @@ describe('DetectTokensController', function () { .stub(network, 'getLatestBlock') .callsFake(() => Promise.resolve({})); sandbox - .stub(preferences, '_detectIsERC721') + .stub(tokensController, '_instantiateNewEthersProvider') + .returns(null); + sandbox + .stub(tokensController, '_detectIsERC721') .returns(Promise.resolve(false)); nock('https://token-api.metaswap.codefi.network') .get(`/tokens/1`) @@ -142,6 +153,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -177,6 +189,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -195,6 +208,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -204,13 +218,19 @@ describe('DetectTokensController', function () { const existingTokenAddress = erc20ContractAddresses[0]; const existingToken = tokenList[existingTokenAddress]; - await preferences.addToken( + await tokensController.addToken( existingTokenAddress, existingToken.symbol, existingToken.decimals, ); const tokenAddressToSkip = erc20ContractAddresses[1]; + const tokenToSkip = tokenList[tokenAddressToSkip]; + await tokensController.addToken( + tokenAddressToSkip, + tokenToSkip.symbol, + tokenToSkip.decimals, + ); sandbox .stub(controller, '_getTokenBalances') @@ -220,15 +240,15 @@ describe('DetectTokensController', function () { ), ); - await preferences.removeToken(tokenAddressToSkip); - + await tokensController.removeAndIgnoreToken(tokenAddressToSkip); await controller.detectNewTokens(); - assert.deepEqual(preferences.store.getState().tokens, [ + assert.deepEqual(tokensController.state.tokens, [ { - address: existingTokenAddress.toLowerCase(), + address: toChecksumHexAddress(existingTokenAddress), decimals: existingToken.decimals, symbol: existingToken.symbol, + image: undefined, isERC721: false, }, ]); @@ -242,6 +262,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -251,7 +272,7 @@ describe('DetectTokensController', function () { const existingTokenAddress = erc20ContractAddresses[0]; const existingToken = tokenList[existingTokenAddress]; - await preferences.addToken( + await tokensController.addToken( existingTokenAddress, existingToken.symbol, existingToken.decimals, @@ -266,8 +287,8 @@ describe('DetectTokensController', function () { const indexOfTokenToAdd = contractAddressesToDetect.indexOf( tokenAddressToAdd, ); - const balances = new Array(contractAddressesToDetect.length); + balances[indexOfTokenToAdd] = new BigNumber(10); sandbox @@ -275,18 +296,19 @@ describe('DetectTokensController', function () { .returns(Promise.resolve(balances)); await controller.detectNewTokens(); - - assert.deepEqual(preferences.store.getState().tokens, [ + assert.deepEqual(tokensController.state.tokens, [ { - address: existingTokenAddress.toLowerCase(), + address: toChecksumHexAddress(existingTokenAddress), decimals: existingToken.decimals, symbol: existingToken.symbol, isERC721: false, + image: undefined, }, { - address: tokenAddressToAdd.toLowerCase(), + address: toChecksumHexAddress(tokenAddressToAdd), decimals: tokenToAdd.decimals, symbol: tokenToAdd.symbol, + image: undefined, isERC721: false, }, ]); @@ -300,6 +322,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -309,7 +332,7 @@ describe('DetectTokensController', function () { const existingTokenAddress = erc20ContractAddresses[0]; const existingToken = tokenList[existingTokenAddress]; - await preferences.addToken( + await tokensController.addToken( existingTokenAddress, existingToken.symbol, existingToken.decimals, @@ -334,17 +357,19 @@ describe('DetectTokensController', function () { await controller.detectNewTokens(); - assert.deepEqual(preferences.store.getState().tokens, [ + assert.deepEqual(tokensController.state.tokens, [ { - address: existingTokenAddress.toLowerCase(), + address: toChecksumHexAddress(existingTokenAddress), decimals: existingToken.decimals, symbol: existingToken.symbol, + image: undefined, isERC721: false, }, { - address: tokenAddressToAdd.toLowerCase(), + address: toChecksumHexAddress(tokenAddressToAdd), decimals: tokenToAdd.decimals, symbol: tokenToAdd.symbol, + image: undefined, isERC721: false, }, ]); @@ -357,6 +382,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -374,6 +400,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.selectedAddress = '0x0'; @@ -390,6 +417,7 @@ describe('DetectTokensController', function () { network, keyringMemStore, tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = false; @@ -405,6 +433,7 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokensController, }); // trigger state update from preferences controller await preferences.setSelectedAddress( diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index b7b981ec3..095e9c486 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,22 +1,12 @@ import { strict as assert } from 'assert'; import { ObservableStore } from '@metamask/obs-store'; -import { ethErrors } from 'eth-rpc-errors'; import { normalize as normalizeAddress } from 'eth-sig-util'; import { ethers } from 'ethers'; import log from 'loglevel'; -import abiERC721 from 'human-standard-collectible-abi'; -import contractsMap from '@metamask/contract-metadata'; -import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens'; import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; -import { - isValidHexAddress, - toChecksumHexAddress, -} from '../../../shared/modules/hexstring-utils'; import { NETWORK_EVENTS } from './network'; -const ERC721_INTERFACE_ID = '0x80ac58cd'; - export default class PreferencesController { /** * @@ -24,9 +14,6 @@ export default class PreferencesController { * @param {Object} opts - Overrides the defaults for the initial state of this.store * @property {Object} store The stored object containing a users preferences, stored in local storage * @property {Array} store.frequentRpcList A list of custom rpcs to provide the user - * @property {Array} store.tokens The tokens the user wants display in their token lists - * @property {Object} store.accountTokens The tokens stored per account and then per network type - * @property {Object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {boolean} store.useNonceField The users preference for nonce field within the UI * @property {Object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the @@ -41,12 +28,6 @@ export default class PreferencesController { constructor(opts = {}) { const initState = { frequentRpcListDetail: [], - accountTokens: {}, - accountHiddenTokens: {}, - assetImages: {}, - tokens: [], - hiddenTokens: [], - suggestedTokens: {}, useBlockie: false, useNonceField: false, usePhishDetect: true, @@ -90,12 +71,6 @@ export default class PreferencesController { this.openPopup = opts.openPopup; this.migrateAddressBookState = opts.migrateAddressBookState; - this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - const { tokens, hiddenTokens } = this._getTokenRelatedStates(); - this.ethersProvider = new ethers.providers.Web3Provider(opts.provider); - this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens); - }); - this._subscribeToInfuraAvailability(); global.setPreference = (key, value) => { @@ -162,14 +137,6 @@ export default class PreferencesController { this.store.updateState({ firstTimeFlowType: type }); } - getSuggestedTokens() { - return this.store.getState().suggestedTokens; - } - - getAssetImages() { - return this.store.getState().assetImages; - } - /** * Add new methodData to state, to avoid requesting this information again through Infura * @@ -182,24 +149,6 @@ export default class PreferencesController { this.store.updateState({ knownMethodData }); } - /** - * wallet_watchAsset request handler. - * - * @param {Object} req - The watchAsset JSON-RPC request object. - */ - async requestWatchAsset(req) { - const { type, options } = req.params; - - switch (type) { - case 'ERC20': - return await this._handleWatchAssetERC20(options); - default: - throw ethErrors.rpc.invalidParams( - `Asset of type "${type}" not supported.`, - ); - } - } - /** * Setter for the `currentLocale` property * @@ -226,25 +175,14 @@ export default class PreferencesController { */ setAddresses(addresses) { const oldIdentities = this.store.getState().identities; - const oldAccountTokens = this.store.getState().accountTokens; - const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens; const identities = addresses.reduce((ids, address, index) => { const oldId = oldIdentities[address] || {}; ids[address] = { name: `Account ${index + 1}`, address, ...oldId }; return ids; }, {}); - const accountTokens = addresses.reduce((tokens, address) => { - const oldTokens = oldAccountTokens[address] || {}; - tokens[address] = oldTokens; - return tokens; - }, {}); - const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => { - const oldHiddenTokens = oldAccountHiddenTokens[address] || {}; - hiddenTokens[address] = oldHiddenTokens; - return hiddenTokens; - }, {}); - this.store.updateState({ identities, accountTokens, accountHiddenTokens }); + + this.store.updateState({ identities }); } /** @@ -254,19 +192,13 @@ export default class PreferencesController { * @returns {string} the address that was removed */ removeAddress(address) { - const { - identities, - accountTokens, - accountHiddenTokens, - } = this.store.getState(); + const { identities } = this.store.getState(); if (!identities[address]) { throw new Error(`${address} can't be deleted cause it was not found`); } delete identities[address]; - delete accountTokens[address]; - delete accountHiddenTokens[address]; - this.store.updateState({ identities, accountTokens, accountHiddenTokens }); + this.store.updateState({ identities }); // If the selected account is no longer valid, // select an arbitrary other account: @@ -284,11 +216,7 @@ export default class PreferencesController { * */ addAddresses(addresses) { - const { - identities, - accountTokens, - accountHiddenTokens, - } = this.store.getState(); + const { identities } = this.store.getState(); addresses.forEach((address) => { // skip if already exists if (identities[address]) { @@ -297,11 +225,9 @@ export default class PreferencesController { // add missing identity const identityCount = Object.keys(identities).length; - accountTokens[address] = {}; - accountHiddenTokens[address] = {}; identities[address] = { name: `Account ${identityCount + 1}`, address }; }); - this.store.updateState({ identities, accountTokens, accountHiddenTokens }); + this.store.updateState({ identities }); } /** @@ -348,25 +274,16 @@ export default class PreferencesController { return selected; } - removeSuggestedTokens() { - return new Promise((resolve) => { - this.store.updateState({ suggestedTokens: {} }); - resolve({}); - }); - } - /** * Setter for the `selectedAddress` property * * @param {string} _address - A new hex address for an account - * @returns {Promise} Promise resolves with tokens * */ setSelectedAddress(_address) { const address = normalizeAddress(_address); - this._updateTokens(address); - const { identities, tokens } = this.store.getState(); + const { identities } = this.store.getState(); const selectedIdentity = identities[address]; if (!selectedIdentity) { throw new Error(`Identity for '${address} not found`); @@ -374,7 +291,6 @@ export default class PreferencesController { selectedIdentity.lastSelected = Date.now(); this.store.updateState({ identities, selectedAddress: address }); - return Promise.resolve(tokens); } /** @@ -387,99 +303,6 @@ export default class PreferencesController { return this.store.getState().selectedAddress; } - /** - * Contains data about tokens users add to their account. - * @typedef {Object} AddedToken - * @property {string} address - The hex address for the token contract. Will be all lower cased and hex-prefixed. - * @property {string} symbol - The symbol of the token, usually 3 or 4 capitalized letters - * {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#symbol} - * @property {boolean} decimals - The number of decimals the token uses. - * {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#decimals} - */ - - /** - * 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. - * @see AddedToken {@link AddedToken} - * - * @param {string} rawAddress - Hex address of the token contract. May or may not be a checksum address. - * @param {string} symbol - The symbol of the token - * @param {number} decimals - The number of decimals the token uses. - * @returns {Promise} Promises the new array of AddedToken objects. - * - */ - async addToken(rawAddress, symbol, decimals, image) { - const address = normalizeAddress(rawAddress); - const newEntry = { address, symbol, decimals: Number(decimals) }; - const { tokens, hiddenTokens } = this.store.getState(); - const assetImages = this.getAssetImages(); - const updatedHiddenTokens = hiddenTokens.filter( - (tokenAddress) => tokenAddress !== rawAddress.toLowerCase(), - ); - const previousEntry = tokens.find((token) => { - return token.address === address; - }); - const previousIndex = tokens.indexOf(previousEntry); - - newEntry.isERC721 = await this._detectIsERC721(newEntry.address); - - if (previousEntry) { - tokens[previousIndex] = newEntry; - } else { - tokens.push(newEntry); - } - assetImages[address] = image; - this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens); - return Promise.resolve(tokens); - } - - /** - * Adds isERC721 field to token object - * (Called when a user attempts to add tokens that were previously added which do not yet had isERC721 field) - * - * @param {string} tokenAddress - The contract address of the token requiring the isERC721 field added. - * @returns {Promise} The new token object with the added isERC721 field. - * - */ - async updateTokenType(tokenAddress) { - const { tokens } = this.store.getState(); - const tokenIndex = tokens.findIndex((token) => { - return token.address === tokenAddress; - }); - tokens[tokenIndex].isERC721 = await this._detectIsERC721(tokenAddress); - this.store.updateState({ tokens }); - return Promise.resolve(tokens[tokenIndex]); - } - - /** - * 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. - * @returns {Promise} The new array of AddedToken objects - * - */ - removeToken(rawAddress) { - const { tokens, hiddenTokens } = this.store.getState(); - const assetImages = this.getAssetImages(); - const updatedTokens = tokens.filter( - (token) => token.address !== rawAddress, - ); - const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()]; - delete assetImages[rawAddress]; - this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens); - return Promise.resolve(updatedTokens); - } - - /** - * A getter for the `tokens` property - * - * @returns {Array} The current array of AddedToken objects - * - */ - getTokens() { - return this.store.getState().tokens; - } - /** * Sets a custom label for an account * @param {string} account - the account to set a label for @@ -770,188 +593,4 @@ export default class PreferencesController { this.store.updateState({ infuraBlocked: isBlocked }); } - - /** - * 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} assetImages - Array of assets objects related to assets added - * @param {array} hiddenTokens - Array of tokens hidden by user - * - */ - _updateAccountTokens(tokens, assetImages, hiddenTokens) { - const { - accountTokens, - chainId, - selectedAddress, - accountHiddenTokens, - } = this._getTokenRelatedStates(); - accountTokens[selectedAddress][chainId] = tokens; - accountHiddenTokens[selectedAddress][chainId] = hiddenTokens; - this.store.updateState({ - accountTokens, - tokens, - assetImages, - accountHiddenTokens, - hiddenTokens, - }); - } - - /** - * Detects whether or not a token is ERC-721 compatible. - * - * @param {string} tokensAddress - the token contract address. - * - */ - async _detectIsERC721(tokenAddress) { - const checksumAddress = toChecksumHexAddress(tokenAddress); - // if this token is already in our contract metadata map we don't need - // to check against the contract - if (contractsMap[checksumAddress]?.erc721 === true) { - return Promise.resolve(true); - } - const tokenContract = await this._createEthersContract( - tokenAddress, - abiERC721, - this.ethersProvider, - ); - - return await tokenContract - .supportsInterface(ERC721_INTERFACE_ID) - .catch((error) => { - log.debug(error); - return false; - }); - } - - async _createEthersContract(tokenAddress, abi, ethersProvider) { - const tokenContract = await new ethers.Contract( - tokenAddress, - abi, - ethersProvider, - ); - return tokenContract; - } - - /** - * Updates `tokens` and `hiddenTokens` of current account and network. - * - * @param {string} selectedAddress - Account address to be updated with. - * - */ - _updateTokens(selectedAddress) { - const { tokens, hiddenTokens } = this._getTokenRelatedStates( - selectedAddress, - ); - this.store.updateState({ tokens, hiddenTokens }); - } - - /** - * A getter for `tokens`, `accountTokens`, `hiddenTokens` and `accountHiddenTokens` related states. - * - * @param {string} [selectedAddress] - A new hex address for an account - * @returns {Object.} States to interact with tokens in `accountTokens` - * - */ - _getTokenRelatedStates(selectedAddress) { - const { accountTokens, accountHiddenTokens } = this.store.getState(); - if (!selectedAddress) { - // eslint-disable-next-line no-param-reassign - selectedAddress = this.store.getState().selectedAddress; - } - const chainId = this.network.getCurrentChainId(); - if (!(selectedAddress in accountTokens)) { - accountTokens[selectedAddress] = {}; - } - if (!(selectedAddress in accountHiddenTokens)) { - accountHiddenTokens[selectedAddress] = {}; - } - if (!(chainId in accountTokens[selectedAddress])) { - accountTokens[selectedAddress][chainId] = []; - } - if (!(chainId in accountHiddenTokens[selectedAddress])) { - accountHiddenTokens[selectedAddress][chainId] = []; - } - const tokens = accountTokens[selectedAddress][chainId]; - const hiddenTokens = accountHiddenTokens[selectedAddress][chainId]; - return { - tokens, - accountTokens, - hiddenTokens, - accountHiddenTokens, - chainId, - selectedAddress, - }; - } - - /** - * Handle the suggestion of an ERC20 asset through `watchAsset` - * * - * @param {Object} tokenMetadata - Token metadata - * - */ - async _handleWatchAssetERC20(tokenMetadata) { - this._validateERC20AssetParams(tokenMetadata); - - const address = normalizeAddress(tokenMetadata.address); - const { symbol, decimals, image } = tokenMetadata; - this._addSuggestedERC20Asset(address, symbol, decimals, image); - - await this.openPopup(); - const tokenAddresses = this.getTokens().filter( - (token) => token.address === address, - ); - return tokenAddresses.length > 0; - } - - /** - * Validates that the passed options for suggested token have all required properties. - * - * @param {Object} opts - The options object to validate - * @throws {string} Throw a custom error indicating that address, symbol and/or decimals - * doesn't fulfill requirements - * - */ - _validateERC20AssetParams({ address, symbol, decimals }) { - if (!address || !symbol || typeof decimals === 'undefined') { - throw ethErrors.rpc.invalidParams( - `Must specify address, symbol, and decimals.`, - ); - } - if (typeof symbol !== 'string') { - throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`); - } - if (!(symbol.length > 0)) { - throw ethErrors.rpc.invalidParams( - `Invalid symbol "${symbol}": shorter than a character.`, - ); - } - if (!(symbol.length < 12)) { - throw ethErrors.rpc.invalidParams( - `Invalid symbol "${symbol}": longer than 11 characters.`, - ); - } - const numDecimals = parseInt(decimals, 10); - if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { - throw ethErrors.rpc.invalidParams( - `Invalid decimals "${decimals}": must be 0 <= 36.`, - ); - } - if (!isValidHexAddress(address, { allowNonPrefixed: false })) { - throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`); - } - } - - _addSuggestedERC20Asset(address, symbol, decimals, image) { - const newEntry = { - address, - symbol, - decimals, - image, - unlisted: !LISTED_CONTRACT_ADDRESSES.includes(address), - }; - const suggested = this.getSuggestedTokens(); - suggested[address] = newEntry; - this.store.updateState({ suggestedTokens: suggested }); - } } diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 0abf8c202..892ae5274 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -1,11 +1,6 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import contractMaps from '@metamask/contract-metadata'; -import abiERC721 from 'human-standard-collectible-abi'; -import { - MAINNET_CHAIN_ID, - RINKEBY_CHAIN_ID, -} from '../../../shared/constants/network'; +import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; import PreferencesController from './preferences'; import NetworkController from './network'; @@ -13,9 +8,6 @@ describe('preferences controller', function () { let preferencesController; let network; let currentChainId; - let triggerNetworkChange; - let switchToMainnet; - let switchToRinkeby; let provider; const migrateAddressBookState = sinon.stub(); @@ -37,22 +29,12 @@ describe('preferences controller', function () { sandbox .stub(network, 'getProviderConfig') .callsFake(() => ({ type: 'mainnet' })); - const spy = sandbox.spy(network, 'on'); preferencesController = new PreferencesController({ migrateAddressBookState, network, provider, }); - triggerNetworkChange = spy.firstCall.args[1]; - switchToMainnet = () => { - currentChainId = MAINNET_CHAIN_ID; - triggerNetworkChange(); - }; - switchToRinkeby = () => { - currentChainId = RINKEBY_CHAIN_ID; - triggerNetworkChange(); - }; }); afterEach(function () { @@ -76,17 +58,6 @@ describe('preferences controller', function () { }); }); - it('should create account tokens for each account in the store', function () { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - const { accountTokens } = preferencesController.store.getState(); - - assert.deepEqual(accountTokens, { - '0xda22le': {}, - '0x7e57e2': {}, - }); - }); - it('should replace its list of addresses', function () { preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le77', '0x7e57e277']); @@ -105,104 +76,6 @@ describe('preferences controller', function () { }); }); - describe('updateTokenType', function () { - it('should add isERC721 = true to token object in state when token is collectible and in our contract-metadata repo', async function () { - const contractAddresses = Object.keys(contractMaps); - const erc721ContractAddresses = contractAddresses.filter( - (contractAddress) => contractMaps[contractAddress].erc721 === true, - ); - const address = erc721ContractAddresses[0]; - const { symbol, decimals } = contractMaps[address]; - preferencesController.store.updateState({ - tokens: [{ address, symbol, decimals }], - }); - const result = await preferencesController.updateTokenType(address); - assert.equal(result.isERC721, true); - }); - - it('should add isERC721 = true to token object in state when token is collectible and not in our contract-metadata repo', async function () { - const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d'; - preferencesController.store.updateState({ - tokens: [ - { - address: tokenAddress, - symbol: 'TESTNFT', - decimals: '0', - }, - ], - }); - sinon - .stub(preferencesController, '_detectIsERC721') - .callsFake(() => true); - - const result = await preferencesController.updateTokenType(tokenAddress); - assert.equal( - preferencesController._detectIsERC721.getCall(0).args[0], - tokenAddress, - ); - assert.equal(result.isERC721, true); - }); - }); - - describe('_detectIsERC721', function () { - it('should return true when token is in our contract-metadata repo', async function () { - const tokenAddress = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d'; - - const result = await preferencesController._detectIsERC721(tokenAddress); - assert.equal(result, true); - }); - - it('should return true when the token is not in our contract-metadata repo but tokenContract.supportsInterface returns true', async function () { - const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d'; - - const supportsInterfaceStub = sinon.stub().returns(Promise.resolve(true)); - sinon - .stub(preferencesController, '_createEthersContract') - .callsFake(() => ({ supportsInterface: supportsInterfaceStub })); - - const result = await preferencesController._detectIsERC721(tokenAddress); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[0], - tokenAddress, - ); - assert.deepEqual( - preferencesController._createEthersContract.getCall(0).args[1], - abiERC721, - ); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[2], - preferencesController.ethersProvider, - ); - assert.equal(result, true); - }); - - it('should return false when the token is not in our contract-metadata repo and tokenContract.supportsInterface returns false', async function () { - const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d'; - - const supportsInterfaceStub = sinon - .stub() - .returns(Promise.resolve(false)); - sinon - .stub(preferencesController, '_createEthersContract') - .callsFake(() => ({ supportsInterface: supportsInterfaceStub })); - - const result = await preferencesController._detectIsERC721(tokenAddress); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[0], - tokenAddress, - ); - assert.deepEqual( - preferencesController._createEthersContract.getCall(0).args[1], - abiERC721, - ); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[2], - preferencesController.ethersProvider, - ); - assert.equal(result, false); - }); - }); - describe('removeAddress', function () { it('should remove an address from state', function () { preferencesController.setAddresses(['0xda22le', '0x7e57e2']); @@ -215,17 +88,6 @@ describe('preferences controller', function () { ); }); - it('should remove an address from state and respective tokens', function () { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - preferencesController.removeAddress('0xda22le'); - - assert.equal( - preferencesController.store.getState().accountTokens['0xda22le'], - undefined, - ); - }); - it('should switch accounts if the selected address is removed', function () { preferencesController.setAddresses(['0xda22le', '0x7e57e2']); @@ -259,489 +121,6 @@ describe('preferences controller', function () { }); }); - describe('getTokens', function () { - it('should return an empty list initially', async function () { - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 0, 'empty list of tokens'); - }); - }); - - describe('addToken', function () { - it('should add that token to its state', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(address, symbol, decimals); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, 'one token added'); - - const added = tokens[0]; - assert.equal(added.address, address, 'set address correctly'); - assert.equal(added.symbol, symbol, 'set symbol correctly'); - assert.equal(added.decimals, decimals, 'set decimals correctly'); - }); - - it('should allow updating a token value', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(address, symbol, decimals); - - const newDecimals = 6; - await preferencesController.addToken(address, symbol, newDecimals); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, 'one token added'); - - const added = tokens[0]; - assert.equal(added.address, address, 'set address correctly'); - assert.equal(added.symbol, symbol, 'set symbol correctly'); - assert.equal(added.decimals, newDecimals, 'updated decimals correctly'); - }); - - it('should allow adding tokens to two separate addresses', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2', '0xda22le']); - - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(address, symbol, decimals); - assert.equal( - preferencesController.getTokens().length, - 1, - 'one token added for 1st address', - ); - - await preferencesController.setSelectedAddress('0xda22le'); - await preferencesController.addToken(address, symbol, decimals); - assert.equal( - preferencesController.getTokens().length, - 1, - 'one token added for 2nd address', - ); - }); - - it('should add token per account', async function () { - const addressFirst = '0xabcdef1234567'; - const addressSecond = '0xabcdef1234568'; - const symbolFirst = 'ABBR'; - const symbolSecond = 'ABBB'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2', '0xda22le']); - - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(addressFirst, symbolFirst, decimals); - const tokensFirstAddress = preferencesController.getTokens(); - - await preferencesController.setSelectedAddress('0xda22le'); - await preferencesController.addToken( - addressSecond, - symbolSecond, - decimals, - ); - const tokensSeconAddress = preferencesController.getTokens(); - - assert.notEqual( - tokensFirstAddress, - tokensSeconAddress, - 'add different tokens for two account and tokens are equal', - ); - }); - - it('should add token per network', async function () { - const addressFirst = '0xabcdef1234567'; - const addressSecond = '0xabcdef1234568'; - const symbolFirst = 'ABBR'; - const symbolSecond = 'ABBB'; - const decimals = 5; - await preferencesController.addToken(addressFirst, symbolFirst, decimals); - const tokensFirstAddress = preferencesController.getTokens(); - - switchToRinkeby(); - await preferencesController.addToken( - addressSecond, - symbolSecond, - decimals, - ); - const tokensSeconAddress = preferencesController.getTokens(); - - assert.notEqual( - tokensFirstAddress, - tokensSeconAddress, - 'add different tokens for two networks and tokens are equal', - ); - }); - }); - - describe('removeToken', function () { - it('should remove the only token from its state', async function () { - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 5); - await preferencesController.removeToken('0xa'); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 0, 'one token removed'); - }); - - it('should remove a token from its state', async function () { - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - await preferencesController.removeToken('0xa'); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, 'one token removed'); - - const [token1] = tokens; - assert.deepEqual(token1, { - address: '0xb', - symbol: 'B', - decimals: 5, - isERC721: false, - }); - }); - - it('should remove a token from its state on corresponding address', async function () { - preferencesController.setAddresses(['0x7e57e2', '0x7e57e3']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - await preferencesController.setSelectedAddress('0x7e57e3'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - const initialTokensSecond = preferencesController.getTokens(); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.removeToken('0xa'); - - const tokensFirst = preferencesController.getTokens(); - assert.equal(tokensFirst.length, 1, 'one token removed in account'); - - const [token1] = tokensFirst; - assert.deepEqual(token1, { - address: '0xb', - symbol: 'B', - decimals: 5, - isERC721: false, - }); - - await preferencesController.setSelectedAddress('0x7e57e3'); - const tokensSecond = preferencesController.getTokens(); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'token deleted for account', - ); - }); - - it('should remove a token from its state on corresponding network', async function () { - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - switchToRinkeby(); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - const initialTokensSecond = preferencesController.getTokens(); - switchToMainnet(); - await preferencesController.removeToken('0xa'); - - const tokensFirst = preferencesController.getTokens(); - assert.equal(tokensFirst.length, 1, 'one token removed in network'); - - const [token1] = tokensFirst; - assert.deepEqual(token1, { - address: '0xb', - symbol: 'B', - decimals: 5, - isERC721: false, - }); - - switchToRinkeby(); - const tokensSecond = preferencesController.getTokens(); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'token deleted for network', - ); - }); - }); - - describe('on setSelectedAddress', function () { - it('should update tokens from its state on corresponding address', async function () { - preferencesController.setAddresses(['0x7e57e2', '0x7e57e3']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - await preferencesController.setSelectedAddress('0x7e57e3'); - await preferencesController.addToken('0xa', 'C', 4); - await preferencesController.addToken('0xb', 'D', 5); - - await preferencesController.setSelectedAddress('0x7e57e2'); - const initialTokensFirst = preferencesController.getTokens(); - await preferencesController.setSelectedAddress('0x7e57e3'); - const initialTokensSecond = preferencesController.getTokens(); - - assert.notDeepEqual( - initialTokensFirst, - initialTokensSecond, - 'tokens not equal for different accounts and tokens', - ); - - await preferencesController.setSelectedAddress('0x7e57e2'); - const tokensFirst = preferencesController.getTokens(); - await preferencesController.setSelectedAddress('0x7e57e3'); - const tokensSecond = preferencesController.getTokens(); - - assert.deepEqual( - tokensFirst, - initialTokensFirst, - 'tokens equal for same account', - ); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'tokens equal for same account', - ); - }); - }); - - describe('on updateStateNetworkType', function () { - it('should remove a token from its state on corresponding network', async function () { - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - const initialTokensFirst = preferencesController.getTokens(); - switchToRinkeby(); - await preferencesController.addToken('0xa', 'C', 4); - await preferencesController.addToken('0xb', 'D', 5); - const initialTokensSecond = preferencesController.getTokens(); - - assert.notDeepEqual( - initialTokensFirst, - initialTokensSecond, - 'tokens not equal for different networks and tokens', - ); - - switchToMainnet(); - const tokensFirst = preferencesController.getTokens(); - switchToRinkeby(); - const tokensSecond = preferencesController.getTokens(); - assert.deepEqual( - tokensFirst, - initialTokensFirst, - 'tokens equal for same network', - ); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'tokens equal for same network', - ); - }); - }); - - describe('on watchAsset', function () { - let req, stubHandleWatchAssetERC20; - const sandbox = sinon.createSandbox(); - - beforeEach(function () { - req = { method: 'wallet_watchAsset', params: {} }; - stubHandleWatchAssetERC20 = sandbox.stub( - preferencesController, - '_handleWatchAssetERC20', - ); - }); - - after(function () { - sandbox.restore(); - }); - - it('should error if passed no type', async function () { - await assert.rejects( - () => preferencesController.requestWatchAsset(req), - { message: 'Asset of type "undefined" not supported.' }, - 'should have errored', - ); - }); - - it('should error if method is not supported', async function () { - req.params.type = 'someasset'; - await assert.rejects( - () => preferencesController.requestWatchAsset(req), - { message: 'Asset of type "someasset" not supported.' }, - 'should have errored', - ); - }); - - it('should handle ERC20 type', async function () { - req.params.type = 'ERC20'; - await preferencesController.requestWatchAsset(req); - sandbox.assert.called(stubHandleWatchAssetERC20); - }); - }); - - describe('on watchAsset of type ERC20', function () { - let req; - - const sandbox = sinon.createSandbox(); - beforeEach(function () { - req = { params: { type: 'ERC20' } }; - }); - after(function () { - sandbox.restore(); - }); - - it('should add suggested token', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - const image = 'someimage'; - req.params.options = { address, symbol, decimals, image }; - - sandbox - .stub(preferencesController, '_validateERC20AssetParams') - .returns(true); - preferencesController.openPopup = async () => undefined; - - await preferencesController._handleWatchAssetERC20(req.params.options); - const suggested = preferencesController.getSuggestedTokens(); - assert.equal( - Object.keys(suggested).length, - 1, - `one token added ${Object.keys(suggested)}`, - ); - - assert.equal( - suggested[address].address, - address, - 'set address correctly', - ); - assert.equal(suggested[address].symbol, symbol, 'set symbol correctly'); - assert.equal( - suggested[address].decimals, - decimals, - 'set decimals correctly', - ); - assert.equal(suggested[address].image, image, 'set image correctly'); - }); - - it('should add token correctly if user confirms', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - const image = 'someimage'; - req.params.options = { address, symbol, decimals, image }; - - sandbox - .stub(preferencesController, '_validateERC20AssetParams') - .returns(true); - preferencesController.openPopup = async () => { - await preferencesController.addToken(address, symbol, decimals, image); - }; - - await preferencesController._handleWatchAssetERC20(req.params.options); - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, `one token added`); - const added = tokens[0]; - assert.equal(added.address, address, 'set address correctly'); - assert.equal(added.symbol, symbol, 'set symbol correctly'); - assert.equal(added.decimals, decimals, 'set decimals correctly'); - - const assetImages = preferencesController.getAssetImages(); - assert.ok(assetImages[address], `set image correctly`); - }); - it('should validate ERC20 asset correctly', async function () { - const validate = preferencesController._validateERC20AssetParams; - - assert.doesNotThrow(() => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - decimals: 0, - }), - ); - assert.doesNotThrow(() => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABCDEFGHIJK', - decimals: 0, - }), - ); - - assert.throws( - () => validate({ symbol: 'ABC', decimals: 0 }), - 'missing address should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - decimals: 0, - }), - 'missing symbol should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - }), - 'missing decimals should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABCDEFGHIJKLM', - decimals: 0, - }), - 'long symbol should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: '', - decimals: 0, - }), - 'empty symbol should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - decimals: -1, - }), - 'decimals < 0 should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - decimals: 38, - }), - 'decimals > 36 should fail', - ); - assert.throws( - () => validate({ address: '0x123', symbol: 'ABC', decimals: 0 }), - 'invalid address should fail', - ); - }); - }); - describe('setPasswordForgotten', function () { it('should default to false', function () { const state = preferencesController.store.getState(); diff --git a/app/scripts/controllers/token-rates-controller.test.js b/app/scripts/controllers/token-rates-controller.test.js index 444e53977..31032495e 100644 --- a/app/scripts/controllers/token-rates-controller.test.js +++ b/app/scripts/controllers/token-rates-controller.test.js @@ -1,31 +1,62 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import { ObservableStore } from '@metamask/obs-store'; +import { TokensController } from '@metamask/controllers'; import TokenRatesController from './token-rates'; +import NetworkController from './network'; +import PreferencesController from './preferences'; + +const networkControllerProviderConfig = { + getAccounts: () => undefined, +}; describe('TokenRatesController', function () { - let nativeCurrency; - let getNativeCurrency; + let nativeCurrency, + getNativeCurrency, + network, + provider, + preferences, + tokensController; beforeEach(function () { nativeCurrency = 'ETH'; getNativeCurrency = () => nativeCurrency; + network = new NetworkController(); + network.setInfuraProjectId('foo'); + network.initializeProvider(networkControllerProviderConfig); + provider = network.getProviderAndBlockTracker().provider; + preferences = new PreferencesController({ network, provider }); + tokensController = new TokensController({ + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: network.store.subscribe.bind(network.store), + }); + sinon.stub(network, 'getLatestBlock').callsFake(() => Promise.resolve({})); + sinon.stub(tokensController, '_instantiateNewEthersProvider').returns(null); + sinon + .stub(tokensController, '_detectIsERC721') + .returns(Promise.resolve(false)); }); - it('should listen for preferences store updates', function () { - const preferences = new ObservableStore({ tokens: [] }); - preferences.putState({ tokens: ['foo'] }); + it('should listen for tokenControllers state updates', async function () { const controller = new TokenRatesController({ - preferences, + tokensController, getNativeCurrency, }); - assert.deepEqual(controller._tokens, ['foo']); + await tokensController.addToken('0x1', 'TEST', 1); + assert.deepEqual(controller._tokens, [ + { + address: '0x1', + decimals: 1, + symbol: 'TEST', + image: undefined, + isERC721: false, + }, + ]); }); it('should poll on correct interval', async function () { const stub = sinon.stub(global, 'setInterval'); - const preferences = new ObservableStore({ tokens: [] }); - preferences.putState({ tokens: ['foo'] }); const controller = new TokenRatesController({ - preferences, + tokensController, getNativeCurrency, }); controller.start(1337); diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 2bc7f2a19..319265593 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -20,11 +20,11 @@ export default class TokenRatesController { * * @param {Object} [config] - Options to configure controller */ - constructor({ preferences, getNativeCurrency } = {}) { + constructor({ tokensController, getNativeCurrency } = {}) { this.store = new ObservableStore(); this.getNativeCurrency = getNativeCurrency; - this.tokens = preferences.getState().tokens; - preferences.subscribe(({ tokens = [] }) => { + this.tokens = tokensController.state.tokens; + tokensController.subscribe(({ tokens = [] }) => { this.tokens = tokens; }); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index aebcae891..95408eee3 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -32,7 +32,8 @@ async function watchAssetHandler( { handleWatchAssetRequest }, ) { try { - res.result = await handleWatchAssetRequest(req); + const { options: asset, type } = req.params; + res.result = await handleWatchAssetRequest(asset, type); return end(); } catch (error) { return end(error); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0ee57bbf8..e06e2219e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -25,6 +25,7 @@ import { NotificationController, GasFeeController, TokenListController, + TokensController, } from '@metamask/controllers'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { @@ -62,10 +63,10 @@ import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'; import PersonalMessageManager from './lib/personal-message-manager'; import TypedMessageManager from './lib/typed-message-manager'; import TransactionController from './controllers/transactions'; -import TokenRatesController from './controllers/token-rates'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; import { PermissionsController } from './controllers/permissions'; +import TokenRatesController from './controllers/token-rates'; import { NOTIFICATION_NAMES } from './controllers/permissions/enums'; import getRestrictedMethods from './controllers/permissions/restrictedMethods'; import nodeify from './lib/nodeify'; @@ -160,6 +161,17 @@ export default class MetamaskController extends EventEmitter { migrateAddressBookState: this.migrateAddressBookState.bind(this), }); + this.tokensController = new TokensController({ + onPreferencesStateChange: this.preferencesController.store.subscribe.bind( + this.preferencesController.store, + ), + onNetworkStateChange: this.networkController.store.subscribe.bind( + this.networkController.store, + ), + config: { provider: this.provider }, + state: initState.TokensController, + }); + this.metaMetricsController = new MetaMetricsController({ segment, preferencesStore: this.preferencesController.store, @@ -270,9 +282,8 @@ export default class MetamaskController extends EventEmitter { initState.NotificationController, ); - // token exchange rate tracker this.tokenRatesController = new TokenRatesController({ - preferences: this.preferencesController.store, + tokensController: this.tokensController, getNativeCurrency: () => { const { ticker } = this.networkController.getProviderConfig(); return ticker ?? 'ETH'; @@ -342,6 +353,10 @@ export default class MetamaskController extends EventEmitter { preferencesController: this.preferencesController, }); + this.tokensController.hub.on('pendingSuggestedAsset', async () => { + await opts.openPopup(); + }); + const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]; this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, @@ -375,6 +390,7 @@ export default class MetamaskController extends EventEmitter { this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, + tokensController: this.tokensController, network: this.networkController, keyringMemStore: this.keyringController.memStore, tokenList: this.tokenListController, @@ -555,6 +571,7 @@ export default class MetamaskController extends EventEmitter { NotificationController: this.notificationController, GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, + TokensController: this.tokensController, }); this.memStore = new ComposableObservableStore({ @@ -588,6 +605,7 @@ export default class MetamaskController extends EventEmitter { NotificationController: this.notificationController, GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, + TokensController: this.tokensController, }, controllerMessenger: this.controllerMessenger, }); @@ -768,6 +786,7 @@ export default class MetamaskController extends EventEmitter { swapsController, threeBoxController, txController, + tokensController, } = this; return { @@ -837,18 +856,22 @@ export default class MetamaskController extends EventEmitter { preferencesController.setSelectedAddress, preferencesController, ), - addToken: nodeify(preferencesController.addToken, preferencesController), + addToken: nodeify(tokensController.addToken, tokensController), + rejectWatchAsset: nodeify( + tokensController.rejectWatchAsset, + tokensController, + ), + acceptWatchAsset: nodeify( + tokensController.acceptWatchAsset, + tokensController, + ), updateTokenType: nodeify( - preferencesController.updateTokenType, - preferencesController, + tokensController.updateTokenType, + tokensController, ), removeToken: nodeify( - preferencesController.removeToken, - preferencesController, - ), - removeSuggestedTokens: nodeify( - preferencesController.removeSuggestedTokens, - preferencesController, + tokensController.removeAndIgnoreToken, + tokensController, ), setAccountLabel: nodeify( preferencesController.setAccountLabel, @@ -1295,23 +1318,50 @@ export default class MetamaskController extends EventEmitter { async fetchInfoToSync() { // Preferences const { - accountTokens, currentLocale, frequentRpcList, identities, selectedAddress, - tokens, + useTokenDetection, } = this.preferencesController.store.getState(); + const { tokenList } = this.tokenListController.state; + const preferences = { - accountTokens, currentLocale, frequentRpcList, identities, selectedAddress, - tokens, }; + // Tokens + const { allTokens, allIgnoredTokens } = this.tokensController.state; + + // Filter ERC20 tokens + const allERC20Tokens = {}; + + Object.keys(allTokens).forEach((chainId) => { + allERC20Tokens[chainId] = {}; + Object.keys(allTokens[chainId]).forEach((accountAddress) => { + const checksummedAccountAddress = toChecksumHexAddress(accountAddress); + allERC20Tokens[chainId][checksummedAccountAddress] = allTokens[chainId][ + checksummedAccountAddress + ].filter((asset) => { + if (asset.isERC721 === undefined) { + const address = useTokenDetection + ? asset.address + : toChecksumHexAddress(asset.address); + if (tokenList[address] !== undefined && tokenList[address].erc20) { + return true; + } + } else if (asset.isERC721 === false) { + return true; + } + return false; + }); + }); + }); + // Accounts const hdKeyring = this.keyringController.getKeyringsByType( 'HD Key Tree', @@ -1351,6 +1401,7 @@ export default class MetamaskController extends EventEmitter { accounts, preferences, transactions, + tokens: { allTokens: allERC20Tokens, allIgnoredTokens }, network: this.networkController.store.getState(), }; } @@ -2366,8 +2417,8 @@ export default class MetamaskController extends EventEmitter { sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), - handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( - this.preferencesController, + handleWatchAssetRequest: this.tokensController.watchAsset.bind( + this.tokensController, ), getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( this.alertController, diff --git a/app/scripts/migrations/063.js b/app/scripts/migrations/063.js new file mode 100644 index 000000000..95985bf37 --- /dev/null +++ b/app/scripts/migrations/063.js @@ -0,0 +1,78 @@ +import { cloneDeep } from 'lodash'; + +const version = 63; + +/** + * Moves token state from preferences controller to TokensController + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + const newState = transformState(state); + versionedData.data = newState; + return versionedData; + }, +}; + +function transformState(state) { + const accountTokens = state?.PreferencesController?.accountTokens; + const accountHiddenTokens = state?.PreferencesController?.accountHiddenTokens; + + const newAllTokens = {}; + if (accountTokens) { + Object.keys(accountTokens).forEach((accountAddress) => { + Object.keys(accountTokens[accountAddress]).forEach((chainId) => { + const tokensArray = accountTokens[accountAddress][chainId]; + if (newAllTokens[chainId] === undefined) { + newAllTokens[chainId] = { [accountAddress]: tokensArray }; + } else { + newAllTokens[chainId] = { + ...newAllTokens[chainId], + [accountAddress]: tokensArray, + }; + } + }); + }); + } + + const newAllIgnoredTokens = {}; + if (accountHiddenTokens) { + Object.keys(accountHiddenTokens).forEach((accountAddress) => { + Object.keys(accountHiddenTokens[accountAddress]).forEach((chainId) => { + const ignoredTokensArray = accountHiddenTokens[accountAddress][chainId]; + if (newAllIgnoredTokens[chainId] === undefined) { + newAllIgnoredTokens[chainId] = { + [accountAddress]: ignoredTokensArray, + }; + } else { + newAllIgnoredTokens[chainId] = { + ...newAllIgnoredTokens[chainId], + [accountAddress]: ignoredTokensArray, + }; + } + }); + }); + } + + if (state.TokensController) { + state.TokensController.allTokens = newAllTokens; + state.TokensController.allIgnoredTokens = newAllIgnoredTokens; + } else { + state.TokensController = { + allTokens: newAllTokens, + allIgnoredTokens: newAllIgnoredTokens, + }; + } + + delete state?.PreferencesController?.accountHiddenTokens; + delete state?.PreferencesController?.accountTokens; + delete state?.PreferencesController?.assetImages; + delete state?.PreferencesController?.hiddenTokens; + delete state?.PreferencesController?.tokens; + delete state?.PreferencesController?.suggestedTokens; + + return state; +} diff --git a/app/scripts/migrations/063.test.js b/app/scripts/migrations/063.test.js new file mode 100644 index 000000000..30804a35c --- /dev/null +++ b/app/scripts/migrations/063.test.js @@ -0,0 +1,251 @@ +import { strict as assert } from 'assert'; +import migration63 from './063'; + +describe('migration #63', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 62, + }, + data: {}, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 63, + }); + }); + + it('should move accountTokens data from PreferencesController to TokensController allTokens field and rotate structure from [accountAddress][chainId] to [chainId][accountAddress]', async function () { + const oldAccountTokens = { + '0x00000000000': { + '0x1': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'DAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'UNI', + }, + ], + '0x89': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'LINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDT', + }, + ], + }, + '0x1111111111': { + '0x1': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'FAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'PUNI', + }, + ], + '0x89': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'SLINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDC', + }, + ], + }, + }; + + const expectedTokens = { + '0x1': { + '0x00000000000': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'DAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'UNI', + }, + ], + '0x1111111111': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'FAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'PUNI', + }, + ], + }, + '0x89': { + '0x00000000000': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'LINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDT', + }, + ], + '0x1111111111': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'SLINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDC', + }, + ], + }, + }; + + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + accountTokens: oldAccountTokens, + }, + }, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data, { + TokensController: { + allTokens: expectedTokens, + allIgnoredTokens: {}, + }, + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + }, + }); + }); + + it('should move accountHiddenTokens data from PreferencesController to TokensController allIgnoredTokens field and rotate structure from [accountAddress][chainId] to [chainId][accountAddress]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + accountTokens: {}, + accountHiddenTokens: { + '0x1111111111': { + '0x1': ['0x000000000000'], + '0x89': ['0x11111111111'], + }, + '0x222222': { + '0x4': ['0x000011112222'], + }, + '0x333333': { + '0x5': ['0x000022223333'], + '0x1': ['0x000033333344'], + }, + }, + }, + }, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data, { + TokensController: { + allTokens: {}, + allIgnoredTokens: { + '0x1': { + '0x1111111111': ['0x000000000000'], + '0x333333': ['0x000033333344'], + }, + '0x89': { + '0x1111111111': ['0x11111111111'], + }, + '0x4': { + '0x222222': ['0x000011112222'], + }, + '0x5': { + '0x333333': ['0x000022223333'], + }, + }, + }, + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + }, + }); + }); + + it('should should remove all token related state from the preferences controller', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + accountTokens: {}, + accountHiddenTokens: {}, + tokens: {}, + hiddenTokens: {}, + assetImages: {}, + suggestedTokens: {}, + }, + }, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data, { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + }, + TokensController: { + allTokens: {}, + allIgnoredTokens: {}, + }, + }); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 693537f80..4744bc531 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -66,6 +66,7 @@ import m059 from './059'; import m060 from './060'; import m061 from './061'; import m062 from './062'; +import m063 from './063'; const migrations = [ m002, @@ -129,6 +130,7 @@ const migrations = [ m060, m061, m062, + m063, ]; export default migrations; diff --git a/package.json b/package.json index dd71fc8a9..0f26e77fd 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,6 @@ "fast-safe-stringify": "^2.0.7", "fuse.js": "^3.2.0", "globalthis": "^1.0.1", - "human-standard-collectible-abi": "^1.0.2", "human-standard-token-abi": "^2.0.0", "immer": "^8.0.1", "json-rpc-engine": "^6.1.0", diff --git a/test/e2e/fixtures/custom-token/state.json b/test/e2e/fixtures/custom-token/state.json index bd85436ad..1e2bff3bf 100644 --- a/test/e2e/fixtures/custom-token/state.json +++ b/test/e2e/fixtures/custom-token/state.json @@ -1,150 +1,165 @@ { "data": { + "AlertController": { + "alertEnabledness": { + "unconnectedAccount": true, + "web3ShimUsage": true + }, + "unconnectedAccountAlertShownOrigins": {}, + "web3ShimUsageOrigins": {} + }, "AppStateController": { - "mkrMigrationReminderTimestamp": null + "connectedStatusPopoverHasBeenShown": true, + "defaultHomeActiveTabName": null, + "recoveryPhraseReminderHasBeenShown": true, + "recoveryPhraseReminderLastShown": 1627317428214 }, "CachedBalancesController": { "cachedBalances": { - "4": {} + "0x4": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": "0x0" + } } }, "CurrencyController": { - "conversionDate": 1575697244.188, - "conversionRate": 149.61, + "conversionDate": 1626907353.891, + "conversionRate": 1968.5, "currentCurrency": "usd", - "nativeCurrency": "ETH" + "nativeCurrency": "ETH", + "pendingCurrentCurrency": null, + "pendingNativeCurrency": null, + "usdConversionRate": 1968.5 }, "IncomingTransactionsController": { "incomingTransactions": {}, - "incomingTxLastFetchedBlocksByNetwork": { - "goerli": null, - "kovan": null, - "mainnet": null, - "rinkeby": 5570536 + "incomingTxLastFetchedBlockByChainId": { + "0x1": null, + "0x2a": null, + "0x3": null, + "0x4": 8977934, + "0x5": null } }, "KeyringController": { "vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" }, + "MetaMetricsController": { + "metaMetricsId": "0xff3e952b9f5a27ffcab42b0b4abf689e77dcc1f9f441871dc962d622b089fb51", + "participateInMetaMetrics": true + }, "NetworkController": { "network": "1337", + "networkDetails": { + "EIPS": {} + }, + "previousProviderStore": { + "chainId": "0x4", + "ticker": "ETH", + "type": "rinkeby" + }, "provider": { + "chainId": "0x539", "nickname": "Localhost 8545", + "rpcPrefs": {}, "rpcUrl": "http://localhost:8545", - "chainId": "0x539", "ticker": "ETH", "type": "rpc" } }, "NotificationController": { - "notifications": { - "1": { - "isShown": true - }, - "3": { - "isShown": true - }, - "5": { - "isShown": true - }, - "6": { - "isShown": true - } - } + "notifications": {} }, "OnboardingController": { "onboardingTabs": {}, - "seedPhraseBackedUp": false + "seedPhraseBackedUp": true }, - "PermissionsMetadata": { - "domainMetadata": { - "metamask.github.io": { - "icon": null, - "name": "M E T A M A S K M E S H T E S T" - } - }, - "permissionsHistory": {}, - "permissionsLog": [ - { - "id": 746677923, - "method": "eth_accounts", - "methodType": "restricted", - "origin": "metamask.github.io", - "request": { - "id": 746677923, - "jsonrpc": "2.0", - "method": "eth_accounts", - "origin": "metamask.github.io", - "params": [] - }, - "requestTime": 1575697241368, - "response": { - "id": 746677923, - "jsonrpc": "2.0", - "result": [] - }, - "responseTime": 1575697241370, - "success": true - } - ] + "PermissionsController": { + "domains": {}, + "permissionsDescriptions": {}, + "permissionsRequests": [] }, "PreferencesController": { - "accountTokens": { - "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { - "0x539": [ - { - "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", - "symbol": "TST", - "decimals": 4 - } - ], - "rinkeby": [], - "ropsten": [] - } - }, - "assetImages": {}, "completedOnboarding": true, "currentLocale": "en", + "dismissSeedBackUpReminder": true, "featureFlags": { - "showIncomingTransactions": true, - "transactionTime": false + "showIncomingTransactions": true }, - "firstTimeFlowType": "create", + "firstTimeFlowType": "import", "forgottenPassword": false, - "frequentRpcListDetail": [], + "frequentRpcListDetail": [ + { + "chainId": "0x539", + "nickname": "Localhost 8545", + "rpcPrefs": {}, + "rpcUrl": "http://localhost:8545", + "ticker": "ETH" + } + ], "identities": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "lastSelected": 1626907346643, "name": "Account 1" } }, + "infuraBlocked": false, + "ipfsGateway": "dweb.link", "knownMethodData": {}, "lostIdentities": {}, - "metaMetricsId": null, - "participateInMetaMetrics": false, "preferences": { + "hideZeroBalanceTokens": false, + "showFiatInTestnets": false, "useNativeCurrencyAsPrimaryCurrency": true }, "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", - "suggestedTokens": {}, + "useBlockie": false, + "useLedgerLive": false, + "useNonceField": false, + "usePhishDetect": true, + "useStaticTokenList": false + }, + "TokenListController": { + "tokenList": {}, + "tokensChainsCache": {} + }, + "TokensController": { + "allTokens": { + "0x539": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": [ + { + "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", + "decimals": 4, + "image": null, + "isERC721": false, + "symbol": "TST" + } + ] + } + }, + "ignoredTokens": [], + "suggestedAssets": [], + "allIgnoredTokens": {}, "tokens": [ { "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", - "symbol": "TST", - "decimals": 4 + "decimals": 4, + "image": null, + "isERC721": false, + "symbol": "TST" } - ], - "useBlockie": false, - "useNonceField": false, - "usePhishDetect": true + ] + }, + "TransactionController": { + "transactions": {} }, "config": {}, "firstTimeInfo": { - "date": 1575697234195, - "version": "7.7.0" + "date": 1626907328205, + "version": "9.8.1" } }, "meta": { - "version": 40 + "version": 63 } } diff --git a/test/e2e/fixtures/imported-account/state.json b/test/e2e/fixtures/imported-account/state.json index aa2fc309b..edb463d87 100644 --- a/test/e2e/fixtures/imported-account/state.json +++ b/test/e2e/fixtures/imported-account/state.json @@ -452,6 +452,7 @@ }, "assetImages": {}, "completedOnboarding": true, + "dismissSeedBackUpReminder": true, "currentLocale": "en", "featureFlags": { "showIncomingTransactions": true, diff --git a/test/e2e/tests/add-hide-token.spec.js b/test/e2e/tests/add-hide-token.spec.js index 8b98378cd..b8253f142 100644 --- a/test/e2e/tests/add-hide-token.spec.js +++ b/test/e2e/tests/add-hide-token.spec.js @@ -27,6 +27,7 @@ describe('Hide token', function () { css: '.asset-list-item__token-button', text: '0 TST', }); + await driver.clickElement('.popover-header__button'); let assets = await driver.findElements('.asset-list-item'); assert.equal(assets.length, 2); diff --git a/test/e2e/tests/contract-interactions.spec.js b/test/e2e/tests/contract-interactions.spec.js index a88a12032..f998b0376 100644 --- a/test/e2e/tests/contract-interactions.spec.js +++ b/test/e2e/tests/contract-interactions.spec.js @@ -80,6 +80,7 @@ describe('Deploy contract and call contract methods', function () { await driver.switchToWindow(dapp); await driver.clickElement('#depositButton'); await driver.waitUntilXWindowHandles(3); + windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle( 'MetaMask Notification', diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 28d5f578e..0ab2af228 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -20,7 +20,6 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle: PropTypes.bool, identiconAddress: PropTypes.string, nonce: PropTypes.string, - assetImage: PropTypes.string, subtitleComponent: PropTypes.node, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), titleComponent: PropTypes.node, @@ -77,7 +76,6 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle, identiconAddress, nonce, - assetImage, detailsComponent, dataComponent, warning, @@ -111,7 +109,6 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle={hideSubtitle} identiconAddress={identiconAddress} nonce={nonce} - assetImage={assetImage} origin={origin} /> {this.renderContent()} diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index cd33ee357..25ab144a0 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -13,7 +13,6 @@ const ConfirmPageContainerSummary = (props) => { className, identiconAddress, nonce, - assetImage, origin, } = props; @@ -36,7 +35,6 @@ const ConfirmPageContainerSummary = (props) => { className="confirm-page-container-summary__identicon" diameter={36} address={identiconAddress} - image={assetImage} /> )}
@@ -61,7 +59,6 @@ ConfirmPageContainerSummary.propTypes = { className: PropTypes.string, identiconAddress: PropTypes.string, nonce: PropTypes.string, - assetImage: PropTypes.string, origin: PropTypes.string.isRequired, }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index cb32c41fd..5de2a4fd1 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -42,7 +42,6 @@ export default class ConfirmPageContainer extends Component { detailsComponent: PropTypes.node, identiconAddress: PropTypes.string, nonce: PropTypes.string, - assetImage: PropTypes.string, warning: PropTypes.string, unapprovedTxCount: PropTypes.number, origin: PropTypes.string.isRequired, @@ -98,7 +97,6 @@ export default class ConfirmPageContainer extends Component { identiconAddress, nonce, unapprovedTxCount, - assetImage, warning, totalTx, positionOfCurrentTx, @@ -120,7 +118,6 @@ export default class ConfirmPageContainer extends Component { showAddToAddressBookModal, contact = {}, } = this.props; - const renderAssetImage = contentComponent || !identiconAddress; const showAddToAddressDialog = contact.name === undefined && toAddress !== undefined; @@ -153,7 +150,6 @@ export default class ConfirmPageContainer extends Component { recipientAddress={toAddress} recipientEns={toEns} recipientNickname={toNickname} - assetImage={renderAssetImage ? assetImage : undefined} /> )} @@ -181,7 +177,6 @@ export default class ConfirmPageContainer extends Component { errorKey={errorKey} identiconAddress={identiconAddress} nonce={nonce} - assetImage={assetImage} warning={warning} onCancelAll={onCancelAll} onCancel={onCancel} diff --git a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js index f8bdc5546..934ca70d0 100644 --- a/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js +++ b/ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js @@ -8,7 +8,6 @@ import Button from '../../../ui/button'; function mapStateToProps(state) { return { token: state.appState.modal.modalState.props.token, - assetImages: state.metamask.assetImages, }; } @@ -31,19 +30,18 @@ class HideTokenConfirmationModal extends Component { static propTypes = { hideToken: PropTypes.func.isRequired, hideModal: PropTypes.func.isRequired, - assetImages: PropTypes.object.isRequired, token: PropTypes.shape({ symbol: PropTypes.string, address: PropTypes.string, + image: PropTypes.string, }), }; state = {}; render() { - const { token, hideToken, hideModal, assetImages } = this.props; - const { symbol, address } = token; - const image = assetImages[address]; + const { token, hideToken, hideModal } = this.props; + const { symbol, address, image } = token; return (
diff --git a/ui/components/app/token-cell/token-cell.js b/ui/components/app/token-cell/token-cell.js index 18a7cd698..8be05c823 100644 --- a/ui/components/app/token-cell/token-cell.js +++ b/ui/components/app/token-cell/token-cell.js @@ -21,7 +21,6 @@ export default function TokenCell({ const t = useI18nContext(); const formattedFiat = useTokenFiatAmount(address, string, symbol); - const warning = balanceError ? ( {t('troubleTokenBalances')} diff --git a/ui/components/app/token-list/token-list.js b/ui/components/app/token-list/token-list.js index 9d0fdc77e..5ff3eb382 100644 --- a/ui/components/app/token-list/token-list.js +++ b/ui/components/app/token-list/token-list.js @@ -6,15 +6,11 @@ import { useSelector } from 'react-redux'; import TokenCell from '../token-cell'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { - getAssetImages, - getShouldHideZeroBalanceTokens, -} from '../../../selectors'; +import { getShouldHideZeroBalanceTokens } from '../../../selectors'; import { getTokens } from '../../../ducks/metamask/metamask'; export default function TokenList({ onTokenClick }) { const t = useI18nContext(); - const assetImages = useSelector(getAssetImages); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, ); @@ -46,7 +42,6 @@ export default function TokenList({ onTokenClick }) { return (
{tokensWithBalances.map((tokenData, index) => { - tokenData.image = assetImages[tokenData.address]; return ; })}
diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index bfd294000..7a86e1513 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -12,6 +12,7 @@ import Button from '../../ui/button'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../shared/constants/swaps'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; +import { isEqualCaseInsensitive } from '../../../helpers/utils/util'; const PAGE_INCREMENT = 10; @@ -28,7 +29,7 @@ const getTransactionGroupRecipientAddressFilter = ( ) => { return ({ initialTransaction: { txParams } }) => { return ( - txParams?.to === recipientAddress || + isEqualCaseInsensitive(txParams?.to, recipientAddress) || (txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] && txParams.data.match(recipientAddress.slice(2))) ); diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index e40003067..6bdf029d3 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -20,7 +20,6 @@ import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { - getAssetImages, getCurrentKeyring, getIsSwapsChain, } from '../../../selectors/selectors'; @@ -42,8 +41,6 @@ const TokenOverview = ({ className, token }) => { }, }); const history = useHistory(); - const assetImages = useSelector(getAssetImages); - const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = keyring.type.search('Hardware') !== -1; const { tokensWithBalances } = useTokenTracker([token]); @@ -109,7 +106,7 @@ const TokenOverview = ({ className, token }) => { dispatch( setSwapsFromToken({ ...token, - iconUrl: assetImages[token.address], + iconUrl: token.image, balance, string: balanceToRender, }), @@ -136,11 +133,7 @@ const TokenOverview = ({ className, token }) => { } className={className} icon={ - + } /> ); @@ -152,6 +145,7 @@ TokenOverview.propTypes = { address: PropTypes.string.isRequired, decimals: PropTypes.number, symbol: PropTypes.string, + image: PropTypes.string, isERC721: PropTypes.bool, }).isRequired, }; diff --git a/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js index 30e42e46f..635687053 100644 --- a/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -98,7 +98,6 @@ SenderAddress.propTypes = { function RecipientWithAddress({ checksummedRecipientAddress, - assetImage, onRecipientClick, addressOnly, recipientNickname, @@ -135,11 +134,7 @@ function RecipientWithAddress({ > {!addressOnly && (
- +
)} {recipientAddress ? ( `metamask/confirm-transaction/${action}`; @@ -283,8 +284,8 @@ export function setTransactionToConfirm(transactionId) { const tokenData = getTokenData(data); const tokens = getTokens(state); - const currentToken = tokens?.find( - ({ address }) => tokenAddress === address, + const currentToken = tokens?.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), ); dispatch( diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index b2bef9558..d78726a6a 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -22,7 +22,6 @@ export default function reduceMetamask(state = {}, action) { frequentRpcList: [], addressBook: [], contractExchangeRates: {}, - tokens: [], pendingTokens: {}, customNonceValue: '', useBlockie: false, @@ -89,12 +88,6 @@ export default function reduceMetamask(state = {}, action) { return Object.assign(metamaskState, { identities }); } - case actionConstants.UPDATE_TOKENS: - return { - ...metamaskState, - tokens: action.newTokens, - }; - case actionConstants.UPDATE_CUSTOM_NONCE: return { ...metamaskState, diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index 702c7c319..51c88033d 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -174,24 +174,6 @@ describe('MetaMask Reducers', () => { }); }); - it('updates tokens', () => { - const newTokens = { - address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', - decimals: 18, - symbol: 'META', - }; - - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_TOKENS, - newTokens, - }, - ); - - expect(state.tokens).toStrictEqual(newTokens); - }); - it('toggles account menu', () => { const state = reduceMetamask( {}, diff --git a/ui/hooks/useCurrentAsset.js b/ui/hooks/useCurrentAsset.js index 068d076e7..4292a630b 100644 --- a/ui/hooks/useCurrentAsset.js +++ b/ui/hooks/useCurrentAsset.js @@ -3,6 +3,7 @@ import { useRouteMatch } from 'react-router-dom'; import { getTokens } from '../ducks/metamask/metamask'; import { getCurrentChainId } from '../selectors'; import { ASSET_ROUTE } from '../helpers/constants/routes'; +import { isEqualCaseInsensitive } from '../helpers/utils/util'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, ETH_SWAPS_TOKEN_OBJECT, @@ -26,7 +27,10 @@ export function useCurrentAsset() { const tokenAddress = match?.params?.asset; const knownTokens = useSelector(getTokens); const token = - tokenAddress && knownTokens.find(({ address }) => address === tokenAddress); + tokenAddress && + knownTokens.find(({ address }) => + isEqualCaseInsensitive(address, tokenAddress), + ); const chainId = useSelector(getCurrentChainId); return ( diff --git a/ui/hooks/useTokenTracker.js b/ui/hooks/useTokenTracker.js index abcf03233..5ad2fb5af 100644 --- a/ui/hooks/useTokenTracker.js +++ b/ui/hooks/useTokenTracker.js @@ -3,6 +3,7 @@ import TokenTracker from '@metamask/eth-token-tracker'; import { useSelector } from 'react-redux'; import { getCurrentChainId, getSelectedAddress } from '../selectors'; import { SECOND } from '../../shared/constants/time'; +import { isEqualCaseInsensitive } from '../helpers/utils/util'; import { useEqualityCheck } from './useEqualityCheck'; export function useTokenTracker( @@ -26,10 +27,14 @@ export function useTokenTracker( // TODO: improve this pattern for adding this field when we improve support for // EIP721 tokens. const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => { - const additionalTokenData = memoizedTokens.find( - (t) => t.address === token.address, + const additionalTokenData = memoizedTokens.find((t) => + isEqualCaseInsensitive(t.address, token.address), ); - return { ...token, isERC721: additionalTokenData?.isERC721 }; + return { + ...token, + isERC721: additionalTokenData?.isERC721, + image: additionalTokenData?.image, + }; }); setTokensWithBalances(matchingTokensWithIsERC721Flag); setLoading(false); diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index 94fb83846..122511a35 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -8,10 +8,12 @@ import { camelCaseToCapitalize } from '../helpers/utils/common.util'; import { PRIMARY, SECONDARY } from '../helpers/constants/common'; import { getTokenAddressParam } from '../helpers/utils/token-util'; import { + isEqualCaseInsensitive, formatDateWithYearContext, shortenAddress, stripHttpSchemes, } from '../helpers/utils/util'; + import { PENDING_STATUS_HASH, TOKEN_CATEGORY_HASH, @@ -97,7 +99,9 @@ export function useTransactionDisplayData(transactionGroup) { // hook to return null const token = isTokenCategory && - knownTokens.find(({ address }) => address === recipientAddress); + knownTokens.find(({ address }) => + isEqualCaseInsensitive(address, recipientAddress), + ); const tokenData = useTokenData( initialTransaction?.txParams?.data, isTokenCategory, diff --git a/ui/pages/asset/asset.js b/ui/pages/asset/asset.js index f10743a76..303d6fdeb 100644 --- a/ui/pages/asset/asset.js +++ b/ui/pages/asset/asset.js @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { Redirect, useParams } from 'react-router-dom'; import { getTokens } from '../../ducks/metamask/metamask'; import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import NativeAsset from './components/native-asset'; import TokenAsset from './components/token-asset'; @@ -12,7 +13,9 @@ const Asset = () => { const tokens = useSelector(getTokens); const { asset } = useParams(); - const token = tokens.find(({ address }) => address === asset); + const token = tokens.find(({ address }) => + isEqualCaseInsensitive(address, asset), + ); useEffect(() => { const el = document.querySelector('.app'); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js index 41d506644..c0ad9da58 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js @@ -5,6 +5,7 @@ import Identicon from '../../components/ui/identicon'; import TokenBalance from '../../components/ui/token-balance'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; export default class ConfirmAddSuggestedToken extends Component { static contextTypes = { @@ -14,28 +15,31 @@ export default class ConfirmAddSuggestedToken extends Component { static propTypes = { history: PropTypes.object, - addToken: PropTypes.func, + acceptWatchAsset: PropTypes.func, + rejectWatchAsset: PropTypes.func, mostRecentOverviewPage: PropTypes.string.isRequired, - pendingTokens: PropTypes.object, - removeSuggestedTokens: PropTypes.func, + suggestedAssets: PropTypes.array, tokens: PropTypes.array, }; componentDidMount() { - this._checkPendingTokens(); + this._checksuggestedAssets(); } componentDidUpdate() { - this._checkPendingTokens(); + this._checksuggestedAssets(); } - _checkPendingTokens() { - const { mostRecentOverviewPage, pendingTokens = {}, history } = this.props; + _checksuggestedAssets() { + const { + mostRecentOverviewPage, + suggestedAssets = [], + history, + } = this.props; - if (Object.keys(pendingTokens).length > 0) { + if (suggestedAssets.length > 0) { return; } - if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { global.platform.closeCurrentWindow(); } else { @@ -49,17 +53,19 @@ export default class ConfirmAddSuggestedToken extends Component { render() { const { - addToken, - pendingTokens, + suggestedAssets, tokens, - removeSuggestedTokens, + rejectWatchAsset, history, mostRecentOverviewPage, + acceptWatchAsset, } = this.props; - const pendingTokenKey = Object.keys(pendingTokens)[0]; - const pendingToken = pendingTokens[pendingTokenKey]; - const hasTokenDuplicates = this.checkTokenDuplicates(pendingTokens, tokens); - const reusesName = this.checkNameReuse(pendingTokens, tokens); + + const hasTokenDuplicates = this.checkTokenDuplicates( + suggestedAssets, + tokens, + ); + const reusesName = this.checkNameReuse(suggestedAssets, tokens); return (
@@ -90,27 +96,25 @@ export default class ConfirmAddSuggestedToken extends Component {
- {Object.entries(pendingTokens).map(([address, token]) => { - const { name, symbol, image } = token; - + {suggestedAssets.map(({ asset }) => { return (
- {this.getTokenName(name, symbol)} + {this.getTokenName(asset.name, asset.symbol)}
- +
); @@ -124,10 +128,11 @@ export default class ConfirmAddSuggestedToken extends Component { type="default" large className="page-container__footer-button" - onClick={() => { - removeSuggestedTokens().then(() => - history.push(mostRecentOverviewPage), + onClick={async () => { + await Promise.all( + suggestedAssets.map(async ({ id }) => rejectWatchAsset(id)), ); + history.push(mostRecentOverviewPage); }} > {this.context.t('cancel')} @@ -136,24 +141,25 @@ export default class ConfirmAddSuggestedToken extends Component { type="secondary" large className="page-container__footer-button" - disabled={pendingTokens.length === 0} - onClick={() => { - addToken(pendingToken) - .then(() => removeSuggestedTokens()) - .then(() => { + disabled={suggestedAssets.length === 0} + onClick={async () => { + await Promise.all( + suggestedAssets.map(async ({ asset, id }) => { + await acceptWatchAsset(id); this.context.trackEvent({ event: 'Token Added', category: 'Wallet', sensitiveProperties: { - token_symbol: pendingToken.symbol, - token_contract_address: pendingToken.address, - token_decimal_precision: pendingToken.decimals, - unlisted: pendingToken.unlisted, + token_symbol: asset.symbol, + token_contract_address: asset.address, + token_decimal_precision: asset.decimals, + unlisted: asset.unlisted, source: 'dapp', }, }); - }) - .then(() => history.push(mostRecentOverviewPage)); + }), + ); + history.push(mostRecentOverviewPage); }} > {this.context.t('addToken')} @@ -164,9 +170,11 @@ export default class ConfirmAddSuggestedToken extends Component { ); } - checkTokenDuplicates(pendingTokens, tokens) { - const pending = Object.keys(pendingTokens); - const existing = tokens.map((token) => token.address); + checkTokenDuplicates(suggestedAssets, tokens) { + const pending = suggestedAssets.map(({ asset }) => + asset.address.toUpperCase(), + ); + const existing = tokens.map((token) => token.address.toUpperCase()); const dupes = pending.filter((proposed) => { return existing.includes(proposed); }); @@ -175,20 +183,20 @@ export default class ConfirmAddSuggestedToken extends Component { } /** - * Returns true if any pendingTokens both: + * Returns true if any suggestedAssets both: * - Share a symbol with an existing `tokens` member. * - Does not share an address with that same `tokens` member. * This should be flagged as possibly deceptive or confusing. */ - checkNameReuse(pendingTokens, tokens) { - const duplicates = Object.keys(pendingTokens) - .map((addr) => pendingTokens[addr]) - .filter((token) => { - const dupes = tokens - .filter((old) => old.symbol === token.symbol) - .filter((old) => old.address !== token.address); - return dupes.length > 0; - }); + checkNameReuse(suggestedAssets, tokens) { + const duplicates = suggestedAssets.filter(({ asset }) => { + const dupes = tokens.filter( + (old) => + old.symbol === asset.symbol && + !isEqualCaseInsensitive(old.address, asset.address), + ); + return dupes.length > 0; + }); return duplicates.length > 0; } } diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js index ae7eb131f..5f40a6cc4 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js @@ -1,28 +1,28 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; -import { addToken, removeSuggestedTokens } from '../../store/actions'; +import { rejectWatchAsset, acceptWatchAsset } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'; const mapStateToProps = (state) => { const { - metamask: { pendingTokens, suggestedTokens, tokens }, + metamask: { suggestedAssets, tokens }, } = state; - const params = { ...pendingTokens, ...suggestedTokens }; return { mostRecentOverviewPage: getMostRecentOverviewPage(state), - pendingTokens: params, + suggestedAssets, tokens, }; }; const mapDispatchToProps = (dispatch) => { return { - addToken: ({ address, symbol, decimals, image }) => - dispatch(addToken(address, symbol, Number(decimals), image)), - removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), + rejectWatchAsset: (suggestedAssetID) => + dispatch(rejectWatchAsset(suggestedAssetID)), + acceptWatchAsset: (suggestedAssetID) => + dispatch(acceptWatchAsset(suggestedAssetID)), }; }; diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js index 303ecaa7c..64b3cc5c2 100644 --- a/ui/pages/confirm-approve/confirm-approve.js +++ b/ui/pages/confirm-approve/confirm-approve.js @@ -31,6 +31,7 @@ import { useApproveTransaction } from '../../hooks/useApproveTransaction'; import { currentNetworkTxListSelector } from '../../selectors/transactions'; import Loading from '../../components/ui/loading-screen'; import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import { getCustomTxParamsData } from './confirm-approve.util'; import ConfirmApproveContent from './confirm-approve-content'; @@ -60,7 +61,9 @@ export default function ConfirmApprove() { ); const currentToken = (tokens && - tokens.find(({ address }) => tokenAddress === address)) || { + tokens.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), + )) || { address: tokenAddress, }; diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js index aec55e531..5f7b94696 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js @@ -13,6 +13,7 @@ import { getTokenValueParam, } from '../../helpers/utils/token-util'; import { hexWEIToDecETH } from '../../helpers/utils/conversions.util'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'; const mapStateToProps = (state, ownProps) => { @@ -48,7 +49,9 @@ const mapStateToProps = (state, ownProps) => { hexMaximumTransactionFee, } = transactionFeeSelector(state, transaction); const tokens = getTokens(state); - const currentToken = tokens?.find(({ address }) => tokenAddress === address); + const currentToken = tokens?.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), + ); const { decimals, symbol: tokenSymbol } = currentToken || {}; const ethTransactionTotalMaxAmount = Number( diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index 126c86d5d..3c857189e 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -81,7 +81,6 @@ export default class ConfirmTransactionBase extends Component { useNonceField: PropTypes.bool, customNonceValue: PropTypes.string, updateCustomNonce: PropTypes.func, - assetImage: PropTypes.string, sendTransaction: PropTypes.func, showTransactionConfirmedModal: PropTypes.func, showRejectTransactionsConfirmationModal: PropTypes.func, @@ -908,7 +907,6 @@ export default class ConfirmTransactionBase extends Component { onEdit, nonce, customNonceValue, - assetImage, unapprovedTxCount, type, hideSenderToRecipient, @@ -967,7 +965,6 @@ export default class ConfirmTransactionBase extends Component { contentComponent={contentComponent} nonce={customNonceValue || nonce} unapprovedTxCount={unapprovedTxCount} - assetImage={assetImage} identiconAddress={identiconAddress} errorMessage={submitError} errorKey={errorKey} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index f407ceb57..d1f03e54a 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -76,7 +76,6 @@ const mapStateToProps = (state, ownProps) => { conversionRate, identities, addressBook, - assetImages, network, unapprovedTxs, nextNonce, @@ -97,7 +96,6 @@ const mapStateToProps = (state, ownProps) => { data, } = (transaction && transaction.txParams) || txParams; const accounts = getMetaMaskAccounts(state); - const assetImage = assetImages[txParamsToAddress]; const { balance } = accounts[fromAddress]; const { name: fromName } = identities[fromAddress]; @@ -191,7 +189,6 @@ const mapStateToProps = (state, ownProps) => { conversionRate, transactionStatus, nonce, - assetImage, unapprovedTxs, unapprovedTxCount, currentNetworkUnapprovedTxs, diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 01b83f5ab..d7d42b9c7 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -49,7 +49,7 @@ export default class Home extends PureComponent { static propTypes = { history: PropTypes.object, forgottenPassword: PropTypes.bool, - suggestedTokens: PropTypes.object, + suggestedAssets: PropTypes.array, unconfirmedTransactionsCount: PropTypes.number, shouldShowSeedPhraseReminder: PropTypes.bool.isRequired, isPopup: PropTypes.bool, @@ -87,6 +87,7 @@ export default class Home extends PureComponent { }; state = { + // eslint-disable-next-line react/no-unused-state mounted: false, canShowBlockageNotification: true, }; @@ -96,7 +97,7 @@ export default class Home extends PureComponent { firstPermissionsRequestId, history, isNotification, - suggestedTokens = {}, + suggestedAssets = [], totalUnapprovedCount, unconfirmedTransactionsCount, haveSwapsQuotes, @@ -105,6 +106,7 @@ export default class Home extends PureComponent { pendingConfirmations, } = this.props; + // eslint-disable-next-line react/no-unused-state this.setState({ mounted: true }); if (isNotification && totalUnapprovedCount === 0) { global.platform.closeCurrentWindow(); @@ -118,7 +120,7 @@ export default class Home extends PureComponent { history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`); } else if (unconfirmedTransactionsCount > 0) { history.push(CONFIRM_TRANSACTION_ROUTE); - } else if (Object.keys(suggestedTokens).length > 0) { + } else if (suggestedAssets.length > 0) { history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE); } else if (pendingConfirmations.length > 0) { history.push(CONFIRMATION_V_NEXT_ROUTE); @@ -129,7 +131,7 @@ export default class Home extends PureComponent { { firstPermissionsRequestId, isNotification, - suggestedTokens, + suggestedAssets, totalUnapprovedCount, unconfirmedTransactionsCount, haveSwapsQuotes, @@ -144,7 +146,7 @@ export default class Home extends PureComponent { } else if ( firstPermissionsRequestId || unconfirmedTransactionsCount > 0 || - Object.keys(suggestedTokens).length > 0 || + suggestedAssets.length > 0 || (!isNotification && (showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams)) ) { diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 4d179abf9..19768c60d 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -46,7 +46,7 @@ import Home from './home.component'; const mapStateToProps = (state) => { const { metamask, appState } = state; const { - suggestedTokens, + suggestedAssets, seedPhraseBackedUp, tokens, threeBoxSynced, @@ -83,7 +83,7 @@ const mapStateToProps = (state) => { return { forgottenPassword, - suggestedTokens, + suggestedAssets, swapsEnabled, unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), shouldShowSeedPhraseReminder: diff --git a/ui/pages/mobile-sync/mobile-sync.component.js b/ui/pages/mobile-sync/mobile-sync.component.js index 5f4e2ee40..bb61df1ae 100644 --- a/ui/pages/mobile-sync/mobile-sync.component.js +++ b/ui/pages/mobile-sync/mobile-sync.component.js @@ -224,6 +224,7 @@ export default class MobileSyncPage extends Component { network, preferences, transactions, + tokens, } = await this.props.fetchInfoToSync(); const { t } = this.context; @@ -232,6 +233,7 @@ export default class MobileSyncPage extends Component { network, preferences, transactions, + tokens, udata: { pwd: this.state.password, seed: this.state.seedWords, diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js index e6c1d9bb9..c71fa23cb 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -6,6 +6,7 @@ import TokenBalance from '../../../../components/ui/token-balance'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'; import { ERC20, PRIMARY } from '../../../../helpers/constants/common'; import { ASSET_TYPES } from '../../../../ducks/send'; +import { isEqualCaseInsensitive } from '../../../../helpers/utils/util'; export default class SendAssetRow extends Component { static propTypes = { @@ -14,10 +15,10 @@ export default class SendAssetRow extends Component { address: PropTypes.string, decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), symbol: PropTypes.string, + image: PropTypes.string, }), ).isRequired, accounts: PropTypes.object.isRequired, - assetImages: PropTypes.object, selectedAddress: PropTypes.string.isRequired, sendAssetAddress: PropTypes.string, updateSendAsset: PropTypes.func.isRequired, @@ -85,8 +86,8 @@ export default class SendAssetRow extends Component { renderSendToken() { const { sendAssetAddress } = this.props; - const token = this.props.tokens.find( - ({ address }) => address === sendAssetAddress, + const token = this.props.tokens.find(({ address }) => + isEqualCaseInsensitive(address, sendAssetAddress), ); return (
this.selectToken(ASSET_TYPES.TOKEN, token)} >
- +
{symbol}
diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js index 674d3bec1..b522cc33e 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js @@ -3,7 +3,6 @@ import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; import { getMetaMaskAccounts, getNativeCurrencyImage, - getAssetImages, } from '../../../../selectors'; import { updateSendAsset, getSendAssetAddress } from '../../../../ducks/send'; import SendAssetRow from './send-asset-row.component'; @@ -16,7 +15,6 @@ function mapStateToProps(state) { accounts: getMetaMaskAccounts(state), nativeCurrency: getNativeCurrency(state), nativeCurrencyImage: getNativeCurrencyImage(state), - assetImages: getAssetImages(state), }; } diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 08e5a89d7..b35125ba9 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -15,7 +15,11 @@ import { ALLOWED_SWAPS_CHAIN_IDS, } from '../../shared/constants/swaps'; -import { shortenAddress, getAccountByAddress } from '../helpers/utils/util'; +import { + shortenAddress, + getAccountByAddress, + isEqualCaseInsensitive, +} from '../helpers/utils/util'; import { getValueFromWeiHex, hexToDecimal, @@ -262,11 +266,6 @@ export function getTargetAccount(state, targetAddress) { export const getTokenExchangeRates = (state) => state.metamask.contractExchangeRates; -export function getAssetImages(state) { - const assetImages = state.metamask.assetImages || {}; - return assetImages; -} - export function getAddressBook(state) { const chainId = getCurrentChainId(state); if (!state.metamask.addressBook[chainId]) { @@ -277,8 +276,8 @@ export function getAddressBook(state) { export function getAddressBookEntry(state, address) { const addressBook = getAddressBook(state); - const entry = addressBook.find( - (contact) => contact.address === toChecksumHexAddress(address), + const entry = addressBook.find((contact) => + isEqualCaseInsensitive(contact.address, toChecksumHexAddress(address)), ); return entry; } @@ -355,7 +354,7 @@ export function getTotalUnapprovedCount(state) { unapprovedTypedMessagesCount + getUnapprovedTxCount(state) + pendingApprovalCount + - getSuggestedTokenCount(state) + getSuggestedAssetCount(state) ); } @@ -376,9 +375,9 @@ export function getUnapprovedTemplatedConfirmations(state) { ); } -function getSuggestedTokenCount(state) { - const { suggestedTokens = {} } = state.metamask; - return Object.keys(suggestedTokens).length; +function getSuggestedAssetCount(state) { + const { suggestedAssets = [] } = state.metamask; + return suggestedAssets.length; } export function getIsMainnet(state) { diff --git a/ui/store/actionConstants.js b/ui/store/actionConstants.js index 7c1ce186a..82ad8afcf 100644 --- a/ui/store/actionConstants.js +++ b/ui/store/actionConstants.js @@ -45,7 +45,6 @@ export const SET_NEXT_NONCE = 'SET_NEXT_NONCE'; // config screen export const SET_RPC_TARGET = 'SET_RPC_TARGET'; export const SET_PROVIDER_TYPE = 'SET_PROVIDER_TYPE'; -export const UPDATE_TOKENS = 'UPDATE_TOKENS'; export const SET_HARDWARE_WALLET_DEFAULT_HD_PATH = 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH'; // loading overlay diff --git a/ui/store/actions.js b/ui/store/actions.js index 16f70edf1..9c7ee52cd 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -1122,10 +1122,9 @@ export function lockMetamask() { }; } -async function _setSelectedAddress(dispatch, address) { +async function _setSelectedAddress(address) { log.debug(`background.setSelectedAddress`); - const tokens = await promisifiedBackground.setSelectedAddress(address); - dispatch(updateTokens(tokens)); + await promisifiedBackground.setSelectedAddress(address); } export function setSelectedAddress(address) { @@ -1133,7 +1132,7 @@ export function setSelectedAddress(address) { dispatch(showLoadingIndication()); log.debug(`background.setSelectedAddress`); try { - await _setSelectedAddress(dispatch, address); + await _setSelectedAddress(address); } catch (error) { dispatch(displayWarning(error.message)); return; @@ -1168,7 +1167,7 @@ export function showAccountDetail(address) { !currentTabIsConnectedToNextAddress; try { - await _setSelectedAddress(dispatch, address); + await _setSelectedAddress(address); await forceUpdateMetamaskState(dispatch); } catch (error) { dispatch(displayWarning(error.message)); @@ -1241,43 +1240,37 @@ export function addToken( image, dontShowLoadingIndicator, ) { - return (dispatch) => { + return async (dispatch) => { if (!address) { throw new Error('MetaMask - Cannot add token without address'); } if (!dontShowLoadingIndicator) { dispatch(showLoadingIndication()); } - return new Promise((resolve, reject) => { - background.addToken(address, symbol, decimals, image, (err, tokens) => { - dispatch(hideLoadingIndication()); - if (err) { - dispatch(displayWarning(err.message)); - reject(err); - return; - } - dispatch(updateTokens(tokens)); - resolve(tokens); - }); - }); + try { + await promisifiedBackground.addToken(address, symbol, decimals, image); + } catch (error) { + log.error(error); + dispatch(displayWarning(error.message)); + } finally { + await forceUpdateMetamaskState(dispatch); + dispatch(hideLoadingIndication()); + } }; } export function removeToken(address) { - return (dispatch) => { + return async (dispatch) => { dispatch(showLoadingIndication()); - return new Promise((resolve, reject) => { - background.removeToken(address, (err, tokens) => { - dispatch(hideLoadingIndication()); - if (err) { - dispatch(displayWarning(err.message)); - reject(err); - return; - } - dispatch(updateTokens(tokens)); - resolve(tokens); - }); - }); + try { + await promisifiedBackground.removeToken(address); + } catch (error) { + log.error(error); + dispatch(displayWarning(error.message)); + } finally { + await forceUpdateMetamaskState(dispatch); + dispatch(hideLoadingIndication()); + } }; } @@ -1298,40 +1291,41 @@ export function addTokens(tokens) { }; } -export function removeSuggestedTokens() { - return (dispatch) => { +export function rejectWatchAsset(suggestedAssetID) { + return async (dispatch) => { dispatch(showLoadingIndication()); - return new Promise((resolve) => { - background.removeSuggestedTokens((err, suggestedTokens) => { - dispatch(hideLoadingIndication()); - if (err) { - dispatch(displayWarning(err.message)); - } - dispatch(clearPendingTokens()); - if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { - global.platform.closeCurrentWindow(); - return; - } - resolve(suggestedTokens); - }); - }) - .then(() => updateMetamaskStateFromBackground()) - .then((suggestedTokens) => - dispatch(updateMetamaskState({ ...suggestedTokens })), - ); + try { + await promisifiedBackground.rejectWatchAsset(suggestedAssetID); + } catch (error) { + log.error(error); + dispatch(displayWarning(error.message)); + return; + } finally { + dispatch(hideLoadingIndication()); + } + dispatch(closeCurrentNotificationWindow()); }; } -export function addKnownMethodData(fourBytePrefix, methodData) { - return () => { - background.addKnownMethodData(fourBytePrefix, methodData); +export function acceptWatchAsset(suggestedAssetID) { + return async (dispatch) => { + dispatch(showLoadingIndication()); + try { + await promisifiedBackground.acceptWatchAsset(suggestedAssetID); + } catch (error) { + log.error(error); + dispatch(displayWarning(error.message)); + return; + } finally { + dispatch(hideLoadingIndication()); + } + dispatch(closeCurrentNotificationWindow()); }; } -export function updateTokens(newTokens) { - return { - type: actionConstants.UPDATE_TOKENS, - newTokens, +export function addKnownMethodData(fourBytePrefix, methodData) { + return () => { + background.addKnownMethodData(fourBytePrefix, methodData); }; } @@ -2529,8 +2523,8 @@ export function getTokenParams(tokenAddress) { return (dispatch, getState) => { const tokenList = getTokenList(getState()); const existingTokens = getState().metamask.tokens; - const existingToken = existingTokens.find( - ({ address }) => tokenAddress === address, + const existingToken = existingTokens.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), ); if (existingToken) { diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 847141fd5..7e803c284 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1069,6 +1069,7 @@ describe('Actions', () => { background.getApi.returns({ addToken: addTokenStub, + getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)), }); actions._setBackgroundConnection(background.getApi()); @@ -1098,17 +1099,18 @@ describe('Actions', () => { background.getApi.returns({ addToken: addTokenStub, + getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { - type: 'UPDATE_TOKENS', - newTokens: tokenDetails, + type: 'UPDATE_METAMASK_STATE', + value: baseMockState, }, + { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch( @@ -1121,38 +1123,6 @@ describe('Actions', () => { expect(store.getActions()).toStrictEqual(expectedActions); }); - - it('errors when addToken in background throws', async () => { - const store = mockStore(); - - const addTokenStub = sinon - .stub() - .callsFake((_, __, ___, ____, cb) => cb(new Error('error'))); - - background.getApi.returns({ - addToken: addTokenStub, - }); - - actions._setBackgroundConnection(background.getApi()); - - const expectedActions = [ - { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, - { type: 'DISPLAY_WARNING', value: 'error' }, - ]; - - await expect( - store.dispatch( - actions.addToken({ - address: '_', - symbol: '', - decimals: 0, - }), - ), - ).rejects.toThrow('error'); - - expect(store.getActions()).toStrictEqual(expectedActions); - }); }); describe('#removeToken', () => { @@ -1167,6 +1137,7 @@ describe('Actions', () => { background.getApi.returns({ removeToken: removeTokenStub, + getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)), }); actions._setBackgroundConnection(background.getApi()); @@ -1175,24 +1146,27 @@ describe('Actions', () => { expect(removeTokenStub.callCount).toStrictEqual(1); }); - it('errors when removeToken in background fails', async () => { + it('should display warning when removeToken in background fails', async () => { const store = mockStore(); background.getApi.returns({ removeToken: sinon.stub().callsFake((_, cb) => cb(new Error('error'))), + getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { + type: 'UPDATE_METAMASK_STATE', + value: baseMockState, + }, + { type: 'HIDE_LOADING_INDICATION' }, ]; - await expect(store.dispatch(actions.removeToken())).rejects.toThrow( - 'error', - ); + await store.dispatch(actions.removeToken()); expect(store.getActions()).toStrictEqual(expectedActions); });