1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Integrate TokensController (#11552)

* Integrate controllers/tokensController

* address rebase issues

* small cleanup

* addressing feedback

* more feedback
This commit is contained in:
Alex Donesky 2021-09-10 12:37:19 -05:00 committed by GitHub
parent ad7d85b04e
commit 490d3b8d40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 849 additions and 1445 deletions

View File

@ -778,7 +778,7 @@ const state = {
"0xaD6D458402F60fD3Bd25163575031ACDce07538D": "./sai.svg" "0xaD6D458402F60fD3Bd25163575031ACDce07538D": "./sai.svg"
}, },
"hiddenTokens": [], "hiddenTokens": [],
"suggestedTokens": {}, "suggestedAssets": [],
"useNonceField": false, "useNonceField": false,
"usePhishDetect": true, "usePhishDetect": true,
"lostIdentities": {}, "lostIdentities": {},

View File

@ -24,12 +24,36 @@ export default class DetectTokensController {
network, network,
keyringMemStore, keyringMemStore,
tokenList, tokenList,
tokensController,
} = {}) { } = {}) {
this.tokensController = tokensController;
this.preferences = preferences; this.preferences = preferences;
this.interval = interval; this.interval = interval;
this.network = network; this.network = network;
this.keyringMemStore = keyringMemStore; this.keyringMemStore = keyringMemStore;
this.tokenList = tokenList; 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) { async _getTokenBalances(tokens) {
@ -88,16 +112,19 @@ export default class DetectTokensController {
); );
return; return;
} }
await Promise.all(
tokensSlice.map(async (tokenAddress, index) => { const tokensWithBalance = tokensSlice.filter((_, index) => {
const balance = result[index]; const balance = result[index];
if (balance && !balance.isZero()) { return balance && !balance.isZero();
await this._preferences.addToken( });
await Promise.all(
tokensWithBalance.map((tokenAddress) => {
return this.tokensController.addToken(
tokenAddress, tokenAddress,
tokenList[tokenAddress].symbol, tokenList[tokenAddress].symbol,
tokenList[tokenAddress].decimals, tokenList[tokenAddress].decimals,
); );
}
}), }),
); );
} }
@ -130,38 +157,6 @@ export default class DetectTokensController {
}, interval); }, 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} * @type {Object}
*/ */

View File

