Integrate TokensController (#11552)

* Integrate controllers/tokensController

* address rebase issues

* small cleanup

* addressing feedback

* more feedback
feature/default_network_editable
Alex Donesky 3 years ago committed by GitHub
parent ad7d85b04e
commit 490d3b8d40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .storybook/test-data.js
  2. 77
      app/scripts/controllers/detect-tokens.js
  3. 63
      app/scripts/controllers/detect-tokens.test.js
  4. 375
      app/scripts/controllers/preferences.js
  5. 623
      app/scripts/controllers/preferences.test.js
  6. 53
      app/scripts/controllers/token-rates-controller.test.js
  7. 6
      app/scripts/controllers/token-rates.js
  8. 3
      app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js
  9. 87
      app/scripts/metamask-controller.js
  10. 78
      app/scripts/migrations/063.js
  11. 251
      app/scripts/migrations/063.test.js
  12. 2
      app/scripts/migrations/index.js
  13. 1
      package.json
  14. 189
      test/e2e/fixtures/custom-token/state.json
  15. 1
      test/e2e/fixtures/imported-account/state.json
  16. 1
      test/e2e/tests/add-hide-token.spec.js
  17. 1
      test/e2e/tests/contract-interactions.spec.js
  18. 3
      ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
  19. 3
      ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
  20. 5
      ui/components/app/confirm-page-container/confirm-page-container.component.js
  21. 8
      ui/components/app/modals/hide-token-confirmation-modal/hide-token-confirmation-modal.js
  22. 1
      ui/components/app/token-cell/token-cell.js
  23. 7
      ui/components/app/token-list/token-list.js
  24. 3
      ui/components/app/transaction-list/transaction-list.component.js
  25. 12
      ui/components/app/wallet-overview/token-overview.js
  26. 11
      ui/components/ui/sender-to-recipient/sender-to-recipient.component.js
  27. 5
      ui/ducks/confirm-transaction/confirm-transaction.duck.js
  28. 7
      ui/ducks/metamask/metamask.js
  29. 18
      ui/ducks/metamask/metamask.test.js
  30. 6
      ui/hooks/useCurrentAsset.js
  31. 11
      ui/hooks/useTokenTracker.js
  32. 6
      ui/hooks/useTransactionDisplayData.js
  33. 5
      ui/pages/asset/asset.js
  34. 110
      ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
  35. 14
      ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
  36. 5
      ui/pages/confirm-approve/confirm-approve.js
  37. 5
      ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
  38. 3
      ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
  39. 3
      ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
  40. 12
      ui/pages/home/home.component.js
  41. 4
      ui/pages/home/home.container.js
  42. 2
      ui/pages/mobile-sync/mobile-sync.component.js
  43. 16
      ui/pages/send/send-content/send-asset-row/send-asset-row.component.js
  44. 2
      ui/pages/send/send-content/send-asset-row/send-asset-row.container.js
  45. 23
      ui/selectors/selectors.js
  46. 1
      ui/store/actionConstants.js
  47. 114
      ui/store/actions.js
  48. 54
      ui/store/actions.test.js

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

@ -24,12 +24,36 @@ export default class DetectTokensController {
network,
keyringMemStore,
tokenList,
tokensController,
} = {}) {
this.tokensController = tokensController;
this.preferences = preferences;
this.interval = interval;
this.network = network;
this.keyringMemStore = keyringMemStore;
this.tokenList = tokenList;
this.selectedAddress = this.preferences?.store.getState().selectedAddress;
this.tokenAddresses = this.tokensController?.state.tokens.map((token) => {
return token.address;
});
this.hiddenTokens = this.tokensController?.state.ignoredTokens;
preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
this.selectedAddress !== selectedAddress ||
this.useTokenDetection !== useTokenDetection
) {
this.selectedAddress = selectedAddress;
this.useTokenDetection = useTokenDetection;
this.restartTokenDetection();
}
});
tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address;
});
this.hiddenTokens = ignoredTokens;
});
}
async _getTokenBalances(tokens) {
@ -88,16 +112,19 @@ export default class DetectTokensController {
);
return;
}
const tokensWithBalance = tokensSlice.filter((_, index) => {
const balance = result[index];
return balance && !balance.isZero();
});
await Promise.all(
tokensSlice.map(async (tokenAddress, index) => {
const balance = result[index];
if (balance && !balance.isZero()) {
await this._preferences.addToken(
tokenAddress,
tokenList[tokenAddress].symbol,
tokenList[tokenAddress].decimals,
);
}
tokensWithBalance.map((tokenAddress) => {
return this.tokensController.addToken(
tokenAddress,
tokenList[tokenAddress].symbol,
tokenList[tokenAddress].decimals,
);
}),
);
}
@ -130,38 +157,6 @@ export default class DetectTokensController {
}, interval);
}
/**
* In setter when selectedAddress is changed, detectNewTokens and restart polling
* @type {Object}
*/
set preferences(preferences) {
if (!preferences) {
return;
}
this._preferences = preferences;
const currentTokens = preferences.store.getState().tokens;
this.tokenAddresses = currentTokens
? currentTokens.map((token) => token.address)
: [];
this.hiddenTokens = preferences.store.getState().hiddenTokens;
preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address;
});
this.hiddenTokens = hiddenTokens;
});
preferences.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
this.selectedAddress !== selectedAddress ||
this.useTokenDetection !== useTokenDetection
) {
this.selectedAddress = selectedAddress;
this.useTokenDetection = useTokenDetection;
this.restartTokenDetection();
}
});
}
/**
* @type {Object}
*/

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

