mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Integrate TokensController (#11552)
* Integrate controllers/tokensController * address rebase issues * small cleanup * addressing feedback * more feedback
This commit is contained in:
parent
ad7d85b04e
commit
490d3b8d40
@ -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,
|
||||
|
78
app/scripts/migrations/063.js
Normal file
78
app/scripts/migrations/063.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
const version = 63;
|
||||
|
||||
/**
|
||||
* Moves token state from preferences controller to TokensController
|
||||
*/
|
||||
export default {
|
||||
version,
|
||||
async migrate(originalVersionedData) {
|
||||
const versionedData = cloneDeep(originalVersionedData);
|
||||
versionedData.meta.version = version;
|
||||
const state = versionedData.data;
|
||||
const newState = transformState(state);
|
||||
versionedData.data = newState;
|
||||
return versionedData;
|
||||
},
|
||||
};
|
||||
|
||||
function transformState(state) {
|
||||
const accountTokens = state?.PreferencesController?.accountTokens;
|
||||
const accountHiddenTokens = state?.PreferencesController?.accountHiddenTokens;
|
||||
|
||||
const newAllTokens = {};
|
||||
if (accountTokens) {
|
||||
Object.keys(accountTokens).forEach((accountAddress) => {
|
||||
Object.keys(accountTokens[accountAddress]).forEach((chainId) => {
|
||||
const tokensArray = accountTokens[accountAddress][chainId];
|
||||
if (newAllTokens[chainId] === undefined) {
|
||||
newAllTokens[chainId] = { [accountAddress]: tokensArray };
|
||||
} else {
|
||||
newAllTokens[chainId] = {
|
||||
...newAllTokens[chainId],
|
||||
[accountAddress]: tokensArray,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const newAllIgnoredTokens = {};
|
||||
if (accountHiddenTokens) {
|
||||
Object.keys(accountHiddenTokens).forEach((accountAddress) => {
|
||||
Object.keys(accountHiddenTokens[accountAddress]).forEach((chainId) => {
|
||||
const ignoredTokensArray = accountHiddenTokens[accountAddress][chainId];
|
||||
if (newAllIgnoredTokens[chainId] === undefined) {
|
||||
newAllIgnoredTokens[chainId] = {
|
||||
[accountAddress]: ignoredTokensArray,
|
||||
};
|
||||
} else {
|
||||
newAllIgnoredTokens[chainId] = {
|
||||
...newAllIgnoredTokens[chainId],
|
||||
[accountAddress]: ignoredTokensArray,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (state.TokensController) {
|
||||
state.TokensController.allTokens = newAllTokens;
|
||||
state.TokensController.allIgnoredTokens = newAllIgnoredTokens;
|
||||
} else {
|
||||
state.TokensController = {
|
||||
allTokens: newAllTokens,
|
||||
allIgnoredTokens: newAllIgnoredTokens,
|
||||
};
|
||||
}
|
||||
|
||||
delete state?.PreferencesController?.accountHiddenTokens;
|
||||
delete state?.PreferencesController?.accountTokens;
|
||||
delete state?.PreferencesController?.assetImages;
|
||||
delete state?.PreferencesController?.hiddenTokens;
|
||||
delete state?.PreferencesController?.tokens;
|
||||
delete state?.PreferencesController?.suggestedTokens;
|
||||
|
||||
return state;
|
||||
}
|
251
app/scripts/migrations/063.test.js
Normal file
251
app/scripts/migrations/063.test.js
Normal file
@ -0,0 +1,251 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import migration63 from './063';
|
||||
|
||||
describe('migration #63', function () {
|
||||
it('should update the version metadata', async function () {
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 62,
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
const newStorage = await migration63.migrate(oldStorage);
|
||||
assert.deepEqual(newStorage.meta, {
|
||||
version: 63,
|
||||
});
|
||||
});
|
||||
|
||||
it('should move accountTokens data from PreferencesController to TokensController allTokens field and rotate structure from [accountAddress][chainId] to [chainId][accountAddress]', async function () {
|
||||
const oldAccountTokens = {
|
||||
'0x00000000000': {
|
||||
'0x1': [
|
||||
{
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'DAI',
|
||||
},
|
||||
{
|
||||
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'UNI',
|
||||
},
|
||||
],
|
||||
'0x89': [
|
||||
{
|
||||
address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'LINK',
|
||||
},
|
||||
{
|
||||
address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
|
||||
decimals: 6,
|
||||
isERC721: false,
|
||||
symbol: 'USDT',
|
||||
},
|
||||
],
|
||||
},
|
||||
'0x1111111111': {
|
||||
'0x1': [
|
||||
{
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'FAI',
|
||||
},
|
||||
{
|
||||
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'PUNI',
|
||||
},
|
||||
],
|
||||
'0x89': [
|
||||
{
|
||||
address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'SLINK',
|
||||
},
|
||||
{
|
||||
address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
|
||||
decimals: 6,
|
||||
isERC721: false,
|
||||
symbol: 'USDC',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedTokens = {
|
||||
'0x1': {
|
||||
'0x00000000000': [
|
||||
{
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'DAI',
|
||||
},
|
||||
{
|
||||
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'UNI',
|
||||
},
|
||||
],
|
||||
'0x1111111111': [
|
||||
{
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'FAI',
|
||||
},
|
||||
{
|
||||
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'PUNI',
|
||||
},
|
||||
],
|
||||
},
|
||||
'0x89': {
|
||||
'0x00000000000': [
|
||||
{
|
||||
address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'LINK',
|
||||
},
|
||||
{
|
||||
address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
|
||||
decimals: 6,
|
||||
isERC721: false,
|
||||
symbol: 'USDT',
|
||||
},
|
||||
],
|
||||
'0x1111111111': [
|
||||
{
|
||||
address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab',
|
||||
decimals: 18,
|
||||
isERC721: false,
|
||||
symbol: 'SLINK',
|
||||
},
|
||||
{
|
||||
address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
|
||||
decimals: 6,
|
||||
isERC721: false,
|
||||
symbol: 'USDC',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
PreferencesController: {
|
||||
completedOnboarding: true,
|
||||
dismissSeedBackUpReminder: false,
|
||||
accountTokens: oldAccountTokens,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration63.migrate(oldStorage);
|
||||
assert.deepStrictEqual(newStorage.data, {
|
||||
TokensController: {
|
||||
allTokens: expectedTokens,
|
||||
allIgnoredTokens: {},
|
||||
},
|
||||
PreferencesController: {
|
||||
completedOnboarding: true,
|
||||
dismissSeedBackUpReminder: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should move accountHiddenTokens data from PreferencesController to TokensController allIgnoredTokens field and rotate structure from [accountAddress][chainId] to [chainId][accountAddress]', async function () {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
PreferencesController: {
|
||||
completedOnboarding: true,
|
||||
dismissSeedBackUpReminder: false,
|
||||
accountTokens: {},
|
||||
accountHiddenTokens: {
|
||||
'0x1111111111': {
|
||||
'0x1': ['0x000000000000'],
|
||||
'0x89': ['0x11111111111'],
|
||||
},
|
||||
'0x222222': {
|
||||
'0x4': ['0x000011112222'],
|
||||
},
|
||||
'0x333333': {
|
||||
'0x5': ['0x000022223333'],
|
||||
'0x1': ['0x000033333344'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration63.migrate(oldStorage);
|
||||
assert.deepStrictEqual(newStorage.data, {
|
||||
TokensController: {
|
||||
allTokens: {},
|
||||
allIgnoredTokens: {
|
||||
'0x1': {
|
||||
'0x1111111111': ['0x000000000000'],
|
||||
'0x333333': ['0x000033333344'],
|
||||
},
|
||||
'0x89': {
|
||||
'0x1111111111': ['0x11111111111'],
|
||||
},
|
||||
'0x4': {
|
||||
'0x222222': ['0x000011112222'],
|
||||
},
|
||||
'0x5': {
|
||||
'0x333333': ['0x000022223333'],
|
||||
},
|
||||
},
|
||||
},
|
||||
PreferencesController: {
|
||||
completedOnboarding: true,
|
||||
dismissSeedBackUpReminder: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should should remove all token related state from the preferences controller', async function () {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
PreferencesController: {
|
||||
completedOnboarding: true,
|
||||
dismissSeedBackUpReminder: false,
|
||||
accountTokens: {},
|
||||
accountHiddenTokens: {},
|
||||
tokens: {},
|
||||
hiddenTokens: {},
|
||||
assetImages: {},
|
||||
suggestedTokens: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration63.migrate(oldStorage);
|
||||
assert.deepStrictEqual(newStorage.data, {
|
||||
PreferencesController: {
|
||||
completedOnboarding: true,
|
||||
dismissSeedBackUpReminder: false,
|
||||
},
|
||||
TokensController: {
|
||||
allTokens: {},
|
||||
allIgnoredTokens: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -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": {
|
||||
"nickname": "Localhost 8545",
|
||||
"rpcUrl": "http://localhost:8545",
|
||||
"chainId": "0x539",
|
||||
"nickname": "Localhost 8545",
|
||||
"rpcPrefs": {},
|
||||
"rpcUrl": "http://localhost:8545",
|
||||
"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,27 +1291,35 @@ 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 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());
|
||||
};
|
||||
}
|
||||
|
||||
@ -1328,13 +1329,6 @@ export function addKnownMethodData(fourBytePrefix, methodData) {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTokens(newTokens) {
|
||||
return {
|
||||
type: actionConstants.UPDATE_TOKENS,
|
||||
newTokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearPendingTokens() {
|
||||
return {
|
||||
type: actionConstants.CLEAR_PENDING_TOKENS,
|
||||
@ -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…
Reference in New Issue
Block a user