@ -6,8 +6,10 @@ import BigNumber from 'bignumber.js';
import { import {
ControllerMessenger, ControllerMessenger,
TokenListController, TokenListController,
TokensController,
} from '@metamask/controllers'; } from '@metamask/controllers';
import { MAINNET, ROPSTEN } from '../../../shared/constants/network'; import { MAINNET, ROPSTEN } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens'; import DetectTokensController from './detect-tokens';
import NetworkController from './network'; import NetworkController from './network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
@ -15,7 +17,7 @@ import PreferencesController from './preferences';
describe('DetectTokensController', function () { describe('DetectTokensController', function () {
let tokenListController; let tokenListController;
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
let keyringMemStore, network, preferences, provider; let keyringMemStore, network, preferences, provider, tokensController;
const noop = () => undefined; const noop = () => undefined;
@ -30,6 +32,12 @@ describe('DetectTokensController', function () {
network.initializeProvider(networkControllerProviderConfig); network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider; provider = network.getProviderAndBlockTracker().provider;
preferences = new PreferencesController({ network, 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([ preferences.setAddresses([
'0x7e57e2', '0x7e57e2',
'0xbc86727e770de68b1060c91f6bb6945c73e10388', '0xbc86727e770de68b1060c91f6bb6945c73e10388',
@ -38,7 +46,10 @@ describe('DetectTokensController', function () {
.stub(network, 'getLatestBlock') .stub(network, 'getLatestBlock')
.callsFake(() => Promise.resolve({})); .callsFake(() => Promise.resolve({}));
sandbox sandbox
.stub(preferences, '_detectIsERC721') .stub(tokensController, '_instantiateNewEthersProvider')
.returns(null);
sandbox
.stub(tokensController, '_detectIsERC721')
.returns(Promise.resolve(false)); .returns(Promise.resolve(false));
nock('https://token-api.metaswap.codefi.network') nock('https://token-api.metaswap.codefi.network')
.get(`/tokens/1`) .get(`/tokens/1`)
@ -142,6 +153,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -177,6 +189,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -195,6 +208,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -204,13 +218,19 @@ describe('DetectTokensController', function () {
const existingTokenAddress = erc20ContractAddresses[0]; const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = tokenList[existingTokenAddress]; const existingToken = tokenList[existingTokenAddress];
await preferences.addToken( await tokensController.addToken(
existingTokenAddress, existingTokenAddress,
existingToken.symbol, existingToken.symbol,
existingToken.decimals, existingToken.decimals,
); );
const tokenAddressToSkip = erc20ContractAddresses[1]; const tokenAddressToSkip = erc20ContractAddresses[1];
const tokenToSkip = tokenList[tokenAddressToSkip];
await tokensController.addToken(
tokenAddressToSkip,
tokenToSkip.symbol,
tokenToSkip.decimals,
);
sandbox sandbox
.stub(controller, '_getTokenBalances') .stub(controller, '_getTokenBalances')
@ -220,15 +240,15 @@ describe('DetectTokensController', function () {
), ),
); );
await preferences.removeToken(tokenAddressToSkip); await tokensController.removeAndIgnoreToken(tokenAddressToSkip);
await controller.detectNewTokens(); await controller.detectNewTokens();
assert.deepEqual(preferences.store.getState().tokens, [ assert.deepEqual(tokensController.state.tokens, [
{ {
address: existingTokenAddress.toLowerCase(), address: toChecksumHexAddress(existingTokenAddress),
decimals: existingToken.decimals, decimals: existingToken.decimals,
symbol: existingToken.symbol, symbol: existingToken.symbol,
image: undefined,
isERC721: false, isERC721: false,
}, },
]); ]);
@ -242,6 +262,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -251,7 +272,7 @@ describe('DetectTokensController', function () {
const existingTokenAddress = erc20ContractAddresses[0]; const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = tokenList[existingTokenAddress]; const existingToken = tokenList[existingTokenAddress];
await preferences.addToken( await tokensController.addToken(
existingTokenAddress, existingTokenAddress,
existingToken.symbol, existingToken.symbol,
existingToken.decimals, existingToken.decimals,
@ -266,8 +287,8 @@ describe('DetectTokensController', function () {
const indexOfTokenToAdd = contractAddressesToDetect.indexOf( const indexOfTokenToAdd = contractAddressesToDetect.indexOf(
tokenAddressToAdd, tokenAddressToAdd,
); );
const balances = new Array(contractAddressesToDetect.length); const balances = new Array(contractAddressesToDetect.length);
balances[indexOfTokenToAdd] = new BigNumber(10); balances[indexOfTokenToAdd] = new BigNumber(10);
sandbox sandbox
@ -275,18 +296,19 @@ describe('DetectTokensController', function () {
.returns(Promise.resolve(balances)); .returns(Promise.resolve(balances));
await controller.detectNewTokens(); await controller.detectNewTokens();
assert.deepEqual(tokensController.state.tokens, [
assert.deepEqual(preferences.store.getState().tokens, [
{ {
address: existingTokenAddress.toLowerCase(), address: toChecksumHexAddress(existingTokenAddress),
decimals: existingToken.decimals, decimals: existingToken.decimals,
symbol: existingToken.symbol, symbol: existingToken.symbol,
isERC721: false, isERC721: false,
image: undefined,
}, },
{ {
address: tokenAddressToAdd.toLowerCase(), address: toChecksumHexAddress(tokenAddressToAdd),
decimals: tokenToAdd.decimals, decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol, symbol: tokenToAdd.symbol,
image: undefined,
isERC721: false, isERC721: false,
}, },
]); ]);
@ -300,6 +322,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -309,7 +332,7 @@ describe('DetectTokensController', function () {
const existingTokenAddress = erc20ContractAddresses[0]; const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = tokenList[existingTokenAddress]; const existingToken = tokenList[existingTokenAddress];
await preferences.addToken( await tokensController.addToken(
existingTokenAddress, existingTokenAddress,
existingToken.symbol, existingToken.symbol,
existingToken.decimals, existingToken.decimals,
@ -334,17 +357,19 @@ describe('DetectTokensController', function () {
await controller.detectNewTokens(); await controller.detectNewTokens();
assert.deepEqual(preferences.store.getState().tokens, [ assert.deepEqual(tokensController.state.tokens, [
{ {
address: existingTokenAddress.toLowerCase(), address: toChecksumHexAddress(existingTokenAddress),
decimals: existingToken.decimals, decimals: existingToken.decimals,
symbol: existingToken.symbol, symbol: existingToken.symbol,
image: undefined,
isERC721: false, isERC721: false,
}, },
{ {
address: tokenAddressToAdd.toLowerCase(), address: toChecksumHexAddress(tokenAddressToAdd),
decimals: tokenToAdd.decimals, decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol, symbol: tokenToAdd.symbol,
image: undefined,
isERC721: false, isERC721: false,
}, },
]); ]);
@ -357,6 +382,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -374,6 +400,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.selectedAddress = '0x0'; controller.selectedAddress = '0x0';
@ -390,6 +417,7 @@ describe('DetectTokensController', function () {
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController, tokenList: tokenListController,
tokensController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = false; controller.isUnlocked = false;
@ -405,6 +433,7 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokensController,
}); });
// trigger state update from preferences controller // trigger state update from preferences controller
await preferences.setSelectedAddress( await preferences.setSelectedAddress(

View File

@ -1,22 +1,12 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors';
import { normalize as normalizeAddress } from 'eth-sig-util'; import { normalize as normalizeAddress } from 'eth-sig-util';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import log from 'loglevel'; 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 { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import {
isValidHexAddress,
toChecksumHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { NETWORK_EVENTS } from './network'; import { NETWORK_EVENTS } from './network';
const ERC721_INTERFACE_ID = '0x80ac58cd';
export default class PreferencesController { 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 * @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 {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.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.useBlockie The users preference for blockie identicons within the UI
* @property {boolean} store.useNonceField The users preference for nonce field 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 * @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 = {}) { constructor(opts = {}) {
const initState = { const initState = {
frequentRpcListDetail: [], frequentRpcListDetail: [],
accountTokens: {},
accountHiddenTokens: {},
assetImages: {},
tokens: [],
hiddenTokens: [],
suggestedTokens: {},
useBlockie: false, useBlockie: false,
useNonceField: false, useNonceField: false,
usePhishDetect: true, usePhishDetect: true,
@ -90,12 +71,6 @@ export default class PreferencesController {
this.openPopup = opts.openPopup; this.openPopup = opts.openPopup;
this.migrateAddressBookState = opts.migrateAddressBookState; 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(); this._subscribeToInfuraAvailability();
global.setPreference = (key, value) => { global.setPreference = (key, value) => {
@ -162,14 +137,6 @@ export default class PreferencesController {
this.store.updateState({ firstTimeFlowType: type }); 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 * 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 }); 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 * Setter for the `currentLocale` property
* *
@ -226,25 +175,14 @@ export default class PreferencesController {
*/ */
setAddresses(addresses) { setAddresses(addresses) {
const oldIdentities = this.store.getState().identities; const oldIdentities = this.store.getState().identities;
const oldAccountTokens = this.store.getState().accountTokens;
const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens;
const identities = addresses.reduce((ids, address, index) => { const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}; const oldId = oldIdentities[address] || {};
ids[address] = { name: `Account ${index + 1}`, address, ...oldId }; ids[address] = { name: `Account ${index + 1}`, address, ...oldId };
return ids; return ids;
}, {}); }, {});
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}; this.store.updateState({ identities });
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 });
} }
/** /**
@ -254,19 +192,13 @@ export default class PreferencesController {
* @returns {string} the address that was removed * @returns {string} the address that was removed
*/ */
removeAddress(address) { removeAddress(address) {
const { const { identities } = this.store.getState();
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState();
if (!identities[address]) { if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`); throw new Error(`${address} can't be deleted cause it was not found`);
} }
delete identities[address]; delete identities[address];
delete accountTokens[address]; this.store.updateState({ identities });
delete accountHiddenTokens[address];
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
// If the selected account is no longer valid, // If the selected account is no longer valid,
// select an arbitrary other account: // select an arbitrary other account:
@ -284,11 +216,7 @@ export default class PreferencesController {
* *
*/ */
addAddresses(addresses) { addAddresses(addresses) {
const { const { identities } = this.store.getState();
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState();
addresses.forEach((address) => { addresses.forEach((address) => {
// skip if already exists // skip if already exists
if (identities[address]) { if (identities[address]) {
@ -297,11 +225,9 @@ export default class PreferencesController {
// add missing identity // add missing identity
const identityCount = Object.keys(identities).length; const identityCount = Object.keys(identities).length;
accountTokens[address] = {};
accountHiddenTokens[address] = {};
identities[address] = { name: `Account ${identityCount + 1}`, 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; return selected;
} }
removeSuggestedTokens() {
return new Promise((resolve) => {
this.store.updateState({ suggestedTokens: {} });
resolve({});
});
}
/** /**
* Setter for the `selectedAddress` property * Setter for the `selectedAddress` property
* *
* @param {string} _address - A new hex address for an account * @param {string} _address - A new hex address for an account
* @returns {Promise<void>} Promise resolves with tokens
* *
*/ */
setSelectedAddress(_address) { setSelectedAddress(_address) {
const address = normalizeAddress(_address); const address = normalizeAddress(_address);
this._updateTokens(address);
const { identities, tokens } = this.store.getState(); const { identities } = this.store.getState();
const selectedIdentity = identities[address]; const selectedIdentity = identities[address];
if (!selectedIdentity) { if (!selectedIdentity) {
throw new Error(`Identity for '${address} not found`); throw new Error(`Identity for '${address} not found`);
@ -374,7 +291,6 @@ export default class PreferencesController {
selectedIdentity.lastSelected = Date.now(); selectedIdentity.lastSelected = Date.now();
this.store.updateState({ identities, selectedAddress: address }); this.store.updateState({ identities, selectedAddress: address });
return Promise.resolve(tokens);
} }
/** /**
@ -387,99 +303,6 @@ export default class PreferencesController {
return this.store.getState().selectedAddress; 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<array>} 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<object>} 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<array>} 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 * Sets a custom label for an account
* @param {string} account - the account to set a label for * @param {string} account - the account to set a label for
@ -770,188 +593,4 @@ export default class PreferencesController {
this.store.updateState({ infuraBlocked: isBlocked }); 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.<array, object, string, string>} 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 });
}
} }

View File

@ -1,11 +1,6 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import contractMaps from '@metamask/contract-metadata'; import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import abiERC721 from 'human-standard-collectible-abi';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
import NetworkController from './network'; import NetworkController from './network';
@ -13,9 +8,6 @@ describe('preferences controller', function () {
let preferencesController; let preferencesController;
let network; let network;
let currentChainId; let currentChainId;
let triggerNetworkChange;
let switchToMainnet;
let switchToRinkeby;
let provider; let provider;
const migrateAddressBookState = sinon.stub(); const migrateAddressBookState = sinon.stub();
@ -37,22 +29,12 @@ describe('preferences controller', function () {
sandbox sandbox
.stub(network, 'getProviderConfig') .stub(network, 'getProviderConfig')
.callsFake(() => ({ type: 'mainnet' })); .callsFake(() => ({ type: 'mainnet' }));
const spy = sandbox.spy(network, 'on');
preferencesController = new PreferencesController({ preferencesController = new PreferencesController({
migrateAddressBookState, migrateAddressBookState,
network, network,
provider, provider,
}); });
triggerNetworkChange = spy.firstCall.args[1];
switchToMainnet = () => {
currentChainId = MAINNET_CHAIN_ID;
triggerNetworkChange();
};
switchToRinkeby = () => {
currentChainId = RINKEBY_CHAIN_ID;
triggerNetworkChange();
};
}); });
afterEach(function () { 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 () { it('should replace its list of addresses', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
preferencesController.setAddresses(['0xda22le77', '0x7e57e277']); 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 () { describe('removeAddress', function () {
it('should remove an address from state', function () { it('should remove an address from state', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); 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 () { it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); 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 () { describe('setPasswordForgotten', function () {
it('should default to false', function () { it('should default to false', function () {
const state = preferencesController.store.getState(); const state = preferencesController.store.getState();

View File

@ -1,31 +1,62 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import { ObservableStore } from '@metamask/obs-store'; import { TokensController } from '@metamask/controllers';
import TokenRatesController from './token-rates'; import TokenRatesController from './token-rates';
import NetworkController from './network';
import PreferencesController from './preferences';
const networkControllerProviderConfig = {
getAccounts: () => undefined,
};
describe('TokenRatesController', function () { describe('TokenRatesController', function () {
let nativeCurrency; let nativeCurrency,
let getNativeCurrency; getNativeCurrency,
network,
provider,
preferences,
tokensController;
beforeEach(function () { beforeEach(function () {
nativeCurrency = 'ETH'; nativeCurrency = 'ETH';
getNativeCurrency = () => nativeCurrency; 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),
}); });
it('should listen for preferences store updates', function () { sinon.stub(network, 'getLatestBlock').callsFake(() => Promise.resolve({}));
const preferences = new ObservableStore({ tokens: [] }); sinon.stub(tokensController, '_instantiateNewEthersProvider').returns(null);
preferences.putState({ tokens: ['foo'] }); sinon
.stub(tokensController, '_detectIsERC721')
.returns(Promise.resolve(false));
});
it('should listen for tokenControllers state updates', async function () {
const controller = new TokenRatesController({ const controller = new TokenRatesController({
preferences, tokensController,
getNativeCurrency, 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 () { it('should poll on correct interval', async function () {
const stub = sinon.stub(global, 'setInterval'); const stub = sinon.stub(global, 'setInterval');
const preferences = new ObservableStore({ tokens: [] });
preferences.putState({ tokens: ['foo'] });
const controller = new TokenRatesController({ const controller = new TokenRatesController({
preferences, tokensController,
getNativeCurrency, getNativeCurrency,
}); });
controller.start(1337); controller.start(1337);

View File

@ -20,11 +20,11 @@ export default class TokenRatesController {
* *
* @param {Object} [config] - Options to configure controller * @param {Object} [config] - Options to configure controller
*/ */
constructor({ preferences, getNativeCurrency } = {}) { constructor({ tokensController, getNativeCurrency } = {}) {
this.store = new ObservableStore(); this.store = new ObservableStore();
this.getNativeCurrency = getNativeCurrency; this.getNativeCurrency = getNativeCurrency;
this.tokens = preferences.getState().tokens; this.tokens = tokensController.state.tokens;
preferences.subscribe(({ tokens = [] }) => { tokensController.subscribe(({ tokens = [] }) => {
this.tokens = tokens; this.tokens = tokens;
}); });
} }

View File

@ -32,7 +32,8 @@ async function watchAssetHandler(
{ handleWatchAssetRequest }, { handleWatchAssetRequest },
) { ) {
try { try {
res.result = await handleWatchAssetRequest(req); const { options: asset, type } = req.params;
res.result = await handleWatchAssetRequest(asset, type);
return end(); return end();
} catch (error) { } catch (error) {
return end(error); return end(error);

View File

@ -25,6 +25,7 @@ import {
NotificationController, NotificationController,
GasFeeController, GasFeeController,
TokenListController, TokenListController,
TokensController,
} from '@metamask/controllers'; } from '@metamask/controllers';
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
import { import {
@ -62,10 +63,10 @@ import EncryptionPublicKeyManager from './lib/encryption-public-key-manager';
import PersonalMessageManager from './lib/personal-message-manager'; import PersonalMessageManager from './lib/personal-message-manager';
import TypedMessageManager from './lib/typed-message-manager'; import TypedMessageManager from './lib/typed-message-manager';
import TransactionController from './controllers/transactions'; import TransactionController from './controllers/transactions';
import TokenRatesController from './controllers/token-rates';
import DetectTokensController from './controllers/detect-tokens'; import DetectTokensController from './controllers/detect-tokens';
import SwapsController from './controllers/swaps'; import SwapsController from './controllers/swaps';
import { PermissionsController } from './controllers/permissions'; import { PermissionsController } from './controllers/permissions';
import TokenRatesController from './controllers/token-rates';
import { NOTIFICATION_NAMES } from './controllers/permissions/enums'; import { NOTIFICATION_NAMES } from './controllers/permissions/enums';
import getRestrictedMethods from './controllers/permissions/restrictedMethods'; import getRestrictedMethods from './controllers/permissions/restrictedMethods';
import nodeify from './lib/nodeify'; import nodeify from './lib/nodeify';
@ -160,6 +161,17 @@ export default class MetamaskController extends EventEmitter {
migrateAddressBookState: this.migrateAddressBookState.bind(this), 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({ this.metaMetricsController = new MetaMetricsController({
segment, segment,
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
@ -270,9 +282,8 @@ export default class MetamaskController extends EventEmitter {
initState.NotificationController, initState.NotificationController,
); );
// token exchange rate tracker
this.tokenRatesController = new TokenRatesController({ this.tokenRatesController = new TokenRatesController({
preferences: this.preferencesController.store, tokensController: this.tokensController,
getNativeCurrency: () => { getNativeCurrency: () => {
const { ticker } = this.networkController.getProviderConfig(); const { ticker } = this.networkController.getProviderConfig();
return ticker ?? 'ETH'; return ticker ?? 'ETH';
@ -342,6 +353,10 @@ export default class MetamaskController extends EventEmitter {
preferencesController: this.preferencesController, preferencesController: this.preferencesController,
}); });
this.tokensController.hub.on('pendingSuggestedAsset', async () => {
await opts.openPopup();
});
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]; const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring];
this.keyringController = new KeyringController({ this.keyringController = new KeyringController({
keyringTypes: additionalKeyrings, keyringTypes: additionalKeyrings,
@ -375,6 +390,7 @@ export default class MetamaskController extends EventEmitter {
this.detectTokensController = new DetectTokensController({ this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController, preferences: this.preferencesController,
tokensController: this.tokensController,
network: this.networkController, network: this.networkController,
keyringMemStore: this.keyringController.memStore, keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController, tokenList: this.tokenListController,
@ -555,6 +571,7 @@ export default class MetamaskController extends EventEmitter {
NotificationController: this.notificationController, NotificationController: this.notificationController,
GasFeeController: this.gasFeeController, GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController, TokenListController: this.tokenListController,
TokensController: this.tokensController,
}); });
this.memStore = new ComposableObservableStore({ this.memStore = new ComposableObservableStore({
@ -588,6 +605,7 @@ export default class MetamaskController extends EventEmitter {
NotificationController: this.notificationController, NotificationController: this.notificationController,
GasFeeController: this.gasFeeController, GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController, TokenListController: this.tokenListController,
TokensController: this.tokensController,
}, },
controllerMessenger: this.controllerMessenger, controllerMessenger: this.controllerMessenger,
}); });
@ -768,6 +786,7 @@ export default class MetamaskController extends EventEmitter {
swapsController, swapsController,
threeBoxController, threeBoxController,
txController, txController,
tokensController,
} = this; } = this;
return { return {
@ -837,18 +856,22 @@ export default class MetamaskController extends EventEmitter {
preferencesController.setSelectedAddress, preferencesController.setSelectedAddress,
preferencesController, preferencesController,
), ),
addToken: nodeify(preferencesController.addToken, preferencesController), addToken: nodeify(tokensController.addToken, tokensController),
rejectWatchAsset: nodeify(
tokensController.rejectWatchAsset,
tokensController,
),
acceptWatchAsset: nodeify(
tokensController.acceptWatchAsset,
tokensController,
),
updateTokenType: nodeify( updateTokenType: nodeify(
preferencesController.updateTokenType, tokensController.updateTokenType,
preferencesController, tokensController,
), ),
removeToken: nodeify( removeToken: nodeify(
preferencesController.removeToken, tokensController.removeAndIgnoreToken,
preferencesController, tokensController,
),
removeSuggestedTokens: nodeify(
preferencesController.removeSuggestedTokens,
preferencesController,
), ),
setAccountLabel: nodeify( setAccountLabel: nodeify(
preferencesController.setAccountLabel, preferencesController.setAccountLabel,
@ -1295,23 +1318,50 @@ export default class MetamaskController extends EventEmitter {
async fetchInfoToSync() { async fetchInfoToSync() {
// Preferences // Preferences
const { const {
accountTokens,
currentLocale, currentLocale,
frequentRpcList, frequentRpcList,
identities, identities,
selectedAddress, selectedAddress,
tokens, useTokenDetection,
} = this.preferencesController.store.getState(); } = this.preferencesController.store.getState();
const { tokenList } = this.tokenListController.state;
const preferences = { const preferences = {
accountTokens,
currentLocale, currentLocale,
frequentRpcList, frequentRpcList,
identities, identities,
selectedAddress, 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 // Accounts
const hdKeyring = this.keyringController.getKeyringsByType( const hdKeyring = this.keyringController.getKeyringsByType(
'HD Key Tree', 'HD Key Tree',
@ -1351,6 +1401,7 @@ export default class MetamaskController extends EventEmitter {
accounts, accounts,
preferences, preferences,
transactions, transactions,
tokens: { allTokens: allERC20Tokens, allIgnoredTokens },
network: this.networkController.store.getState(), network: this.networkController.store.getState(),
}; };
} }
@ -2366,8 +2417,8 @@ export default class MetamaskController extends EventEmitter {
sendMetrics: this.metaMetricsController.trackEvent.bind( sendMetrics: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController, this.metaMetricsController,
), ),
handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( handleWatchAssetRequest: this.tokensController.watchAsset.bind(
this.preferencesController, this.tokensController,
), ),
getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind(
this.alertController, this.alertController,

View File

@ -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;
}

View File

@ -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: {},
},
});
});
});

View File

@ -66,6 +66,7 @@ import m059 from './059';
import m060 from './060'; import m060 from './060';
import m061 from './061'; import m061 from './061';
import m062 from './062'; import m062 from './062';
import m063 from './063';
const migrations = [ const migrations = [
m002, m002,
@ -129,6 +130,7 @@ const migrations = [
m060, m060,
m061, m061,
m062, m062,
m063,
]; ];
export default migrations; export default migrations;

View File

@ -156,7 +156,6 @@
"fast-safe-stringify": "^2.0.7", "fast-safe-stringify": "^2.0.7",
"fuse.js": "^3.2.0", "fuse.js": "^3.2.0",
"globalthis": "^1.0.1", "globalthis": "^1.0.1",
"human-standard-collectible-abi": "^1.0.2",
"human-standard-token-abi": "^2.0.0", "human-standard-token-abi": "^2.0.0",
"immer": "^8.0.1", "immer": "^8.0.1",
"json-rpc-engine": "^6.1.0", "json-rpc-engine": "^6.1.0",

View File

@ -1,150 +1,165 @@
{ {
"data": { "data": {
"AlertController": {
"alertEnabledness": {
"unconnectedAccount": true,
"web3ShimUsage": true
},
"unconnectedAccountAlertShownOrigins": {},
"web3ShimUsageOrigins": {}
},
"AppStateController": { "AppStateController": {
"mkrMigrationReminderTimestamp": null "connectedStatusPopoverHasBeenShown": true,
"defaultHomeActiveTabName": null,
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": 1627317428214
}, },
"CachedBalancesController": { "CachedBalancesController": {
"cachedBalances": { "cachedBalances": {
"4": {} "0x4": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": "0x0"
}
} }
}, },
"CurrencyController": { "CurrencyController": {
"conversionDate": 1575697244.188, "conversionDate": 1626907353.891,
"conversionRate": 149.61, "conversionRate": 1968.5,
"currentCurrency": "usd", "currentCurrency": "usd",
"nativeCurrency": "ETH" "nativeCurrency": "ETH",
"pendingCurrentCurrency": null,
"pendingNativeCurrency": null,
"usdConversionRate": 1968.5
}, },
"IncomingTransactionsController": { "IncomingTransactionsController": {
"incomingTransactions": {}, "incomingTransactions": {},
"incomingTxLastFetchedBlocksByNetwork": { "incomingTxLastFetchedBlockByChainId": {
"goerli": null, "0x1": null,
"kovan": null, "0x2a": null,
"mainnet": null, "0x3": null,
"rinkeby": 5570536 "0x4": 8977934,
"0x5": null
} }
}, },
"KeyringController": { "KeyringController": {
"vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" "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": { "NetworkController": {
"network": "1337", "network": "1337",
"networkDetails": {
"EIPS": {}
},
"previousProviderStore": {
"chainId": "0x4",
"ticker": "ETH",
"type": "rinkeby"
},
"provider": { "provider": {
"nickname": "Localhost 8545",
"rpcUrl": "http://localhost:8545",
"chainId": "0x539", "chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": {},
"rpcUrl": "http://localhost:8545",
"ticker": "ETH", "ticker": "ETH",
"type": "rpc" "type": "rpc"
} }
}, },
"NotificationController": { "NotificationController": {
"notifications": { "notifications": {}
"1": {
"isShown": true
},
"3": {
"isShown": true
},
"5": {
"isShown": true
},
"6": {
"isShown": true
}
}
}, },
"OnboardingController": { "OnboardingController": {
"onboardingTabs": {}, "onboardingTabs": {},
"seedPhraseBackedUp": false "seedPhraseBackedUp": true
}, },
"PermissionsMetadata": { "PermissionsController": {
"domainMetadata": { "domains": {},
"metamask.github.io": { "permissionsDescriptions": {},
"icon": null, "permissionsRequests": []
"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
}
]
}, },
"PreferencesController": { "PreferencesController": {
"accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
"0x539": [
{
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"symbol": "TST",
"decimals": 4
}
],
"rinkeby": [],
"ropsten": []
}
},
"assetImages": {},
"completedOnboarding": true, "completedOnboarding": true,
"currentLocale": "en", "currentLocale": "en",
"dismissSeedBackUpReminder": true,
"featureFlags": { "featureFlags": {
"showIncomingTransactions": true, "showIncomingTransactions": true
"transactionTime": false
}, },
"firstTimeFlowType": "create", "firstTimeFlowType": "import",
"forgottenPassword": false, "forgottenPassword": false,
"frequentRpcListDetail": [], "frequentRpcListDetail": [
{
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": {},
"rpcUrl": "http://localhost:8545",
"ticker": "ETH"
}
],
"identities": { "identities": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
"address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1",
"lastSelected": 1626907346643,
"name": "Account 1" "name": "Account 1"
} }
}, },
"infuraBlocked": false,
"ipfsGateway": "dweb.link",
"knownMethodData": {}, "knownMethodData": {},
"lostIdentities": {}, "lostIdentities": {},
"metaMetricsId": null,
"participateInMetaMetrics": false,
"preferences": { "preferences": {
"hideZeroBalanceTokens": false,
"showFiatInTestnets": false,
"useNativeCurrencyAsPrimaryCurrency": true "useNativeCurrencyAsPrimaryCurrency": true
}, },
"selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", "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": [ "tokens": [
{ {
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"symbol": "TST", "decimals": 4,
"decimals": 4 "image": null,
"isERC721": false,
"symbol": "TST"
} }
], ]
"useBlockie": false, },
"useNonceField": false, "TransactionController": {
"usePhishDetect": true "transactions": {}
}, },
"config": {}, "config": {},
"firstTimeInfo": { "firstTimeInfo": {
"date": 1575697234195, "date": 1626907328205,
"version": "7.7.0" "version": "9.8.1"
} }
}, },
"meta": { "meta": {
"version": 40 "version": 63
} }
} }

View File

@ -452,6 +452,7 @@
}, },
"assetImages": {}, "assetImages": {},
"completedOnboarding": true, "completedOnboarding": true,
"dismissSeedBackUpReminder": true,
"currentLocale": "en", "currentLocale": "en",
"featureFlags": { "featureFlags": {
"showIncomingTransactions": true, "showIncomingTransactions": true,

View File

@ -27,6 +27,7 @@ describe('Hide token', function () {
css: '.asset-list-item__token-button', css: '.asset-list-item__token-button',
text: '0 TST', text: '0 TST',
}); });
await driver.clickElement('.popover-header__button');
let assets = await driver.findElements('.asset-list-item'); let assets = await driver.findElements('.asset-list-item');
assert.equal(assets.length, 2); assert.equal(assets.length, 2);

View File

@ -80,6 +80,7 @@ describe('Deploy contract and call contract methods', function () {
await driver.switchToWindow(dapp); await driver.switchToWindow(dapp);
await driver.clickElement('#depositButton'); await driver.clickElement('#depositButton');
await driver.waitUntilXWindowHandles(3); await driver.waitUntilXWindowHandles(3);
windowHandles = await driver.getAllWindowHandles(); windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle( await driver.switchToWindowWithTitle(
'MetaMask Notification', 'MetaMask Notification',

View File

@ -20,7 +20,6 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle: PropTypes.bool, hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string, identiconAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
subtitleComponent: PropTypes.node, subtitleComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
titleComponent: PropTypes.node, titleComponent: PropTypes.node,
@ -77,7 +76,6 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle, hideSubtitle,
identiconAddress, identiconAddress,
nonce, nonce,
assetImage,
detailsComponent, detailsComponent,
dataComponent, dataComponent,
warning, warning,
@ -111,7 +109,6 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle={hideSubtitle} hideSubtitle={hideSubtitle}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
nonce={nonce} nonce={nonce}
assetImage={assetImage}
origin={origin} origin={origin}
/> />
{this.renderContent()} {this.renderContent()}

View File

@ -13,7 +13,6 @@ const ConfirmPageContainerSummary = (props) => {
className, className,
identiconAddress, identiconAddress,
nonce, nonce,
assetImage,
origin, origin,
} = props; } = props;
@ -36,7 +35,6 @@ const ConfirmPageContainerSummary = (props) => {
className="confirm-page-container-summary__identicon" className="confirm-page-container-summary__identicon"
diameter={36} diameter={36}
address={identiconAddress} address={identiconAddress}
image={assetImage}
/> />
)} )}
<div className="confirm-page-container-summary__title-text"> <div className="confirm-page-container-summary__title-text">
@ -61,7 +59,6 @@ ConfirmPageContainerSummary.propTypes = {
className: PropTypes.string, className: PropTypes.string,
identiconAddress: PropTypes.string, identiconAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
origin: PropTypes.string.isRequired, origin: PropTypes.string.isRequired,
}; };

View File

@ -42,7 +42,6 @@ export default class ConfirmPageContainer extends Component {
detailsComponent: PropTypes.node, detailsComponent: PropTypes.node,
identiconAddress: PropTypes.string, identiconAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
assetImage: PropTypes.string,
warning: PropTypes.string, warning: PropTypes.string,
unapprovedTxCount: PropTypes.number, unapprovedTxCount: PropTypes.number,
origin: PropTypes.string.isRequired, origin: PropTypes.string.isRequired,
@ -98,7 +97,6 @@ export default class ConfirmPageContainer extends Component {
identiconAddress, identiconAddress,
nonce, nonce,
unapprovedTxCount, unapprovedTxCount,
assetImage,
warning, warning,
totalTx, totalTx,
positionOfCurrentTx, positionOfCurrentTx,
@ -120,7 +118,6 @@ export default class ConfirmPageContainer extends Component {
showAddToAddressBookModal, showAddToAddressBookModal,
contact = {}, contact = {},
} = this.props; } = this.props;
const renderAssetImage = contentComponent || !identiconAddress;
const showAddToAddressDialog = const showAddToAddressDialog =
contact.name === undefined && toAddress !== undefined; contact.name === undefined && toAddress !== undefined;
@ -153,7 +150,6 @@ export default class ConfirmPageContainer extends Component {
recipientAddress={toAddress} recipientAddress={toAddress}
recipientEns={toEns} recipientEns={toEns}
recipientNickname={toNickname} recipientNickname={toNickname}
assetImage={renderAssetImage ? assetImage : undefined}
/> />
)} )}
</ConfirmPageContainerHeader> </ConfirmPageContainerHeader>
@ -181,7 +177,6 @@ export default class ConfirmPageContainer extends Component {
errorKey={errorKey} errorKey={errorKey}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
nonce={nonce} nonce={nonce}
assetImage={assetImage}
warning={warning} warning={warning}
onCancelAll={onCancelAll} onCancelAll={onCancelAll}
onCancel={onCancel} onCancel={onCancel}

View File

@ -8,7 +8,6 @@ import Button from '../../../ui/button';
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
token: state.appState.modal.modalState.props.token, token: state.appState.modal.modalState.props.token,
assetImages: state.metamask.assetImages,
}; };
} }
@ -31,19 +30,18 @@ class HideTokenConfirmationModal extends Component {
static propTypes = { static propTypes = {
hideToken: PropTypes.func.isRequired, hideToken: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired, hideModal: PropTypes.func.isRequired,
assetImages: PropTypes.object.isRequired,
token: PropTypes.shape({ token: PropTypes.shape({
symbol: PropTypes.string, symbol: PropTypes.string,
address: PropTypes.string, address: PropTypes.string,
image: PropTypes.string,
}), }),
}; };
state = {}; state = {};
render() { render() {
const { token, hideToken, hideModal, assetImages } = this.props; const { token, hideToken, hideModal } = this.props;
const { symbol, address } = token; const { symbol, address, image } = token;
const image = assetImages[address];
return ( return (
<div className="hide-token-confirmation"> <div className="hide-token-confirmation">

View File

@ -21,7 +21,6 @@ export default function TokenCell({
const t = useI18nContext(); const t = useI18nContext();
const formattedFiat = useTokenFiatAmount(address, string, symbol); const formattedFiat = useTokenFiatAmount(address, string, symbol);
const warning = balanceError ? ( const warning = balanceError ? (
<span> <span>
{t('troubleTokenBalances')} {t('troubleTokenBalances')}

View File

@ -6,15 +6,11 @@ import { useSelector } from 'react-redux';
import TokenCell from '../token-cell'; import TokenCell from '../token-cell';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { import { getShouldHideZeroBalanceTokens } from '../../../selectors';
getAssetImages,
getShouldHideZeroBalanceTokens,
} from '../../../selectors';
import { getTokens } from '../../../ducks/metamask/metamask'; import { getTokens } from '../../../ducks/metamask/metamask';
export default function TokenList({ onTokenClick }) { export default function TokenList({ onTokenClick }) {
const t = useI18nContext(); const t = useI18nContext();
const assetImages = useSelector(getAssetImages);
const shouldHideZeroBalanceTokens = useSelector( const shouldHideZeroBalanceTokens = useSelector(
getShouldHideZeroBalanceTokens, getShouldHideZeroBalanceTokens,
); );
@ -46,7 +42,6 @@ export default function TokenList({ onTokenClick }) {
return ( return (
<div> <div>
{tokensWithBalances.map((tokenData, index) => { {tokensWithBalances.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address];
return <TokenCell key={index} {...tokenData} onClick={onTokenClick} />; return <TokenCell key={index} {...tokenData} onClick={onTokenClick} />;
})} })}
</div> </div>

View File

@ -12,6 +12,7 @@ import Button from '../../ui/button';
import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions';
import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../shared/constants/swaps'; import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../shared/constants/swaps';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
import { isEqualCaseInsensitive } from '../../../helpers/utils/util';
const PAGE_INCREMENT = 10; const PAGE_INCREMENT = 10;
@ -28,7 +29,7 @@ const getTransactionGroupRecipientAddressFilter = (
) => { ) => {
return ({ initialTransaction: { txParams } }) => { return ({ initialTransaction: { txParams } }) => {
return ( return (
txParams?.to === recipientAddress || isEqualCaseInsensitive(txParams?.to, recipientAddress) ||
(txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] && (txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] &&
txParams.data.match(recipientAddress.slice(2))) txParams.data.match(recipientAddress.slice(2)))
); );

View File

@ -20,7 +20,6 @@ import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send'; import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
import { import {
getAssetImages,
getCurrentKeyring, getCurrentKeyring,
getIsSwapsChain, getIsSwapsChain,
} from '../../../selectors/selectors'; } from '../../../selectors/selectors';
@ -42,8 +41,6 @@ const TokenOverview = ({ className, token }) => {
}, },
}); });
const history = useHistory(); const history = useHistory();
const assetImages = useSelector(getAssetImages);
const keyring = useSelector(getCurrentKeyring); const keyring = useSelector(getCurrentKeyring);
const usingHardwareWallet = keyring.type.search('Hardware') !== -1; const usingHardwareWallet = keyring.type.search('Hardware') !== -1;
const { tokensWithBalances } = useTokenTracker([token]); const { tokensWithBalances } = useTokenTracker([token]);
@ -109,7 +106,7 @@ const TokenOverview = ({ className, token }) => {
dispatch( dispatch(
setSwapsFromToken({ setSwapsFromToken({
...token, ...token,
iconUrl: assetImages[token.address], iconUrl: token.image,
balance, balance,
string: balanceToRender, string: balanceToRender,
}), }),
@ -136,11 +133,7 @@ const TokenOverview = ({ className, token }) => {
} }
className={className} className={className}
icon={ icon={
<Identicon <Identicon diameter={32} address={token.address} image={token.image} />
diameter={32}
address={token.address}
image={assetImages[token.address]}
/>
} }
/> />
); );
@ -152,6 +145,7 @@ TokenOverview.propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
decimals: PropTypes.number, decimals: PropTypes.number,
symbol: PropTypes.string, symbol: PropTypes.string,
image: PropTypes.string,
isERC721: PropTypes.bool, isERC721: PropTypes.bool,
}).isRequired, }).isRequired,
}; };

View File

@ -98,7 +98,6 @@ SenderAddress.propTypes = {
function RecipientWithAddress({ function RecipientWithAddress({
checksummedRecipientAddress, checksummedRecipientAddress,
assetImage,
onRecipientClick, onRecipientClick,
addressOnly, addressOnly,
recipientNickname, recipientNickname,
@ -135,11 +134,7 @@ function RecipientWithAddress({
> >
{!addressOnly && ( {!addressOnly && (
<div className="sender-to-recipient__sender-icon"> <div className="sender-to-recipient__sender-icon">
<Identicon <Identicon address={checksummedRecipientAddress} diameter={24} />
address={checksummedRecipientAddress}
diameter={24}
image={assetImage}
/>
</div> </div>
)} )}
<Tooltip <Tooltip
@ -170,7 +165,6 @@ RecipientWithAddress.propTypes = {
recipientEns: PropTypes.string, recipientEns: PropTypes.string,
recipientNickname: PropTypes.string, recipientNickname: PropTypes.string,
addressOnly: PropTypes.bool, addressOnly: PropTypes.bool,
assetImage: PropTypes.string,
onRecipientClick: PropTypes.func, onRecipientClick: PropTypes.func,
}; };
@ -195,7 +189,6 @@ Arrow.propTypes = {
export default function SenderToRecipient({ export default function SenderToRecipient({
senderAddress, senderAddress,
addressOnly, addressOnly,
assetImage,
senderName, senderName,
recipientNickname, recipientNickname,
recipientName, recipientName,
@ -223,7 +216,6 @@ export default function SenderToRecipient({
<Arrow variant={variant} /> <Arrow variant={variant} />
{recipientAddress ? ( {recipientAddress ? (
<RecipientWithAddress <RecipientWithAddress
assetImage={assetImage}
checksummedRecipientAddress={checksummedRecipientAddress} checksummedRecipientAddress={checksummedRecipientAddress}
onRecipientClick={onRecipientClick} onRecipientClick={onRecipientClick}
addressOnly={addressOnly} addressOnly={addressOnly}
@ -255,7 +247,6 @@ SenderToRecipient.propTypes = {
recipientNickname: PropTypes.string, recipientNickname: PropTypes.string,
variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]),
addressOnly: PropTypes.bool, addressOnly: PropTypes.bool,
assetImage: PropTypes.string,
onRecipientClick: PropTypes.func, onRecipientClick: PropTypes.func,
onSenderClick: PropTypes.func, onSenderClick: PropTypes.func,
warnUserOnAccountMismatch: PropTypes.bool, warnUserOnAccountMismatch: PropTypes.bool,

View File

@ -17,6 +17,7 @@ import { getTokenData, sumHexes } from '../../helpers/utils/transactions.util';
import { conversionUtil } from '../../../shared/modules/conversion.utils'; import { conversionUtil } from '../../../shared/modules/conversion.utils';
import { getAveragePriceEstimateInHexWEI } from '../../selectors/custom-gas'; import { getAveragePriceEstimateInHexWEI } from '../../selectors/custom-gas';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
// Actions // Actions
const createActionType = (action) => `metamask/confirm-transaction/${action}`; const createActionType = (action) => `metamask/confirm-transaction/${action}`;
@ -283,8 +284,8 @@ export function setTransactionToConfirm(transactionId) {
const tokenData = getTokenData(data); const tokenData = getTokenData(data);
const tokens = getTokens(state); const tokens = getTokens(state);
const currentToken = tokens?.find( const currentToken = tokens?.find(({ address }) =>
({ address }) => tokenAddress === address, isEqualCaseInsensitive(tokenAddress, address),
); );
dispatch( dispatch(

View File

@ -22,7 +22,6 @@ export default function reduceMetamask(state = {}, action) {
frequentRpcList: [], frequentRpcList: [],
addressBook: [], addressBook: [],
contractExchangeRates: {}, contractExchangeRates: {},
tokens: [],
pendingTokens: {}, pendingTokens: {},
customNonceValue: '', customNonceValue: '',
useBlockie: false, useBlockie: false,
@ -89,12 +88,6 @@ export default function reduceMetamask(state = {}, action) {
return Object.assign(metamaskState, { identities }); return Object.assign(metamaskState, { identities });
} }
case actionConstants.UPDATE_TOKENS:
return {
...metamaskState,
tokens: action.newTokens,
};
case actionConstants.UPDATE_CUSTOM_NONCE: case actionConstants.UPDATE_CUSTOM_NONCE:
return { return {
...metamaskState, ...metamaskState,

View File

@ -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', () => { it('toggles account menu', () => {
const state = reduceMetamask( const state = reduceMetamask(
{}, {},

View File

@ -3,6 +3,7 @@ import { useRouteMatch } from 'react-router-dom';
import { getTokens } from '../ducks/metamask/metamask'; import { getTokens } from '../ducks/metamask/metamask';
import { getCurrentChainId } from '../selectors'; import { getCurrentChainId } from '../selectors';
import { ASSET_ROUTE } from '../helpers/constants/routes'; import { ASSET_ROUTE } from '../helpers/constants/routes';
import { isEqualCaseInsensitive } from '../helpers/utils/util';
import { import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP, SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
ETH_SWAPS_TOKEN_OBJECT, ETH_SWAPS_TOKEN_OBJECT,
@ -26,7 +27,10 @@ export function useCurrentAsset() {
const tokenAddress = match?.params?.asset; const tokenAddress = match?.params?.asset;
const knownTokens = useSelector(getTokens); const knownTokens = useSelector(getTokens);
const token = const token =
tokenAddress && knownTokens.find(({ address }) => address === tokenAddress); tokenAddress &&
knownTokens.find(({ address }) =>
isEqualCaseInsensitive(address, tokenAddress),
);
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
return ( return (

View File

@ -3,6 +3,7 @@ import TokenTracker from '@metamask/eth-token-tracker';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getCurrentChainId, getSelectedAddress } from '../selectors'; import { getCurrentChainId, getSelectedAddress } from '../selectors';
import { SECOND } from '../../shared/constants/time'; import { SECOND } from '../../shared/constants/time';
import { isEqualCaseInsensitive } from '../helpers/utils/util';
import { useEqualityCheck } from './useEqualityCheck'; import { useEqualityCheck } from './useEqualityCheck';
export function useTokenTracker( export function useTokenTracker(
@ -26,10 +27,14 @@ export function useTokenTracker(
// TODO: improve this pattern for adding this field when we improve support for // TODO: improve this pattern for adding this field when we improve support for
// EIP721 tokens. // EIP721 tokens.
const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => { const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => {
const additionalTokenData = memoizedTokens.find( const additionalTokenData = memoizedTokens.find((t) =>
(t) => t.address === token.address, isEqualCaseInsensitive(t.address, token.address),
); );
return { ...token, isERC721: additionalTokenData?.isERC721 }; return {
...token,
isERC721: additionalTokenData?.isERC721,
image: additionalTokenData?.image,
};
}); });
setTokensWithBalances(matchingTokensWithIsERC721Flag); setTokensWithBalances(matchingTokensWithIsERC721Flag);
setLoading(false); setLoading(false);

View File

@ -8,10 +8,12 @@ import { camelCaseToCapitalize } from '../helpers/utils/common.util';
import { PRIMARY, SECONDARY } from '../helpers/constants/common'; import { PRIMARY, SECONDARY } from '../helpers/constants/common';
import { getTokenAddressParam } from '../helpers/utils/token-util'; import { getTokenAddressParam } from '../helpers/utils/token-util';
import { import {
isEqualCaseInsensitive,
formatDateWithYearContext, formatDateWithYearContext,
shortenAddress, shortenAddress,
stripHttpSchemes, stripHttpSchemes,
} from '../helpers/utils/util'; } from '../helpers/utils/util';
import { import {
PENDING_STATUS_HASH, PENDING_STATUS_HASH,
TOKEN_CATEGORY_HASH, TOKEN_CATEGORY_HASH,
@ -97,7 +99,9 @@ export function useTransactionDisplayData(transactionGroup) {
// hook to return null // hook to return null
const token = const token =
isTokenCategory && isTokenCategory &&
knownTokens.find(({ address }) => address === recipientAddress); knownTokens.find(({ address }) =>
isEqualCaseInsensitive(address, recipientAddress),
);
const tokenData = useTokenData( const tokenData = useTokenData(
initialTransaction?.txParams?.data, initialTransaction?.txParams?.data,
isTokenCategory, isTokenCategory,

View File

@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { Redirect, useParams } from 'react-router-dom'; import { Redirect, useParams } from 'react-router-dom';
import { getTokens } from '../../ducks/metamask/metamask'; import { getTokens } from '../../ducks/metamask/metamask';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import NativeAsset from './components/native-asset'; import NativeAsset from './components/native-asset';
import TokenAsset from './components/token-asset'; import TokenAsset from './components/token-asset';
@ -12,7 +13,9 @@ const Asset = () => {
const tokens = useSelector(getTokens); const tokens = useSelector(getTokens);
const { asset } = useParams(); const { asset } = useParams();
const token = tokens.find(({ address }) => address === asset); const token = tokens.find(({ address }) =>
isEqualCaseInsensitive(address, asset),
);
useEffect(() => { useEffect(() => {
const el = document.querySelector('.app'); const el = document.querySelector('.app');

View File

@ -5,6 +5,7 @@ import Identicon from '../../components/ui/identicon';
import TokenBalance from '../../components/ui/token-balance'; import TokenBalance from '../../components/ui/token-balance';
import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
export default class ConfirmAddSuggestedToken extends Component { export default class ConfirmAddSuggestedToken extends Component {
static contextTypes = { static contextTypes = {
@ -14,28 +15,31 @@ export default class ConfirmAddSuggestedToken extends Component {
static propTypes = { static propTypes = {
history: PropTypes.object, history: PropTypes.object,
addToken: PropTypes.func, acceptWatchAsset: PropTypes.func,
rejectWatchAsset: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired,
pendingTokens: PropTypes.object, suggestedAssets: PropTypes.array,
removeSuggestedTokens: PropTypes.func,
tokens: PropTypes.array, tokens: PropTypes.array,
}; };
componentDidMount() { componentDidMount() {
this._checkPendingTokens(); this._checksuggestedAssets();
} }
componentDidUpdate() { componentDidUpdate() {
this._checkPendingTokens(); this._checksuggestedAssets();
} }
_checkPendingTokens() { _checksuggestedAssets() {
const { mostRecentOverviewPage, pendingTokens = {}, history } = this.props; const {
mostRecentOverviewPage,
suggestedAssets = [],
history,
} = this.props;
if (Object.keys(pendingTokens).length > 0) { if (suggestedAssets.length > 0) {
return; return;
} }
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
global.platform.closeCurrentWindow(); global.platform.closeCurrentWindow();
} else { } else {
@ -49,17 +53,19 @@ export default class ConfirmAddSuggestedToken extends Component {
render() { render() {
const { const {
addToken, suggestedAssets,
pendingTokens,
tokens, tokens,
removeSuggestedTokens, rejectWatchAsset,
history, history,
mostRecentOverviewPage, mostRecentOverviewPage,
acceptWatchAsset,
} = this.props; } = this.props;
const pendingTokenKey = Object.keys(pendingTokens)[0];
const pendingToken = pendingTokens[pendingTokenKey]; const hasTokenDuplicates = this.checkTokenDuplicates(
const hasTokenDuplicates = this.checkTokenDuplicates(pendingTokens, tokens); suggestedAssets,
const reusesName = this.checkNameReuse(pendingTokens, tokens); tokens,
);
const reusesName = this.checkNameReuse(suggestedAssets, tokens);
return ( return (
<div className="page-container"> <div className="page-container">
@ -90,27 +96,25 @@ export default class ConfirmAddSuggestedToken extends Component {
</div> </div>
</div> </div>
<div className="confirm-add-token__token-list"> <div className="confirm-add-token__token-list">
{Object.entries(pendingTokens).map(([address, token]) => { {suggestedAssets.map(({ asset }) => {
const { name, symbol, image } = token;
return ( return (
<div <div
className="confirm-add-token__token-list-item" className="confirm-add-token__token-list-item"
key={address} key={asset.address}
> >
<div className="confirm-add-token__token confirm-add-token__data"> <div className="confirm-add-token__token confirm-add-token__data">
<Identicon <Identicon
className="confirm-add-token__token-icon" className="confirm-add-token__token-icon"
diameter={48} diameter={48}
address={address} address={asset.address}
image={image} image={asset.image}
/> />
<div className="confirm-add-token__name"> <div className="confirm-add-token__name">
{this.getTokenName(name, symbol)} {this.getTokenName(asset.name, asset.symbol)}
</div> </div>
</div> </div>
<div className="confirm-add-token__balance"> <div className="confirm-add-token__balance">
<TokenBalance token={token} /> <TokenBalance token={asset} />
</div> </div>
</div> </div>
); );
@ -124,10 +128,11 @@ export default class ConfirmAddSuggestedToken extends Component {
type="default" type="default"
large large
className="page-container__footer-button" className="page-container__footer-button"
onClick={() => { onClick={async () => {
removeSuggestedTokens().then(() => await Promise.all(
history.push(mostRecentOverviewPage), suggestedAssets.map(async ({ id }) => rejectWatchAsset(id)),
); );
history.push(mostRecentOverviewPage);
}} }}
> >
{this.context.t('cancel')} {this.context.t('cancel')}
@ -136,24 +141,25 @@ export default class ConfirmAddSuggestedToken extends Component {
type="secondary" type="secondary"
large large
className="page-container__footer-button" className="page-container__footer-button"
disabled={pendingTokens.length === 0} disabled={suggestedAssets.length === 0}
onClick={() => { onClick={async () => {
addToken(pendingToken) await Promise.all(
.then(() => removeSuggestedTokens()) suggestedAssets.map(async ({ asset, id }) => {
.then(() => { await acceptWatchAsset(id);
this.context.trackEvent({ this.context.trackEvent({
event: 'Token Added', event: 'Token Added',
category: 'Wallet', category: 'Wallet',
sensitiveProperties: { sensitiveProperties: {
token_symbol: pendingToken.symbol, token_symbol: asset.symbol,
token_contract_address: pendingToken.address, token_contract_address: asset.address,
token_decimal_precision: pendingToken.decimals, token_decimal_precision: asset.decimals,
unlisted: pendingToken.unlisted, unlisted: asset.unlisted,
source: 'dapp', source: 'dapp',
}, },
}); });
}) }),
.then(() => history.push(mostRecentOverviewPage)); );
history.push(mostRecentOverviewPage);
}} }}
> >
{this.context.t('addToken')} {this.context.t('addToken')}
@ -164,9 +170,11 @@ export default class ConfirmAddSuggestedToken extends Component {
); );
} }
checkTokenDuplicates(pendingTokens, tokens) { checkTokenDuplicates(suggestedAssets, tokens) {
const pending = Object.keys(pendingTokens); const pending = suggestedAssets.map(({ asset }) =>
const existing = tokens.map((token) => token.address); asset.address.toUpperCase(),
);
const existing = tokens.map((token) => token.address.toUpperCase());
const dupes = pending.filter((proposed) => { const dupes = pending.filter((proposed) => {
return existing.includes(proposed); return existing.includes(proposed);
}); });
@ -175,18 +183,18 @@ 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. * - Share a symbol with an existing `tokens` member.
* - Does not share an address with that same `tokens` member. * - Does not share an address with that same `tokens` member.
* This should be flagged as possibly deceptive or confusing. * This should be flagged as possibly deceptive or confusing.
*/ */
checkNameReuse(pendingTokens, tokens) { checkNameReuse(suggestedAssets, tokens) {
const duplicates = Object.keys(pendingTokens) const duplicates = suggestedAssets.filter(({ asset }) => {
.map((addr) => pendingTokens[addr]) const dupes = tokens.filter(
.filter((token) => { (old) =>
const dupes = tokens old.symbol === asset.symbol &&
.filter((old) => old.symbol === token.symbol) !isEqualCaseInsensitive(old.address, asset.address),
.filter((old) => old.address !== token.address); );
return dupes.length > 0; return dupes.length > 0;
}); });
return duplicates.length > 0; return duplicates.length > 0;

View File

@ -1,28 +1,28 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { compose } from 'redux'; import { compose } from 'redux';
import { withRouter } from 'react-router-dom'; 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 { getMostRecentOverviewPage } from '../../ducks/history/history';
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'; import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
metamask: { pendingTokens, suggestedTokens, tokens }, metamask: { suggestedAssets, tokens },
} = state; } = state;
const params = { ...pendingTokens, ...suggestedTokens };
return { return {
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
pendingTokens: params, suggestedAssets,
tokens, tokens,
}; };
}; };
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
addToken: ({ address, symbol, decimals, image }) => rejectWatchAsset: (suggestedAssetID) =>
dispatch(addToken(address, symbol, Number(decimals), image)), dispatch(rejectWatchAsset(suggestedAssetID)),
removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), acceptWatchAsset: (suggestedAssetID) =>
dispatch(acceptWatchAsset(suggestedAssetID)),
}; };
}; };

View File

@ -31,6 +31,7 @@ import { useApproveTransaction } from '../../hooks/useApproveTransaction';
import { currentNetworkTxListSelector } from '../../selectors/transactions'; import { currentNetworkTxListSelector } from '../../selectors/transactions';
import Loading from '../../components/ui/loading-screen'; import Loading from '../../components/ui/loading-screen';
import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component'; 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 { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content'; import ConfirmApproveContent from './confirm-approve-content';
@ -60,7 +61,9 @@ export default function ConfirmApprove() {
); );
const currentToken = (tokens && const currentToken = (tokens &&
tokens.find(({ address }) => tokenAddress === address)) || { tokens.find(({ address }) =>
isEqualCaseInsensitive(tokenAddress, address),
)) || {
address: tokenAddress, address: tokenAddress,
}; };

View File

@ -13,6 +13,7 @@ import {
getTokenValueParam, getTokenValueParam,
} from '../../helpers/utils/token-util'; } from '../../helpers/utils/token-util';
import { hexWEIToDecETH } from '../../helpers/utils/conversions.util'; import { hexWEIToDecETH } from '../../helpers/utils/conversions.util';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'; import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component';
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
@ -48,7 +49,9 @@ const mapStateToProps = (state, ownProps) => {
hexMaximumTransactionFee, hexMaximumTransactionFee,
} = transactionFeeSelector(state, transaction); } = transactionFeeSelector(state, transaction);
const tokens = getTokens(state); 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 { decimals, symbol: tokenSymbol } = currentToken || {};
const ethTransactionTotalMaxAmount = Number( const ethTransactionTotalMaxAmount = Number(

View File

@ -81,7 +81,6 @@ export default class ConfirmTransactionBase extends Component {
useNonceField: PropTypes.bool, useNonceField: PropTypes.bool,
customNonceValue: PropTypes.string, customNonceValue: PropTypes.string,
updateCustomNonce: PropTypes.func, updateCustomNonce: PropTypes.func,
assetImage: PropTypes.string,
sendTransaction: PropTypes.func, sendTransaction: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func, showTransactionConfirmedModal: PropTypes.func,
showRejectTransactionsConfirmationModal: PropTypes.func, showRejectTransactionsConfirmationModal: PropTypes.func,
@ -908,7 +907,6 @@ export default class ConfirmTransactionBase extends Component {
onEdit, onEdit,
nonce, nonce,
customNonceValue, customNonceValue,
assetImage,
unapprovedTxCount, unapprovedTxCount,
type, type,
hideSenderToRecipient, hideSenderToRecipient,
@ -967,7 +965,6 @@ export default class ConfirmTransactionBase extends Component {
contentComponent={contentComponent} contentComponent={contentComponent}
nonce={customNonceValue || nonce} nonce={customNonceValue || nonce}
unapprovedTxCount={unapprovedTxCount} unapprovedTxCount={unapprovedTxCount}
assetImage={assetImage}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
errorMessage={submitError} errorMessage={submitError}
errorKey={errorKey} errorKey={errorKey}

View File

@ -76,7 +76,6 @@ const mapStateToProps = (state, ownProps) => {
conversionRate, conversionRate,
identities, identities,
addressBook, addressBook,
assetImages,
network, network,
unapprovedTxs, unapprovedTxs,
nextNonce, nextNonce,
@ -97,7 +96,6 @@ const mapStateToProps = (state, ownProps) => {
data, data,
} = (transaction && transaction.txParams) || txParams; } = (transaction && transaction.txParams) || txParams;
const accounts = getMetaMaskAccounts(state); const accounts = getMetaMaskAccounts(state);
const assetImage = assetImages[txParamsToAddress];
const { balance } = accounts[fromAddress]; const { balance } = accounts[fromAddress];
const { name: fromName } = identities[fromAddress]; const { name: fromName } = identities[fromAddress];
@ -191,7 +189,6 @@ const mapStateToProps = (state, ownProps) => {
conversionRate, conversionRate,
transactionStatus, transactionStatus,
nonce, nonce,
assetImage,
unapprovedTxs, unapprovedTxs,
unapprovedTxCount, unapprovedTxCount,
currentNetworkUnapprovedTxs, currentNetworkUnapprovedTxs,

View File

@ -49,7 +49,7 @@ export default class Home extends PureComponent {
static propTypes = { static propTypes = {
history: PropTypes.object, history: PropTypes.object,
forgottenPassword: PropTypes.bool, forgottenPassword: PropTypes.bool,
suggestedTokens: PropTypes.object, suggestedAssets: PropTypes.array,
unconfirmedTransactionsCount: PropTypes.number, unconfirmedTransactionsCount: PropTypes.number,
shouldShowSeedPhraseReminder: PropTypes.bool.isRequired, shouldShowSeedPhraseReminder: PropTypes.bool.isRequired,
isPopup: PropTypes.bool, isPopup: PropTypes.bool,
@ -87,6 +87,7 @@ export default class Home extends PureComponent {
}; };
state = { state = {
// eslint-disable-next-line react/no-unused-state
mounted: false, mounted: false,
canShowBlockageNotification: true, canShowBlockageNotification: true,
}; };
@ -96,7 +97,7 @@ export default class Home extends PureComponent {
firstPermissionsRequestId, firstPermissionsRequestId,
history, history,
isNotification, isNotification,
suggestedTokens = {}, suggestedAssets = [],
totalUnapprovedCount, totalUnapprovedCount,
unconfirmedTransactionsCount, unconfirmedTransactionsCount,
haveSwapsQuotes, haveSwapsQuotes,
@ -105,6 +106,7 @@ export default class Home extends PureComponent {
pendingConfirmations, pendingConfirmations,
} = this.props; } = this.props;
// eslint-disable-next-line react/no-unused-state
this.setState({ mounted: true }); this.setState({ mounted: true });
if (isNotification && totalUnapprovedCount === 0) { if (isNotification && totalUnapprovedCount === 0) {
global.platform.closeCurrentWindow(); global.platform.closeCurrentWindow();
@ -118,7 +120,7 @@ export default class Home extends PureComponent {
history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`); history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`);
} else if (unconfirmedTransactionsCount > 0) { } else if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE); history.push(CONFIRM_TRANSACTION_ROUTE);
} else if (Object.keys(suggestedTokens).length > 0) { } else if (suggestedAssets.length > 0) {
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE); history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE);
} else if (pendingConfirmations.length > 0) { } else if (pendingConfirmations.length > 0) {
history.push(CONFIRMATION_V_NEXT_ROUTE); history.push(CONFIRMATION_V_NEXT_ROUTE);
@ -129,7 +131,7 @@ export default class Home extends PureComponent {
{ {
firstPermissionsRequestId, firstPermissionsRequestId,
isNotification, isNotification,
suggestedTokens, suggestedAssets,
totalUnapprovedCount, totalUnapprovedCount,
unconfirmedTransactionsCount, unconfirmedTransactionsCount,
haveSwapsQuotes, haveSwapsQuotes,
@ -144,7 +146,7 @@ export default class Home extends PureComponent {
} else if ( } else if (
firstPermissionsRequestId || firstPermissionsRequestId ||
unconfirmedTransactionsCount > 0 || unconfirmedTransactionsCount > 0 ||
Object.keys(suggestedTokens).length > 0 || suggestedAssets.length > 0 ||
(!isNotification && (!isNotification &&
(showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams)) (showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams))
) { ) {

View File

@ -46,7 +46,7 @@ import Home from './home.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { metamask, appState } = state; const { metamask, appState } = state;
const { const {
suggestedTokens, suggestedAssets,
seedPhraseBackedUp, seedPhraseBackedUp,
tokens, tokens,
threeBoxSynced, threeBoxSynced,
@ -83,7 +83,7 @@ const mapStateToProps = (state) => {
return { return {
forgottenPassword, forgottenPassword,
suggestedTokens, suggestedAssets,
swapsEnabled, swapsEnabled,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
shouldShowSeedPhraseReminder: shouldShowSeedPhraseReminder:

View File

@ -224,6 +224,7 @@ export default class MobileSyncPage extends Component {
network, network,
preferences, preferences,
transactions, transactions,
tokens,
} = await this.props.fetchInfoToSync(); } = await this.props.fetchInfoToSync();
const { t } = this.context; const { t } = this.context;
@ -232,6 +233,7 @@ export default class MobileSyncPage extends Component {
network, network,
preferences, preferences,
transactions, transactions,
tokens,
udata: { udata: {
pwd: this.state.password, pwd: this.state.password,
seed: this.state.seedWords, seed: this.state.seedWords,

View File

@ -6,6 +6,7 @@ import TokenBalance from '../../../../components/ui/token-balance';
import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display';
import { ERC20, PRIMARY } from '../../../../helpers/constants/common'; import { ERC20, PRIMARY } from '../../../../helpers/constants/common';
import { ASSET_TYPES } from '../../../../ducks/send'; import { ASSET_TYPES } from '../../../../ducks/send';
import { isEqualCaseInsensitive } from '../../../../helpers/utils/util';
export default class SendAssetRow extends Component { export default class SendAssetRow extends Component {
static propTypes = { static propTypes = {
@ -14,10 +15,10 @@ export default class SendAssetRow extends Component {
address: PropTypes.string, address: PropTypes.string,
decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
symbol: PropTypes.string, symbol: PropTypes.string,
image: PropTypes.string,
}), }),
).isRequired, ).isRequired,
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
assetImages: PropTypes.object,
selectedAddress: PropTypes.string.isRequired, selectedAddress: PropTypes.string.isRequired,
sendAssetAddress: PropTypes.string, sendAssetAddress: PropTypes.string,
updateSendAsset: PropTypes.func.isRequired, updateSendAsset: PropTypes.func.isRequired,
@ -85,8 +86,8 @@ export default class SendAssetRow extends Component {
renderSendToken() { renderSendToken() {
const { sendAssetAddress } = this.props; const { sendAssetAddress } = this.props;
const token = this.props.tokens.find( const token = this.props.tokens.find(({ address }) =>
({ address }) => address === sendAssetAddress, isEqualCaseInsensitive(address, sendAssetAddress),
); );
return ( return (
<div <div
@ -168,9 +169,8 @@ export default class SendAssetRow extends Component {
} }
renderAsset(token, insideDropdown = false) { renderAsset(token, insideDropdown = false) {
const { address, symbol } = token; const { address, symbol, image } = token;
const { t } = this.context; const { t } = this.context;
const { assetImages } = this.props;
return ( return (
<div <div
@ -179,11 +179,7 @@ export default class SendAssetRow extends Component {
onClick={() => this.selectToken(ASSET_TYPES.TOKEN, token)} onClick={() => this.selectToken(ASSET_TYPES.TOKEN, token)}
> >
<div className="send-v2__asset-dropdown__asset-icon"> <div className="send-v2__asset-dropdown__asset-icon">
<Identicon <Identicon address={address} diameter={36} image={image} />
address={address}
diameter={36}
image={assetImages[address]}
/>
</div> </div>
<div className="send-v2__asset-dropdown__asset-data"> <div className="send-v2__asset-dropdown__asset-data">
<div className="send-v2__asset-dropdown__symbol">{symbol}</div> <div className="send-v2__asset-dropdown__symbol">{symbol}</div>

View File

@ -3,7 +3,6 @@ import { getNativeCurrency } from '../../../../ducks/metamask/metamask';
import { import {
getMetaMaskAccounts, getMetaMaskAccounts,
getNativeCurrencyImage, getNativeCurrencyImage,
getAssetImages,
} from '../../../../selectors'; } from '../../../../selectors';
import { updateSendAsset, getSendAssetAddress } from '../../../../ducks/send'; import { updateSendAsset, getSendAssetAddress } from '../../../../ducks/send';
import SendAssetRow from './send-asset-row.component'; import SendAssetRow from './send-asset-row.component';
@ -16,7 +15,6 @@ function mapStateToProps(state) {
accounts: getMetaMaskAccounts(state), accounts: getMetaMaskAccounts(state),
nativeCurrency: getNativeCurrency(state), nativeCurrency: getNativeCurrency(state),
nativeCurrencyImage: getNativeCurrencyImage(state), nativeCurrencyImage: getNativeCurrencyImage(state),
assetImages: getAssetImages(state),
}; };
} }

View File

@ -15,7 +15,11 @@ import {
ALLOWED_SWAPS_CHAIN_IDS, ALLOWED_SWAPS_CHAIN_IDS,
} from '../../shared/constants/swaps'; } from '../../shared/constants/swaps';
import { shortenAddress, getAccountByAddress } from '../helpers/utils/util'; import {
shortenAddress,
getAccountByAddress,
isEqualCaseInsensitive,
} from '../helpers/utils/util';
import { import {
getValueFromWeiHex, getValueFromWeiHex,
hexToDecimal, hexToDecimal,
@ -262,11 +266,6 @@ export function getTargetAccount(state, targetAddress) {
export const getTokenExchangeRates = (state) => export const getTokenExchangeRates = (state) =>
state.metamask.contractExchangeRates; state.metamask.contractExchangeRates;
export function getAssetImages(state) {
const assetImages = state.metamask.assetImages || {};
return assetImages;
}
export function getAddressBook(state) { export function getAddressBook(state) {
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
if (!state.metamask.addressBook[chainId]) { if (!state.metamask.addressBook[chainId]) {
@ -277,8 +276,8 @@ export function getAddressBook(state) {
export function getAddressBookEntry(state, address) { export function getAddressBookEntry(state, address) {
const addressBook = getAddressBook(state); const addressBook = getAddressBook(state);
const entry = addressBook.find( const entry = addressBook.find((contact) =>
(contact) => contact.address === toChecksumHexAddress(address), isEqualCaseInsensitive(contact.address, toChecksumHexAddress(address)),
); );
return entry; return entry;
} }
@ -355,7 +354,7 @@ export function getTotalUnapprovedCount(state) {
unapprovedTypedMessagesCount + unapprovedTypedMessagesCount +
getUnapprovedTxCount(state) + getUnapprovedTxCount(state) +
pendingApprovalCount + pendingApprovalCount +
getSuggestedTokenCount(state) getSuggestedAssetCount(state)
); );
} }
@ -376,9 +375,9 @@ export function getUnapprovedTemplatedConfirmations(state) {
); );
} }
function getSuggestedTokenCount(state) { function getSuggestedAssetCount(state) {
const { suggestedTokens = {} } = state.metamask; const { suggestedAssets = [] } = state.metamask;
return Object.keys(suggestedTokens).length; return suggestedAssets.length;
} }
export function getIsMainnet(state) { export function getIsMainnet(state) {

View File

@ -45,7 +45,6 @@ export const SET_NEXT_NONCE = 'SET_NEXT_NONCE';
// config screen // config screen
export const SET_RPC_TARGET = 'SET_RPC_TARGET'; export const SET_RPC_TARGET = 'SET_RPC_TARGET';
export const SET_PROVIDER_TYPE = 'SET_PROVIDER_TYPE'; export const SET_PROVIDER_TYPE = 'SET_PROVIDER_TYPE';
export const UPDATE_TOKENS = 'UPDATE_TOKENS';
export const SET_HARDWARE_WALLET_DEFAULT_HD_PATH = export const SET_HARDWARE_WALLET_DEFAULT_HD_PATH =
'SET_HARDWARE_WALLET_DEFAULT_HD_PATH'; 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH';
// loading overlay // loading overlay

View File

@ -1122,10 +1122,9 @@ export function lockMetamask() {
}; };
} }
async function _setSelectedAddress(dispatch, address) { async function _setSelectedAddress(address) {
log.debug(`background.setSelectedAddress`); log.debug(`background.setSelectedAddress`);
const tokens = await promisifiedBackground.setSelectedAddress(address); await promisifiedBackground.setSelectedAddress(address);
dispatch(updateTokens(tokens));
} }
export function setSelectedAddress(address) { export function setSelectedAddress(address) {
@ -1133,7 +1132,7 @@ export function setSelectedAddress(address) {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
log.debug(`background.setSelectedAddress`); log.debug(`background.setSelectedAddress`);
try { try {
await _setSelectedAddress(dispatch, address); await _setSelectedAddress(address);
} catch (error) { } catch (error) {
dispatch(displayWarning(error.message)); dispatch(displayWarning(error.message));
return; return;
@ -1168,7 +1167,7 @@ export function showAccountDetail(address) {
!currentTabIsConnectedToNextAddress; !currentTabIsConnectedToNextAddress;
try { try {
await _setSelectedAddress(dispatch, address); await _setSelectedAddress(address);
await forceUpdateMetamaskState(dispatch); await forceUpdateMetamaskState(dispatch);
} catch (error) { } catch (error) {
dispatch(displayWarning(error.message)); dispatch(displayWarning(error.message));
@ -1241,43 +1240,37 @@ export function addToken(
image, image,
dontShowLoadingIndicator, dontShowLoadingIndicator,
) { ) {
return (dispatch) => { return async (dispatch) => {
if (!address) { if (!address) {
throw new Error('MetaMask - Cannot add token without address'); throw new Error('MetaMask - Cannot add token without address');
} }
if (!dontShowLoadingIndicator) { if (!dontShowLoadingIndicator) {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
} }
return new Promise((resolve, reject) => { try {
background.addToken(address, symbol, decimals, image, (err, tokens) => { await promisifiedBackground.addToken(address, symbol, decimals, image);
} catch (error) {
log.error(error);
dispatch(displayWarning(error.message));
} finally {
await forceUpdateMetamaskState(dispatch);
dispatch(hideLoadingIndication()); dispatch(hideLoadingIndication());
if (err) {
dispatch(displayWarning(err.message));
reject(err);
return;
} }
dispatch(updateTokens(tokens));
resolve(tokens);
});
});
}; };
} }
export function removeToken(address) { export function removeToken(address) {
return (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
return new Promise((resolve, reject) => { try {
background.removeToken(address, (err, tokens) => { await promisifiedBackground.removeToken(address);
} catch (error) {
log.error(error);
dispatch(displayWarning(error.message));
} finally {
await forceUpdateMetamaskState(dispatch);
dispatch(hideLoadingIndication()); dispatch(hideLoadingIndication());
if (err) {
dispatch(displayWarning(err.message));
reject(err);
return;
} }
dispatch(updateTokens(tokens));
resolve(tokens);
});
});
}; };
} }
@ -1298,27 +1291,35 @@ export function addTokens(tokens) {
}; };
} }
export function removeSuggestedTokens() { export function rejectWatchAsset(suggestedAssetID) {
return (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
return new Promise((resolve) => { try {
background.removeSuggestedTokens((err, suggestedTokens) => { await promisifiedBackground.rejectWatchAsset(suggestedAssetID);
dispatch(hideLoadingIndication()); } catch (error) {
if (err) { log.error(error);
dispatch(displayWarning(err.message)); dispatch(displayWarning(error.message));
}
dispatch(clearPendingTokens());
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
global.platform.closeCurrentWindow();
return; return;
} finally {
dispatch(hideLoadingIndication());
} }
resolve(suggestedTokens); dispatch(closeCurrentNotificationWindow());
}); };
}) }
.then(() => updateMetamaskStateFromBackground())
.then((suggestedTokens) => export function acceptWatchAsset(suggestedAssetID) {
dispatch(updateMetamaskState({ ...suggestedTokens })), 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());
}; };
} }
@ -1328,13 +1329,6 @@ export function addKnownMethodData(fourBytePrefix, methodData) {
}; };
} }
export function updateTokens(newTokens) {
return {
type: actionConstants.UPDATE_TOKENS,
newTokens,
};
}
export function clearPendingTokens() { export function clearPendingTokens() {
return { return {
type: actionConstants.CLEAR_PENDING_TOKENS, type: actionConstants.CLEAR_PENDING_TOKENS,
@ -2529,8 +2523,8 @@ export function getTokenParams(tokenAddress) {
return (dispatch, getState) => { return (dispatch, getState) => {
const tokenList = getTokenList(getState()); const tokenList = getTokenList(getState());
const existingTokens = getState().metamask.tokens; const existingTokens = getState().metamask.tokens;
const existingToken = existingTokens.find( const existingToken = existingTokens.find(({ address }) =>
({ address }) => tokenAddress === address, isEqualCaseInsensitive(tokenAddress, address),
); );
if (existingToken) { if (existingToken) {

View File

@ -1069,6 +1069,7 @@ describe('Actions', () => {
background.getApi.returns({ background.getApi.returns({
addToken: addTokenStub, addToken: addTokenStub,
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
}); });
actions._setBackgroundConnection(background.getApi()); actions._setBackgroundConnection(background.getApi());
@ -1098,17 +1099,18 @@ describe('Actions', () => {
background.getApi.returns({ background.getApi.returns({
addToken: addTokenStub, addToken: addTokenStub,
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
}); });
actions._setBackgroundConnection(background.getApi()); actions._setBackgroundConnection(background.getApi());
const expectedActions = [ const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ {
type: 'UPDATE_TOKENS', type: 'UPDATE_METAMASK_STATE',
newTokens: tokenDetails, value: baseMockState,
}, },
{ type: 'HIDE_LOADING_INDICATION' },
]; ];
await store.dispatch( await store.dispatch(
@ -1121,38 +1123,6 @@ describe('Actions', () => {
expect(store.getActions()).toStrictEqual(expectedActions); 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', () => { describe('#removeToken', () => {
@ -1167,6 +1137,7 @@ describe('Actions', () => {
background.getApi.returns({ background.getApi.returns({
removeToken: removeTokenStub, removeToken: removeTokenStub,
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
}); });
actions._setBackgroundConnection(background.getApi()); actions._setBackgroundConnection(background.getApi());
@ -1175,24 +1146,27 @@ describe('Actions', () => {
expect(removeTokenStub.callCount).toStrictEqual(1); 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(); const store = mockStore();
background.getApi.returns({ background.getApi.returns({
removeToken: sinon.stub().callsFake((_, cb) => cb(new Error('error'))), removeToken: sinon.stub().callsFake((_, cb) => cb(new Error('error'))),
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
}); });
actions._setBackgroundConnection(background.getApi()); actions._setBackgroundConnection(background.getApi());
const expectedActions = [ const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' }, { type: 'DISPLAY_WARNING', value: 'error' },
{
type: 'UPDATE_METAMASK_STATE',
value: baseMockState,
},
{ type: 'HIDE_LOADING_INDICATION' },
]; ];
await expect(store.dispatch(actions.removeToken())).rejects.toThrow( await store.dispatch(actions.removeToken());
'error',
);
expect(store.getActions()).toStrictEqual(expectedActions); expect(store.getActions()).toStrictEqual(expectedActions);
}); });