@ -1,22 +1,12 @@
import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors';
import { normalize as normalizeAddress } from 'eth-sig-util';
import { ethers } from 'ethers';
import log from 'loglevel';
import abiERC721 from 'human-standard-collectible-abi';
import contractsMap from '@metamask/contract-metadata';
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import {
isValidHexAddress,
toChecksumHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { NETWORK_EVENTS } from './network';
const ERC721_INTERFACE_ID = '0x80ac58cd';
export default class PreferencesController {
/**
*
@ -24,9 +14,6 @@ export default class PreferencesController {
* @param {Object} opts - Overrides the defaults for the initial state of this.store
* @property {Object} store The stored object containing a users preferences, stored in local storage
* @property {Array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {Array} store.tokens The tokens the user wants display in their token lists
* @property {Object} store.accountTokens The tokens stored per account and then per network type
* @property {Object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {boolean} store.useNonceField The users preference for nonce field within the UI
* @property {Object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
@ -41,12 +28,6 @@ export default class PreferencesController {
constructor(opts = {}) {
const initState = {
frequentRpcListDetail: [],
accountTokens: {},
accountHiddenTokens: {},
assetImages: {},
tokens: [],
hiddenTokens: [],
suggestedTokens: {},
useBlockie: false,
useNonceField: false,
usePhishDetect: true,
@ -90,12 +71,6 @@ export default class PreferencesController {
this.openPopup = opts.openPopup;
this.migrateAddressBookState = opts.migrateAddressBookState;
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
this._subscribeToInfuraAvailability();
global.setPreference = (key, value) => {
@ -162,14 +137,6 @@ export default class PreferencesController {
this.store.updateState({ firstTimeFlowType: type });
}
getSuggestedTokens() {
return this.store.getState().suggestedTokens;
}
getAssetImages() {
return this.store.getState().assetImages;
}
/**
* Add new methodData to state, to avoid requesting this information again through Infura
*
@ -182,24 +149,6 @@ export default class PreferencesController {
this.store.updateState({ knownMethodData });
}
/**
* wallet_watchAsset request handler.
*
* @param {Object} req - The watchAsset JSON-RPC request object.
*/
async requestWatchAsset(req) {
const { type, options } = req.params;
switch (type) {
case 'ERC20':
return await this._handleWatchAssetERC20(options);
default:
throw ethErrors.rpc.invalidParams(
`Asset of type "${type}" not supported.`,
);
}
}
/**
* Setter for the `currentLocale` property
*
@ -226,25 +175,14 @@ export default class PreferencesController {
*/
setAddresses(addresses) {
const oldIdentities = this.store.getState().identities;
const oldAccountTokens = this.store.getState().accountTokens;
const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens;
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {};
ids[address] = { name: `Account ${index + 1}`, address, ...oldId };
return ids;
}, {});
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {};
tokens[address] = oldTokens;
return tokens;
}, {});
const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => {
const oldHiddenTokens = oldAccountHiddenTokens[address] || {};
hiddenTokens[address] = oldHiddenTokens;
return hiddenTokens;
}, {});
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
this.store.updateState({ identities });
}
/**
@ -254,19 +192,13 @@ export default class PreferencesController {
* @returns {string} the address that was removed
*/
removeAddress(address) {
const {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState();
const { identities } = this.store.getState();
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`);
}
delete identities[address];
delete accountTokens[address];
delete accountHiddenTokens[address];
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
this.store.updateState({ identities });
// If the selected account is no longer valid,
// select an arbitrary other account:
@ -284,11 +216,7 @@ export default class PreferencesController {
*
*/
addAddresses(addresses) {
const {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState();
const { identities } = this.store.getState();
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) {
@ -297,11 +225,9 @@ export default class PreferencesController {
// add missing identity
const identityCount = Object.keys(identities).length;
accountTokens[address] = {};
accountHiddenTokens[address] = {};
identities[address] = { name: `Account ${identityCount + 1}`, address };
});
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
this.store.updateState({ identities });
}
/**
@ -348,25 +274,16 @@ export default class PreferencesController {
return selected;
}
removeSuggestedTokens() {
return new Promise((resolve) => {
this.store.updateState({ suggestedTokens: {} });
resolve({});
});
}
/**
* Setter for the `selectedAddress` property
*
* @param {string} _address - A new hex address for an account
* @returns {Promise<void>} Promise resolves with tokens
*
*/
setSelectedAddress(_address) {
const address = normalizeAddress(_address);
this._updateTokens(address);
const { identities, tokens } = this.store.getState();
const { identities } = this.store.getState();
const selectedIdentity = identities[address];
if (!selectedIdentity) {
throw new Error(`Identity for '${address} not found`);
@ -374,7 +291,6 @@ export default class PreferencesController {
selectedIdentity.lastSelected = Date.now();
this.store.updateState({ identities, selectedAddress: address });
return Promise.resolve(tokens);
}
/**
@ -387,99 +303,6 @@ export default class PreferencesController {
return this.store.getState().selectedAddress;
}
/**
* Contains data about tokens users add to their account.
* @typedef {Object} AddedToken
* @property {string} address - The hex address for the token contract. Will be all lower cased and hex-prefixed.
* @property {string} symbol - The symbol of the token, usually 3 or 4 capitalized letters
* {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#symbol}
* @property {boolean} decimals - The number of decimals the token uses.
* {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#decimals}
*/
/**
* Adds a new token to the token array and removes it from the hiddenToken array, or updates the token if passed an address that already exists.
* Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects.
* @see AddedToken {@link AddedToken}
*
* @param {string} rawAddress - Hex address of the token contract. May or may not be a checksum address.
* @param {string} symbol - The symbol of the token
* @param {number} decimals - The number of decimals the token uses.
* @returns {Promise<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
* @param {string} account - the account to set a label for
@ -770,188 +593,4 @@ export default class PreferencesController {
this.store.updateState({ infuraBlocked: isBlocked });
}
/**
* Updates `accountTokens`, `tokens`, `accountHiddenTokens` and `hiddenTokens` of current account and network according to it.
*
* @param {array} tokens - Array of tokens to be updated.
* @param {array} assetImages - Array of assets objects related to assets added
* @param {array} hiddenTokens - Array of tokens hidden by user
*
*/
_updateAccountTokens(tokens, assetImages, hiddenTokens) {
const {
accountTokens,
chainId,
selectedAddress,
accountHiddenTokens,
} = this._getTokenRelatedStates();
accountTokens[selectedAddress][chainId] = tokens;
accountHiddenTokens[selectedAddress][chainId] = hiddenTokens;
this.store.updateState({
accountTokens,
tokens,
assetImages,
accountHiddenTokens,
hiddenTokens,
});
}
/**
* Detects whether or not a token is ERC-721 compatible.
*
* @param {string} tokensAddress - the token contract address.
*
*/
async _detectIsERC721(tokenAddress) {
const checksumAddress = toChecksumHexAddress(tokenAddress);
// if this token is already in our contract metadata map we don't need
// to check against the contract
if (contractsMap[checksumAddress]?.erc721 === true) {
return Promise.resolve(true);
}
const tokenContract = await this._createEthersContract(
tokenAddress,
abiERC721,
this.ethersProvider,
);
return await tokenContract
.supportsInterface(ERC721_INTERFACE_ID)
.catch((error) => {
log.debug(error);
return false;
});
}
async _createEthersContract(tokenAddress, abi, ethersProvider) {
const tokenContract = await new ethers.Contract(
tokenAddress,
abi,
ethersProvider,
);
return tokenContract;
}
/**
* Updates `tokens` and `hiddenTokens` of current account and network.
*
* @param {string} selectedAddress - Account address to be updated with.
*
*/
_updateTokens(selectedAddress) {
const { tokens, hiddenTokens } = this._getTokenRelatedStates(
selectedAddress,
);
this.store.updateState({ tokens, hiddenTokens });
}
/**
* A getter for `tokens`, `accountTokens`, `hiddenTokens` and `accountHiddenTokens` related states.
*
* @param {string} [selectedAddress] - A new hex address for an account
* @returns {Object.<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 });
}
}

@ -1,11 +1,6 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import contractMaps from '@metamask/contract-metadata';
import abiERC721 from 'human-standard-collectible-abi';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import PreferencesController from './preferences';
import NetworkController from './network';
@ -13,9 +8,6 @@ describe('preferences controller', function () {
let preferencesController;
let network;
let currentChainId;
let triggerNetworkChange;
let switchToMainnet;
let switchToRinkeby;
let provider;
const migrateAddressBookState = sinon.stub();
@ -37,22 +29,12 @@ describe('preferences controller', function () {
sandbox
.stub(network, 'getProviderConfig')
.callsFake(() => ({ type: 'mainnet' }));
const spy = sandbox.spy(network, 'on');
preferencesController = new PreferencesController({
migrateAddressBookState,
network,
provider,
});
triggerNetworkChange = spy.firstCall.args[1];
switchToMainnet = () => {
currentChainId = MAINNET_CHAIN_ID;
triggerNetworkChange();
};
switchToRinkeby = () => {
currentChainId = RINKEBY_CHAIN_ID;
triggerNetworkChange();
};
});
afterEach(function () {
@ -76,17 +58,6 @@ describe('preferences controller', function () {
});
});
it('should create account tokens for each account in the store', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
const { accountTokens } = preferencesController.store.getState();
assert.deepEqual(accountTokens, {
'0xda22le': {},
'0x7e57e2': {},
});
});
it('should replace its list of addresses', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
preferencesController.setAddresses(['0xda22le77', '0x7e57e277']);
@ -105,104 +76,6 @@ describe('preferences controller', function () {
});
});
describe('updateTokenType', function () {
it('should add isERC721 = true to token object in state when token is collectible and in our contract-metadata repo', async function () {
const contractAddresses = Object.keys(contractMaps);
const erc721ContractAddresses = contractAddresses.filter(
(contractAddress) => contractMaps[contractAddress].erc721 === true,
);
const address = erc721ContractAddresses[0];
const { symbol, decimals } = contractMaps[address];
preferencesController.store.updateState({
tokens: [{ address, symbol, decimals }],
});
const result = await preferencesController.updateTokenType(address);
assert.equal(result.isERC721, true);
});
it('should add isERC721 = true to token object in state when token is collectible and not in our contract-metadata repo', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
preferencesController.store.updateState({
tokens: [
{
address: tokenAddress,
symbol: 'TESTNFT',
decimals: '0',
},
],
});
sinon
.stub(preferencesController, '_detectIsERC721')
.callsFake(() => true);
const result = await preferencesController.updateTokenType(tokenAddress);
assert.equal(
preferencesController._detectIsERC721.getCall(0).args[0],
tokenAddress,
);
assert.equal(result.isERC721, true);
});
});
describe('_detectIsERC721', function () {
it('should return true when token is in our contract-metadata repo', async function () {
const tokenAddress = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d';
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(result, true);
});
it('should return true when the token is not in our contract-metadata repo but tokenContract.supportsInterface returns true', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
const supportsInterfaceStub = sinon.stub().returns(Promise.resolve(true));
sinon
.stub(preferencesController, '_createEthersContract')
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[0],
tokenAddress,
);
assert.deepEqual(
preferencesController._createEthersContract.getCall(0).args[1],
abiERC721,
);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[2],
preferencesController.ethersProvider,
);
assert.equal(result, true);
});
it('should return false when the token is not in our contract-metadata repo and tokenContract.supportsInterface returns false', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
const supportsInterfaceStub = sinon
.stub()
.returns(Promise.resolve(false));
sinon
.stub(preferencesController, '_createEthersContract')
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[0],
tokenAddress,
);
assert.deepEqual(
preferencesController._createEthersContract.getCall(0).args[1],
abiERC721,
);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[2],
preferencesController.ethersProvider,
);
assert.equal(result, false);
});
});
describe('removeAddress', function () {
it('should remove an address from state', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
@ -215,17 +88,6 @@ describe('preferences controller', function () {
);
});
it('should remove an address from state and respective tokens', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
preferencesController.removeAddress('0xda22le');
assert.equal(
preferencesController.store.getState().accountTokens['0xda22le'],
undefined,
);
});
it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
@ -259,489 +121,6 @@ describe('preferences controller', function () {
});
});
describe('getTokens', function () {
it('should return an empty list initially', async function () {
preferencesController.setAddresses(['0x7e57e2']);
await preferencesController.setSelectedAddress('0x7e57e2');
const tokens = preferencesController.getTokens();
assert.equal(tokens.length, 0, 'empty list of tokens');
});
});
describe('addToken', function () {
it('should add that token to its state', async function () {
const address = '0xabcdef1234567';
const symbol = 'ABBR';
const decimals = 5;
preferencesController.setAddresses(['0x7e57e2']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken(address, symbol, decimals);
const tokens = preferencesController.getTokens();
assert.equal(tokens.length, 1, 'one token added');
const added = tokens[0];
assert.equal(added.address, address, 'set address correctly');
assert.equal(added.symbol, symbol, 'set symbol correctly');
assert.equal(added.decimals, decimals, 'set decimals correctly');
});
it('should allow updating a token value', async function () {
const address = '0xabcdef1234567';
const symbol = 'ABBR';
const decimals = 5;
preferencesController.setAddresses(['0x7e57e2']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken(address, symbol, decimals);
const newDecimals = 6;
await preferencesController.addToken(address, symbol, newDecimals);
const tokens = preferencesController.getTokens();
assert.equal(tokens.length, 1, 'one token added');
const added = tokens[0];
assert.equal(added.address, address, 'set address correctly');
assert.equal(added.symbol, symbol, 'set symbol correctly');
assert.equal(added.decimals, newDecimals, 'updated decimals correctly');
});
it('should allow adding tokens to two separate addresses', async function () {
const address = '0xabcdef1234567';
const symbol = 'ABBR';
const decimals = 5;
preferencesController.setAddresses(['0x7e57e2', '0xda22le']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken(address, symbol, decimals);
assert.equal(
preferencesController.getTokens().length,
1,
'one token added for 1st address',
);
await preferencesController.setSelectedAddress('0xda22le');
await preferencesController.addToken(address, symbol, decimals);
assert.equal(
preferencesController.getTokens().length,
1,
'one token added for 2nd address',
);
});
it('should add token per account', async function () {
const addressFirst = '0xabcdef1234567';
const addressSecond = '0xabcdef1234568';
const symbolFirst = 'ABBR';
const symbolSecond = 'ABBB';
const decimals = 5;
preferencesController.setAddresses(['0x7e57e2', '0xda22le']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken(addressFirst, symbolFirst, decimals);
const tokensFirstAddress = preferencesController.getTokens();
await preferencesController.setSelectedAddress('0xda22le');
await preferencesController.addToken(
addressSecond,
symbolSecond,
decimals,
);
const tokensSeconAddress = preferencesController.getTokens();
assert.notEqual(
tokensFirstAddress,
tokensSeconAddress,
'add different tokens for two account and tokens are equal',
);
});
it('should add token per network', async function () {
const addressFirst = '0xabcdef1234567';
const addressSecond = '0xabcdef1234568';
const symbolFirst = 'ABBR';
const symbolSecond = 'ABBB';
const decimals = 5;
await preferencesController.addToken(addressFirst, symbolFirst, decimals);
const tokensFirstAddress = preferencesController.getTokens();
switchToRinkeby();
await preferencesController.addToken(
addressSecond,
symbolSecond,
decimals,
);
const tokensSeconAddress = preferencesController.getTokens();
assert.notEqual(
tokensFirstAddress,
tokensSeconAddress,
'add different tokens for two networks and tokens are equal',
);
});
});
describe('removeToken', function () {
it('should remove the only token from its state', async function () {
preferencesController.setAddresses(['0x7e57e2']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken('0xa', 'A', 5);
await preferencesController.removeToken('0xa');
const tokens = preferencesController.getTokens();
assert.equal(tokens.length, 0, 'one token removed');
});
it('should remove a token from its state', async function () {
preferencesController.setAddresses(['0x7e57e2']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
await preferencesController.removeToken('0xa');
const tokens = preferencesController.getTokens();
assert.equal(tokens.length, 1, 'one token removed');
const [token1] = tokens;
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
});
it('should remove a token from its state on corresponding address', async function () {
preferencesController.setAddresses(['0x7e57e2', '0x7e57e3']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
await preferencesController.setSelectedAddress('0x7e57e3');
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
const initialTokensSecond = preferencesController.getTokens();
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.removeToken('0xa');
const tokensFirst = preferencesController.getTokens();
assert.equal(tokensFirst.length, 1, 'one token removed in account');
const [token1] = tokensFirst;
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
await preferencesController.setSelectedAddress('0x7e57e3');
const tokensSecond = preferencesController.getTokens();
assert.deepEqual(
tokensSecond,
initialTokensSecond,
'token deleted for account',
);
});
it('should remove a token from its state on corresponding network', async function () {
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
switchToRinkeby();
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
const initialTokensSecond = preferencesController.getTokens();
switchToMainnet();
await preferencesController.removeToken('0xa');
const tokensFirst = preferencesController.getTokens();
assert.equal(tokensFirst.length, 1, 'one token removed in network');
const [token1] = tokensFirst;
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
switchToRinkeby();
const tokensSecond = preferencesController.getTokens();
assert.deepEqual(
tokensSecond,
initialTokensSecond,
'token deleted for network',
);
});
});
describe('on setSelectedAddress', function () {
it('should update tokens from its state on corresponding address', async function () {
preferencesController.setAddresses(['0x7e57e2', '0x7e57e3']);
await preferencesController.setSelectedAddress('0x7e57e2');
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
await preferencesController.setSelectedAddress('0x7e57e3');
await preferencesController.addToken('0xa', 'C', 4);
await preferencesController.addToken('0xb', 'D', 5);
await preferencesController.setSelectedAddress('0x7e57e2');
const initialTokensFirst = preferencesController.getTokens();
await preferencesController.setSelectedAddress('0x7e57e3');
const initialTokensSecond = preferencesController.getTokens();
assert.notDeepEqual(
initialTokensFirst,
initialTokensSecond,
'tokens not equal for different accounts and tokens',
);
await preferencesController.setSelectedAddress('0x7e57e2');
const tokensFirst = preferencesController.getTokens();
await preferencesController.setSelectedAddress('0x7e57e3');
const tokensSecond = preferencesController.getTokens();
assert.deepEqual(
tokensFirst,
initialTokensFirst,
'tokens equal for same account',
);
assert.deepEqual(
tokensSecond,
initialTokensSecond,
'tokens equal for same account',
);
});
});
describe('on updateStateNetworkType', function () {
it('should remove a token from its state on corresponding network', async function () {
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
const initialTokensFirst = preferencesController.getTokens();
switchToRinkeby();
await preferencesController.addToken('0xa', 'C', 4);
await preferencesController.addToken('0xb', 'D', 5);
const initialTokensSecond = preferencesController.getTokens();
assert.notDeepEqual(
initialTokensFirst,
initialTokensSecond,
'tokens not equal for different networks and tokens',
);
switchToMainnet();
const tokensFirst = preferencesController.getTokens();
switchToRinkeby();
const tokensSecond = preferencesController.getTokens();
assert.deepEqual(
tokensFirst,
initialTokensFirst,
'tokens equal for same network',
);
assert.deepEqual(
tokensSecond,
initialTokensSecond,
'tokens equal for same network',
);
});
});
describe('on watchAsset', function () {
let req, stubHandleWatchAssetERC20;
const sandbox = sinon.createSandbox();
beforeEach(function () {
req = { method: 'wallet_watchAsset', params: {} };
stubHandleWatchAssetERC20 = sandbox.stub(
preferencesController,
'_handleWatchAssetERC20',
);
});
after(function () {
sandbox.restore();
});
it('should error if passed no type', async function () {
await assert.rejects(
() => preferencesController.requestWatchAsset(req),
{ message: 'Asset of type "undefined" not supported.' },
'should have errored',
);
});
it('should error if method is not supported', async function () {
req.params.type = 'someasset';
await assert.rejects(
() => preferencesController.requestWatchAsset(req),
{ message: 'Asset of type "someasset" not supported.' },
'should have errored',
);
});
it('should handle ERC20 type', async function () {
req.params.type = 'ERC20';
await preferencesController.requestWatchAsset(req);
sandbox.assert.called(stubHandleWatchAssetERC20);
});
});
describe('on watchAsset of type ERC20', function () {
let req;
const sandbox = sinon.createSandbox();
beforeEach(function () {
req = { params: { type: 'ERC20' } };
});
after(function () {
sandbox.restore();
});
it('should add suggested token', async function () {
const address = '0xabcdef1234567';
const symbol = 'ABBR';
const decimals = 5;
const image = 'someimage';
req.params.options = { address, symbol, decimals, image };
sandbox
.stub(preferencesController, '_validateERC20AssetParams')
.returns(true);
preferencesController.openPopup = async () => undefined;
await preferencesController._handleWatchAssetERC20(req.params.options);
const suggested = preferencesController.getSuggestedTokens();
assert.equal(
Object.keys(suggested).length,
1,
`one token added ${Object.keys(suggested)}`,
);
assert.equal(
suggested[address].address,
address,
'set address correctly',
);
assert.equal(suggested[address].symbol, symbol, 'set symbol correctly');
assert.equal(
suggested[address].decimals,
decimals,
'set decimals correctly',
);
assert.equal(suggested[address].image, image, 'set image correctly');
});
it('should add token correctly if user confirms', async function () {
const address = '0xabcdef1234567';
const symbol = 'ABBR';
const decimals = 5;
const image = 'someimage';
req.params.options = { address, symbol, decimals, image };
sandbox
.stub(preferencesController, '_validateERC20AssetParams')
.returns(true);
preferencesController.openPopup = async () => {
await preferencesController.addToken(address, symbol, decimals, image);
};
await preferencesController._handleWatchAssetERC20(req.params.options);
const tokens = preferencesController.getTokens();
assert.equal(tokens.length, 1, `one token added`);
const added = tokens[0];
assert.equal(added.address, address, 'set address correctly');
assert.equal(added.symbol, symbol, 'set symbol correctly');
assert.equal(added.decimals, decimals, 'set decimals correctly');
const assetImages = preferencesController.getAssetImages();
assert.ok(assetImages[address], `set image correctly`);
});
it('should validate ERC20 asset correctly', async function () {
const validate = preferencesController._validateERC20AssetParams;
assert.doesNotThrow(() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC',
decimals: 0,
}),
);
assert.doesNotThrow(() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABCDEFGHIJK',
decimals: 0,
}),
);
assert.throws(
() => validate({ symbol: 'ABC', decimals: 0 }),
'missing address should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
decimals: 0,
}),
'missing symbol should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC',
}),
'missing decimals should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABCDEFGHIJKLM',
decimals: 0,
}),
'long symbol should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: '',
decimals: 0,
}),
'empty symbol should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC',
decimals: -1,
}),
'decimals < 0 should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC',
decimals: 38,
}),
'decimals > 36 should fail',
);
assert.throws(
() => validate({ address: '0x123', symbol: 'ABC', decimals: 0 }),
'invalid address should fail',
);
});
});
describe('setPasswordForgotten', function () {
it('should default to false', function () {
const state = preferencesController.store.getState();

@ -1,31 +1,62 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { ObservableStore } from '@metamask/obs-store';
import { TokensController } from '@metamask/controllers';
import TokenRatesController from './token-rates';
import NetworkController from './network';
import PreferencesController from './preferences';
const networkControllerProviderConfig = {
getAccounts: () => undefined,
};
describe('TokenRatesController', function () {
let nativeCurrency;
let getNativeCurrency;
let nativeCurrency,
getNativeCurrency,
network,
provider,
preferences,
tokensController;
beforeEach(function () {
nativeCurrency = 'ETH';
getNativeCurrency = () => nativeCurrency;
network = new NetworkController();
network.setInfuraProjectId('foo');
network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
preferences = new PreferencesController({ network, provider });
tokensController = new TokensController({
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: network.store.subscribe.bind(network.store),
});
sinon.stub(network, 'getLatestBlock').callsFake(() => Promise.resolve({}));
sinon.stub(tokensController, '_instantiateNewEthersProvider').returns(null);
sinon
.stub(tokensController, '_detectIsERC721')
.returns(Promise.resolve(false));
});
it('should listen for preferences store updates', function () {
const preferences = new ObservableStore({ tokens: [] });
preferences.putState({ tokens: ['foo'] });
it('should listen for tokenControllers state updates', async function () {
const controller = new TokenRatesController({
preferences,
tokensController,
getNativeCurrency,
});
assert.deepEqual(controller._tokens, ['foo']);
await tokensController.addToken('0x1', 'TEST', 1);
assert.deepEqual(controller._tokens, [
{
address: '0x1',
decimals: 1,
symbol: 'TEST',
image: undefined,
isERC721: false,
},
]);
});
it('should poll on correct interval', async function () {
const stub = sinon.stub(global, 'setInterval');
const preferences = new ObservableStore({ tokens: [] });
preferences.putState({ tokens: ['foo'] });
const controller = new TokenRatesController({
preferences,
tokensController,
getNativeCurrency,
});
controller.start(1337);

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

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

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

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

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

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

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

@ -1,150 +1,165 @@
{
"data": {
"AlertController": {
"alertEnabledness": {
"unconnectedAccount": true,
"web3ShimUsage": true
},
"unconnectedAccountAlertShownOrigins": {},
"web3ShimUsageOrigins": {}
},
"AppStateController": {
"mkrMigrationReminderTimestamp": null
"connectedStatusPopoverHasBeenShown": true,
"defaultHomeActiveTabName": null,
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": 1627317428214
},
"CachedBalancesController": {
"cachedBalances": {
"4": {}
"0x4": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": "0x0"
}
}
},
"CurrencyController": {
"conversionDate": 1575697244.188,
"conversionRate": 149.61,
"conversionDate": 1626907353.891,
"conversionRate": 1968.5,
"currentCurrency": "usd",
"nativeCurrency": "ETH"
"nativeCurrency": "ETH",
"pendingCurrentCurrency": null,
"pendingNativeCurrency": null,
"usdConversionRate": 1968.5
},
"IncomingTransactionsController": {
"incomingTransactions": {},
"incomingTxLastFetchedBlocksByNetwork": {
"goerli": null,
"kovan": null,
"mainnet": null,
"rinkeby": 5570536
"incomingTxLastFetchedBlockByChainId": {
"0x1": null,
"0x2a": null,
"0x3": null,
"0x4": 8977934,
"0x5": null
}
},
"KeyringController": {
"vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}"
},
"MetaMetricsController": {
"metaMetricsId": "0xff3e952b9f5a27ffcab42b0b4abf689e77dcc1f9f441871dc962d622b089fb51",
"participateInMetaMetrics": true
},
"NetworkController": {
"network": "1337",
"networkDetails": {
"EIPS": {}
},
"previousProviderStore": {
"chainId": "0x4",
"ticker": "ETH",
"type": "rinkeby"
},
"provider": {
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": {},
"rpcUrl": "http://localhost:8545",
"chainId": "0x539",
"ticker": "ETH",
"type": "rpc"
}
},
"NotificationController": {
"notifications": {
"1": {
"isShown": true
},
"3": {
"isShown": true
},
"5": {
"isShown": true
},
"6": {
"isShown": true
}
}
"notifications": {}
},
"OnboardingController": {
"onboardingTabs": {},
"seedPhraseBackedUp": false
"seedPhraseBackedUp": true
},
"PermissionsMetadata": {
"domainMetadata": {
"metamask.github.io": {
"icon": null,
"name": "M E T A M A S K M E S H T E S T"
}
},
"permissionsHistory": {},
"permissionsLog": [
{
"id": 746677923,
"method": "eth_accounts",
"methodType": "restricted",
"origin": "metamask.github.io",
"request": {
"id": 746677923,
"jsonrpc": "2.0",
"method": "eth_accounts",
"origin": "metamask.github.io",
"params": []
},
"requestTime": 1575697241368,
"response": {
"id": 746677923,
"jsonrpc": "2.0",
"result": []
},
"responseTime": 1575697241370,
"success": true
}
]
"PermissionsController": {
"domains": {},
"permissionsDescriptions": {},
"permissionsRequests": []
},
"PreferencesController": {
"accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
"0x539": [
{
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"symbol": "TST",
"decimals": 4
}
],
"rinkeby": [],
"ropsten": []
}
},
"assetImages": {},
"completedOnboarding": true,
"currentLocale": "en",
"dismissSeedBackUpReminder": true,
"featureFlags": {
"showIncomingTransactions": true,
"transactionTime": false
"showIncomingTransactions": true
},
"firstTimeFlowType": "create",
"firstTimeFlowType": "import",
"forgottenPassword": false,
"frequentRpcListDetail": [],
"frequentRpcListDetail": [
{
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": {},
"rpcUrl": "http://localhost:8545",
"ticker": "ETH"
}
],
"identities": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
"address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1",
"lastSelected": 1626907346643,
"name": "Account 1"
}
},
"infuraBlocked": false,
"ipfsGateway": "dweb.link",
"knownMethodData": {},
"lostIdentities": {},
"metaMetricsId": null,
"participateInMetaMetrics": false,
"preferences": {
"hideZeroBalanceTokens": false,
"showFiatInTestnets": false,
"useNativeCurrencyAsPrimaryCurrency": true
},
"selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1",
"suggestedTokens": {},
"useBlockie": false,
"useLedgerLive": false,
"useNonceField": false,
"usePhishDetect": true,
"useStaticTokenList": false
},
"TokenListController": {
"tokenList": {},
"tokensChainsCache": {}
},
"TokensController": {
"allTokens": {
"0x539": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": [
{
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"decimals": 4,
"image": null,
"isERC721": false,
"symbol": "TST"
}
]
}
},
"ignoredTokens": [],
"suggestedAssets": [],
"allIgnoredTokens": {},
"tokens": [
{
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"symbol": "TST",
"decimals": 4
"decimals": 4,
"image": null,
"isERC721": false,
"symbol": "TST"
}
],
"useBlockie": false,
"useNonceField": false,
"usePhishDetect": true
]
},
"TransactionController": {
"transactions": {}
},
"config": {},
"firstTimeInfo": {
"date": 1575697234195,
"version": "7.7.0"
"date": 1626907328205,
"version": "9.8.1"
}
},
"meta": {
"version": 40
"version": 63
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -174,24 +174,6 @@ describe('MetaMask Reducers', () => {
});
});
it('updates tokens', () => {
const newTokens = {
address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
decimals: 18,
symbol: 'META',
};
const state = reduceMetamask(
{},
{
type: actionConstants.UPDATE_TOKENS,
newTokens,
},
);
expect(state.tokens).toStrictEqual(newTokens);
});
it('toggles account menu', () => {
const state = reduceMetamask(
{},

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1069,6 +1069,7 @@ describe('Actions', () => {
background.getApi.returns({
addToken: addTokenStub,
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
actions._setBackgroundConnection(background.getApi());
@ -1098,17 +1099,18 @@ describe('Actions', () => {
background.getApi.returns({
addToken: addTokenStub,
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
actions._setBackgroundConnection(background.getApi());
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{
type: 'UPDATE_TOKENS',
newTokens: tokenDetails,
type: 'UPDATE_METAMASK_STATE',
value: baseMockState,
},
{ type: 'HIDE_LOADING_INDICATION' },
];
await store.dispatch(
@ -1121,38 +1123,6 @@ describe('Actions', () => {
expect(store.getActions()).toStrictEqual(expectedActions);
});
it('errors when addToken in background throws', async () => {
const store = mockStore();
const addTokenStub = sinon
.stub()
.callsFake((_, __, ___, ____, cb) => cb(new Error('error')));
background.getApi.returns({
addToken: addTokenStub,
});
actions._setBackgroundConnection(background.getApi());
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' },
];
await expect(
store.dispatch(
actions.addToken({
address: '_',
symbol: '',
decimals: 0,
}),
),
).rejects.toThrow('error');
expect(store.getActions()).toStrictEqual(expectedActions);
});
});
describe('#removeToken', () => {
@ -1167,6 +1137,7 @@ describe('Actions', () => {
background.getApi.returns({
removeToken: removeTokenStub,
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
actions._setBackgroundConnection(background.getApi());
@ -1175,24 +1146,27 @@ describe('Actions', () => {
expect(removeTokenStub.callCount).toStrictEqual(1);
});
it('errors when removeToken in background fails', async () => {
it('should display warning when removeToken in background fails', async () => {
const store = mockStore();
background.getApi.returns({
removeToken: sinon.stub().callsFake((_, cb) => cb(new Error('error'))),
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
actions._setBackgroundConnection(background.getApi());
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' },
{
type: 'UPDATE_METAMASK_STATE',
value: baseMockState,
},
{ type: 'HIDE_LOADING_INDICATION' },
];
await expect(store.dispatch(actions.removeToken())).rejects.toThrow(
'error',
);
await store.dispatch(actions.removeToken());
expect(store.getActions()).toStrictEqual(expectedActions);
});

Loading…
Cancel
Save