1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Integrating the TokenListController to Extension (#11398)

This commit is contained in:
Niranjana Binoy 2021-09-09 16:56:27 -04:00 committed by GitHub
parent 5cf3e19910
commit eb987a47b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1160 additions and 240 deletions

View File

@ -1,10 +1,9 @@
import Web3 from 'web3'; import Web3 from 'web3';
import contracts from '@metamask/contract-metadata';
import { warn } from 'loglevel'; import { warn } from 'loglevel';
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'; import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
import { MINUTE } from '../../../shared/constants/time'; import { MINUTE } from '../../../shared/constants/time';
import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util';
// By default, poll every 3 minutes // By default, poll every 3 minutes
const DEFAULT_INTERVAL = MINUTE * 3; const DEFAULT_INTERVAL = MINUTE * 3;
@ -24,57 +23,13 @@ export default class DetectTokensController {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList,
} = {}) { } = {}) {
this.preferences = preferences; this.preferences = preferences;
this.interval = interval; this.interval = interval;
this.network = network; this.network = network;
this.keyringMemStore = keyringMemStore; this.keyringMemStore = keyringMemStore;
} this.tokenList = tokenList;
/**
* For each token in @metamask/contract-metadata, find check selectedAddress balance.
*/
async detectNewTokens() {
if (!this.isActive) {
return;
}
if (this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID) {
return;
}
const tokensToDetect = [];
this.web3.setProvider(this._network._provider);
for (const contractAddress in contracts) {
if (
contracts[contractAddress].erc20 &&
!this.tokenAddresses.includes(contractAddress.toLowerCase()) &&
!this.hiddenTokens.includes(contractAddress.toLowerCase())
) {
tokensToDetect.push(contractAddress);
}
}
let result;
try {
result = await this._getTokenBalances(tokensToDetect);
} catch (error) {
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
error,
);
return;
}
tokensToDetect.forEach((tokenAddress, index) => {
const balance = result[index];
if (balance && !balance.isZero()) {
this._preferences.addToken(
tokenAddress,
contracts[tokenAddress].symbol,
contracts[tokenAddress].decimals,
);
}
});
} }
async _getTokenBalances(tokens) { async _getTokenBalances(tokens) {
@ -91,6 +46,63 @@ export default class DetectTokensController {
}); });
} }
/**
* For each token in the tokenlist provided by the TokenListController, check selectedAddress balance.
*/
async detectNewTokens() {
if (!this.isActive) {
return;
}
const { tokenList } = this._tokenList.state;
if (Object.keys(tokenList).length === 0) {
return;
}
const tokensToDetect = [];
this.web3.setProvider(this._network._provider);
for (const tokenAddress in tokenList) {
if (
!this.tokenAddresses.find((address) =>
isEqualCaseInsensitive(address, tokenAddress),
) &&
!this.hiddenTokens.find((address) =>
isEqualCaseInsensitive(address, tokenAddress),
)
) {
tokensToDetect.push(tokenAddress);
}
}
const sliceOfTokensToDetect = [
tokensToDetect.slice(0, 1000),
tokensToDetect.slice(1000, tokensToDetect.length - 1),
];
for (const tokensSlice of sliceOfTokensToDetect) {
let result;
try {
result = await this._getTokenBalances(tokensSlice);
} catch (error) {
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
error,
);
return;
}
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,
);
}
}),
);
}
}
/** /**
* Restart token detection polling period and call detectNewTokens * Restart token detection polling period and call detectNewTokens
* in case of address change or user session initialization. * in case of address change or user session initialization.
@ -138,9 +150,13 @@ export default class DetectTokensController {
}); });
this.hiddenTokens = hiddenTokens; this.hiddenTokens = hiddenTokens;
}); });
preferences.store.subscribe(({ selectedAddress }) => { preferences.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (this.selectedAddress !== selectedAddress) { if (
this.selectedAddress !== selectedAddress ||
this.useTokenDetection !== useTokenDetection
) {
this.selectedAddress = selectedAddress; this.selectedAddress = selectedAddress;
this.useTokenDetection = useTokenDetection;
this.restartTokenDetection(); this.restartTokenDetection();
} }
}); });
@ -176,6 +192,16 @@ export default class DetectTokensController {
}); });
} }
/**
* @type {Object}
*/
set tokenList(tokenList) {
if (!tokenList) {
return;
}
this._tokenList = tokenList;
}
/** /**
* Internal isActive state * Internal isActive state
* @type {Object} * @type {Object}

View File

@ -1,15 +1,19 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import nock from 'nock';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import contracts from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import {
ControllerMessenger,
TokenListController,
} from '@metamask/controllers';
import { MAINNET, ROPSTEN } from '../../../shared/constants/network'; import { MAINNET, ROPSTEN } from '../../../shared/constants/network';
import DetectTokensController from './detect-tokens'; import DetectTokensController from './detect-tokens';
import NetworkController from './network'; import NetworkController from './network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
describe('DetectTokensController', function () { describe('DetectTokensController', function () {
let tokenListController;
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
let keyringMemStore, network, preferences, provider; let keyringMemStore, network, preferences, provider;
@ -36,6 +40,87 @@ describe('DetectTokensController', function () {
sandbox sandbox
.stub(preferences, '_detectIsERC721') .stub(preferences, '_detectIsERC721')
.returns(Promise.resolve(false)); .returns(Promise.resolve(false));
nock('https://token-api.metaswap.codefi.network')
.get(`/tokens/1`)
.reply(200, [
{
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
symbol: 'SNX',
decimals: 18,
occurrences: 11,
aggregators: [
'paraswap',
'pmm',
'airswapLight',
'zeroEx',
'bancor',
'coinGecko',
'zapper',
'kleros',
'zerion',
'cmc',
'oneInch',
],
name: 'Synthetix',
iconUrl: 'https://airswap-token-images.s3.amazonaws.com/SNX.png',
},
{
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
occurrences: 11,
aggregators: [
'paraswap',
'pmm',
'airswapLight',
'zeroEx',
'bancor',
'coinGecko',
'zapper',
'kleros',
'zerion',
'cmc',
'oneInch',
],
name: 'Chainlink',
iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png',
},
{
address: '0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c',
symbol: 'BNT',
decimals: 18,
occurrences: 11,
aggregators: [
'paraswap',
'pmm',
'airswapLight',
'zeroEx',
'bancor',
'coinGecko',
'zapper',
'kleros',
'zerion',
'cmc',
'oneInch',
],
name: 'Bancor',
iconUrl: 'https://s3.amazonaws.com/airswap-token-images/BNT.png',
},
])
.get(`/tokens/3`)
.reply(200, { error: 'ChainId 3 is not supported' })
.persist();
const tokenListMessenger = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
tokenListController = new TokenListController({
chainId: '1',
useStaticTokenList: false,
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessenger,
});
await tokenListController.start();
}); });
after(function () { after(function () {
@ -56,6 +141,7 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -75,10 +161,22 @@ describe('DetectTokensController', function () {
it('should not check tokens while on test network', async function () { it('should not check tokens while on test network', async function () {
sandbox.useFakeTimers(); sandbox.useFakeTimers();
network.setProviderType(ROPSTEN); network.setProviderType(ROPSTEN);
const tokenListMessengerRopsten = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
tokenListController = new TokenListController({
chainId: '3',
useStaticTokenList: false,
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessengerRopsten,
});
await tokenListController.start();
const controller = new DetectTokensController({ const controller = new DetectTokensController({
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -96,17 +194,16 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
const contractAddresses = Object.keys(contracts); const { tokenList } = tokenListController.state;
const erc20ContractAddresses = contractAddresses.filter( const erc20ContractAddresses = Object.keys(tokenList);
(contractAddress) => contracts[contractAddress].erc20 === true,
);
const existingTokenAddress = erc20ContractAddresses[0]; const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = contracts[existingTokenAddress]; const existingToken = tokenList[existingTokenAddress];
await preferences.addToken( await preferences.addToken(
existingTokenAddress, existingTokenAddress,
existingToken.symbol, existingToken.symbol,
@ -144,17 +241,16 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
const contractAddresses = Object.keys(contracts); const { tokenList } = tokenListController.state;
const erc20ContractAddresses = contractAddresses.filter( const erc20ContractAddresses = Object.keys(tokenList);
(contractAddress) => contracts[contractAddress].erc20 === true,
);
const existingTokenAddress = erc20ContractAddresses[0]; const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = contracts[existingTokenAddress]; const existingToken = tokenList[existingTokenAddress];
await preferences.addToken( await preferences.addToken(
existingTokenAddress, existingTokenAddress,
existingToken.symbol, existingToken.symbol,
@ -162,16 +258,16 @@ describe('DetectTokensController', function () {
); );
const tokenAddressToAdd = erc20ContractAddresses[1]; const tokenAddressToAdd = erc20ContractAddresses[1];
const tokenToAdd = contracts[tokenAddressToAdd]; const tokenToAdd = tokenList[tokenAddressToAdd];
const contractAddresssesToDetect = contractAddresses.filter( const contractAddressesToDetect = erc20ContractAddresses.filter(
(address) => address !== existingTokenAddress, (address) => address !== existingTokenAddress,
); );
const indexOfTokenToAdd = contractAddresssesToDetect.indexOf( const indexOfTokenToAdd = contractAddressesToDetect.indexOf(
tokenAddressToAdd, tokenAddressToAdd,
); );
const balances = new Array(contractAddresssesToDetect.length); const balances = new Array(contractAddressesToDetect.length);
balances[indexOfTokenToAdd] = new BigNumber(10); balances[indexOfTokenToAdd] = new BigNumber(10);
sandbox sandbox
@ -203,17 +299,16 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
const contractAddresses = Object.keys(contracts); const { tokenList } = tokenListController.state;
const erc20ContractAddresses = contractAddresses.filter( const erc20ContractAddresses = Object.keys(tokenList);
(contractAddress) => contracts[contractAddress].erc20 === true,
);
const existingTokenAddress = erc20ContractAddresses[0]; const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = contracts[existingTokenAddress]; const existingToken = tokenList[existingTokenAddress];
await preferences.addToken( await preferences.addToken(
existingTokenAddress, existingTokenAddress,
existingToken.symbol, existingToken.symbol,
@ -221,16 +316,16 @@ describe('DetectTokensController', function () {
); );
const tokenAddressToAdd = erc20ContractAddresses[1]; const tokenAddressToAdd = erc20ContractAddresses[1];
const tokenToAdd = contracts[tokenAddressToAdd]; const tokenToAdd = tokenList[tokenAddressToAdd];
const contractAddresssesToDetect = contractAddresses.filter( const contractAddressesToDetect = erc20ContractAddresses.filter(
(address) => address !== existingTokenAddress, (address) => address !== existingTokenAddress,
); );
const indexOfTokenToAdd = contractAddresssesToDetect.indexOf( const indexOfTokenToAdd = contractAddressesToDetect.indexOf(
tokenAddressToAdd, tokenAddressToAdd,
); );
const balances = new Array(contractAddresssesToDetect.length); const balances = new Array(contractAddressesToDetect.length);
balances[indexOfTokenToAdd] = new BigNumber(10); balances[indexOfTokenToAdd] = new BigNumber(10);
sandbox sandbox
@ -261,6 +356,7 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = true; controller.isUnlocked = true;
@ -277,6 +373,7 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.selectedAddress = '0x0'; controller.selectedAddress = '0x0';
@ -292,6 +389,7 @@ describe('DetectTokensController', function () {
preferences, preferences,
network, network,
keyringMemStore, keyringMemStore,
tokenList: tokenListController,
}); });
controller.isOpen = true; controller.isOpen = true;
controller.isUnlocked = false; controller.isUnlocked = false;

View File

@ -51,7 +51,10 @@ export default class PreferencesController {
useNonceField: false, useNonceField: false,
usePhishDetect: true, usePhishDetect: true,
dismissSeedBackUpReminder: false, dismissSeedBackUpReminder: false,
useStaticTokenList: false,
// set to true means the dynamic list from the API is being used
// set to false will be using the static list from contract-metadata
useTokenDetection: true,
// WARNING: Do not use feature flags for security-sensitive things. // WARNING: Do not use feature flags for security-sensitive things.
// Feature flag toggling is available in the global namespace // Feature flag toggling is available in the global namespace
@ -140,13 +143,13 @@ export default class PreferencesController {
} }
/** /**
* Setter for the `useStaticTokenList` property * Setter for the `useTokenDetection` property
* *
* @param {boolean} val - Whether or not the user prefers to use the static token list or dynamic token list from the API * @param {boolean} val - Whether or not the user prefers to use the static token list or dynamic token list from the API
* *
*/ */
setUseStaticTokenList(val) { setUseTokenDetection(val) {
this.store.updateState({ useStaticTokenList: val }); this.store.updateState({ useTokenDetection: val });
} }
/** /**

View File

@ -869,22 +869,22 @@ describe('preferences controller', function () {
); );
}); });
}); });
describe('setUseStaticTokenList', function () { describe('setUseTokenDetection', function () {
it('should default to false', function () { it('should default to true', function () {
const state = preferencesController.store.getState(); const state = preferencesController.store.getState();
assert.equal(state.useStaticTokenList, false); assert.equal(state.useTokenDetection, true);
}); });
it('should set the useStaticTokenList property in state', function () { it('should set the useTokenDetection property in state', function () {
assert.equal( assert.equal(
preferencesController.store.getState().useStaticTokenList, preferencesController.store.getState().useTokenDetection,
false,
);
preferencesController.setUseStaticTokenList(true);
assert.equal(
preferencesController.store.getState().useStaticTokenList,
true, true,
); );
preferencesController.setUseTokenDetection(false);
assert.equal(
preferencesController.store.getState().useTokenDetection,
false,
);
}); });
}); });
}); });

View File

@ -16,7 +16,6 @@ import TrezorKeyring from 'eth-trezor-keyring';
import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring'; import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring';
import EthQuery from 'eth-query'; import EthQuery from 'eth-query';
import nanoid from 'nanoid'; import nanoid from 'nanoid';
import contractMap from '@metamask/contract-metadata';
import { import {
AddressBookController, AddressBookController,
ApprovalController, ApprovalController,
@ -238,8 +237,8 @@ export default class MetamaskController extends EventEmitter {
}); });
this.tokenListController = new TokenListController({ this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()), chainId: hexToDecimal(this.networkController.getCurrentChainId()),
useStaticTokenList: this.preferencesController.store.getState() useStaticTokenList: !this.preferencesController.store.getState()
.useStaticTokenList, .useTokenDetection,
onNetworkStateChange: (cb) => onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => { this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = { const modifiedNetworkState = {
@ -251,11 +250,17 @@ export default class MetamaskController extends EventEmitter {
}; };
return cb(modifiedNetworkState); return cb(modifiedNetworkState);
}), }),
onPreferencesStateChange: this.preferencesController.store.subscribe.bind( onPreferencesStateChange: (cb) =>
this.preferencesController.store, this.preferencesController.store.subscribe((preferencesState) => {
), const modifiedPreferencesState = {
...preferencesState,
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
};
return cb(modifiedPreferencesState);
}),
messenger: tokenListMessenger, messenger: tokenListMessenger,
state: initState.tokenListController, state: initState.TokenListController,
}); });
this.phishingController = new PhishingController(); this.phishingController = new PhishingController();
@ -372,6 +377,7 @@ export default class MetamaskController extends EventEmitter {
preferences: this.preferencesController, preferences: this.preferencesController,
network: this.networkController, network: this.networkController,
keyringMemStore: this.keyringController.memStore, keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
}); });
this.addressBookController = new AddressBookController( this.addressBookController = new AddressBookController(
@ -775,8 +781,8 @@ export default class MetamaskController extends EventEmitter {
setUseBlockie: this.setUseBlockie.bind(this), setUseBlockie: this.setUseBlockie.bind(this),
setUseNonceField: this.setUseNonceField.bind(this), setUseNonceField: this.setUseNonceField.bind(this),
setUsePhishDetect: this.setUsePhishDetect.bind(this), setUsePhishDetect: this.setUsePhishDetect.bind(this),
setUseStaticTokenList: nodeify( setUseTokenDetection: nodeify(
this.preferencesController.setUseStaticTokenList, this.preferencesController.setUseTokenDetection,
this.preferencesController, this.preferencesController,
), ),
setIpfsGateway: this.setIpfsGateway.bind(this), setIpfsGateway: this.setIpfsGateway.bind(this),
@ -1297,28 +1303,8 @@ export default class MetamaskController extends EventEmitter {
tokens, tokens,
} = this.preferencesController.store.getState(); } = this.preferencesController.store.getState();
// Filter ERC20 tokens
const filteredAccountTokens = {};
Object.keys(accountTokens).forEach((address) => {
const checksummedAddress = toChecksumHexAddress(address);
filteredAccountTokens[checksummedAddress] = {};
Object.keys(accountTokens[address]).forEach((chainId) => {
filteredAccountTokens[checksummedAddress][chainId] =
chainId === MAINNET_CHAIN_ID
? accountTokens[address][chainId].filter(
({ address: tokenAddress }) => {
const checksumAddress = toChecksumHexAddress(tokenAddress);
return contractMap[checksumAddress]
? contractMap[checksumAddress].erc20
: true;
},
)
: accountTokens[address][chainId];
});
});
const preferences = { const preferences = {
accountTokens: filteredAccountTokens, accountTokens,
currentLocale, currentLocale,
frequentRpcList, frequentRpcList,
identities, identities,

View File

@ -25,5 +25,49 @@
"fallback_to_v1": false "fallback_to_v1": false
} }
} }
},
"tokenList": {
"0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": {
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x0d8775f648430679a709e98d2b0cb6250d2887ef": {
"address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
"symbol": "BAT",
"decimals": 18,
"name": "Basic Attention Tok",
"iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
}
} }
} }

View File

@ -154,6 +154,53 @@
"editingTransactionId": null, "editingTransactionId": null,
"toNickname": "" "toNickname": ""
}, },
"useTokenDetection": true,
"tokenList": {
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
"symbol": "WBTC",
"decimals": 8,
"name": "Wrapped Bitcoin",
"iconUrl": "https://s3.amazonaws.com/airswap-token-images/WBTC.png",
"aggregators": [
"airswapLight",
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 12
},
"0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e": {
"address": "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e",
"symbol": "YFI",
"decimals": 18,
"name": "yearn.finance",
"iconUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e/logo.png",
"aggregators": [
"airswapLight",
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 12
}
},
"currentNetworkTxList": [ "currentNetworkTxList": [
{ {
"id": 3387511061307736, "id": 3387511061307736,

View File

@ -88,6 +88,361 @@
} }
] ]
}, },
"TokenListController": {
"tokenList": {
"0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": {
"address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd",
"symbol": "LRC",
"decimals": 18,
"name": "Loopring",
"iconUrl": "https://airswap-token-images.s3.amazonaws.com/LRC.png",
"aggregators": [
"airswapLight",
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 12
},
"0x04fa0d235c4abf4bcf4787af4cf447de572ef828": {
"address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828",
"symbol": "UMA",
"decimals": 18,
"name": "UMA",
"iconUrl": "https://assets.coingecko.com/coins/images/10951/thumb/UMA.png?1586307916",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": {
"address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"symbol": "SUSHI",
"decimals": 18,
"name": "SushiSwap",
"iconUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xd533a949740bb3306d119cc777fa900ba034cd52": {
"address": "0xd533a949740bb3306d119cc777fa900ba034cd52",
"symbol": "CRV",
"decimals": 18,
"name": "Curve DAO Token",
"iconUrl": "https://assets.coingecko.com/coins/images/12124/thumb/Curve.png?1597369484",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xc00e94cb662c3520282e6f5717214004a7f26888": {
"address": "0xc00e94cb662c3520282e6f5717214004a7f26888",
"symbol": "COMP",
"decimals": 18,
"name": "Compound",
"iconUrl": "https://assets.coingecko.com/coins/images/10775/thumb/COMP.png?1592625425",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xba100000625a3754423978a60c9317c58a424e3d": {
"address": "0xba100000625a3754423978a60c9317c58a424e3d",
"symbol": "BAL",
"decimals": 18,
"name": "Balancer",
"iconUrl": "https://assets.coingecko.com/coins/images/11683/thumb/Balancer.png?1592792958",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": {
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x0d8775f648430679a709e98d2b0cb6250d2887ef": {
"address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
"symbol": "BAT",
"decimals": 18,
"name": "Basic Attention Tok",
"iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
}
},
"tokensChainsCache": {
"1": {
"timestamp": 1628769574961,
"data": [
{
"address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd",
"symbol": "LRC",
"decimals": 18,
"name": "Loopring",
"iconUrl": "https://airswap-token-images.s3.amazonaws.com/LRC.png",
"aggregators": [
"airswapLight",
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 12
},
{
"address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828",
"symbol": "UMA",
"decimals": 18,
"name": "UMA",
"iconUrl": "https://assets.coingecko.com/coins/images/10951/thumb/UMA.png?1586307916",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"symbol": "SUSHI",
"decimals": 18,
"name": "SushiSwap",
"iconUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xd533a949740bb3306d119cc777fa900ba034cd52",
"symbol": "CRV",
"decimals": 18,
"name": "Curve DAO Token",
"iconUrl": "https://assets.coingecko.com/coins/images/12124/thumb/Curve.png?1597369484",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xc00e94cb662c3520282e6f5717214004a7f26888",
"symbol": "COMP",
"decimals": 18,
"name": "Compound",
"iconUrl": "https://assets.coingecko.com/coins/images/10775/thumb/COMP.png?1592625425",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xba100000625a3754423978a60c9317c58a424e3d",
"symbol": "BAL",
"decimals": 18,
"name": "Balancer",
"iconUrl": "https://assets.coingecko.com/coins/images/11683/thumb/Balancer.png?1592792958",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
"symbol": "BAT",
"decimals": 18,
"name": "Basic Attention Tok",
"iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
}
]
},
"3": {
"timestamp": 1628769543620
},
"1337": {
"timestamp": 1628769513476
}
}
},
"PreferencesController": { "PreferencesController": {
"accountTokens": { "accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
@ -123,7 +478,8 @@
"tokens": [], "tokens": [],
"useBlockie": false, "useBlockie": false,
"useNonceField": false, "useNonceField": false,
"usePhishDetect": true "usePhishDetect": true,
"useTokenDetection": true
}, },
"config": {}, "config": {},
"firstTimeInfo": { "firstTimeInfo": {

View File

@ -1,5 +1,5 @@
const { strict: assert } = require('assert'); const { strict: assert } = require('assert');
const { withFixtures, xxLargeDelayMs } = require('../helpers'); const { withFixtures, xxLargeDelayMs, xLargeDelayMs } = require('../helpers');
describe('Permissions', function () { describe('Permissions', function () {
it('sets permissions and connect to Dapp', async function () { it('sets permissions and connect to Dapp', async function () {
@ -62,7 +62,7 @@ describe('Permissions', function () {
text: 'Connected sites', text: 'Connected sites',
tag: 'h2', tag: 'h2',
}); });
await driver.delay(xLargeDelayMs);
const domains = await driver.findClickableElements( const domains = await driver.findClickableElements(
'.connected-sites-list__domain-name', '.connected-sites-list__domain-name',
); );

View File

@ -52,6 +52,12 @@ async function setupFetchMocking(driver) {
if (url.match(/featureFlags$/u)) { if (url.match(/featureFlags$/u)) {
return { json: async () => clone(mockResponses.swaps.featureFlags) }; return { json: async () => clone(mockResponses.swaps.featureFlags) };
} }
} else if (
url.match(/^https:\/\/(token-api\.airswap-prod\.codefi\.network)/u)
) {
if (url.match(/tokens\/1337$/u)) {
return { json: async () => clone(mockResponses.tokenList) };
}
} }
return window.origFetch(...args); return window.origFetch(...args);
}; };

View File

@ -222,6 +222,76 @@ export const createSwapsMockStore = () => {
swapsFeatureIsLive: false, swapsFeatureIsLive: false,
useNewSwapsApi: false, useNewSwapsApi: false,
}, },
useTokenDetection: true,
tokenList: {
'0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': {
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
symbol: 'UNI',
decimals: 18,
name: 'Uniswap',
iconUrl:
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
'0x514910771af9ca656af840dff83e8264ecf986ca': {
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
name: 'Chainlink',
iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
'0x6b3595068778dd592e39a122f4f5a5cf09c90fe2': {
address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
symbol: 'SUSHI',
decimals: 18,
name: 'SushiSwap',
iconUrl:
'https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688',
aggregators: [
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 11,
},
},
}, },
appState: { appState: {
modal: { modal: {

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import contractMap from '@metamask/contract-metadata';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import Jazzicon from '../jazzicon'; import Jazzicon from '../jazzicon';
@ -23,6 +22,8 @@ export default class Identicon extends PureComponent {
useBlockie: PropTypes.bool, useBlockie: PropTypes.bool,
alt: PropTypes.string, alt: PropTypes.string,
imageBorder: PropTypes.bool, imageBorder: PropTypes.bool,
useTokenDetection: PropTypes.bool,
tokenList: PropTypes.object,
}; };
static defaultProps = { static defaultProps = {
@ -33,6 +34,7 @@ export default class Identicon extends PureComponent {
image: undefined, image: undefined,
useBlockie: false, useBlockie: false,
alt: '', alt: '',
tokenList: {},
}; };
renderImage() { renderImage() {
@ -51,8 +53,14 @@ export default class Identicon extends PureComponent {
} }
renderJazzicon() { renderJazzicon() {
const { address, className, diameter, alt } = this.props; const {
address,
className,
diameter,
alt,
useTokenDetection,
tokenList,
} = this.props;
return ( return (
<Jazzicon <Jazzicon
address={address} address={address}
@ -60,6 +68,8 @@ export default class Identicon extends PureComponent {
className={classnames('identicon', className)} className={classnames('identicon', className)}
style={getStyles(diameter)} style={getStyles(diameter)}
alt={alt} alt={alt}
useTokenDetection={useTokenDetection}
tokenList={tokenList}
/> />
); );
} }
@ -78,16 +88,25 @@ export default class Identicon extends PureComponent {
} }
render() { render() {
const { address, image, useBlockie, addBorder, diameter } = this.props; const {
address,
image,
useBlockie,
addBorder,
diameter,
useTokenDetection,
tokenList,
} = this.props;
if (image) { if (image) {
return this.renderImage(); return this.renderImage();
} }
if (address) { if (address) {
const checksummedAddress = toChecksumHexAddress(address); // token from dynamic api list is fetched when useTokenDetection is true
const tokenAddress = useTokenDetection
if (checksummedAddress && contractMap[checksummedAddress]?.logo) { ? address
: toChecksumHexAddress(address);
if (tokenAddress && tokenList[tokenAddress]?.iconUrl) {
return this.renderJazzicon(); return this.renderJazzicon();
} }

View File

@ -3,11 +3,13 @@ import Identicon from './identicon.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
metamask: { useBlockie }, metamask: { useBlockie, useTokenDetection, tokenList },
} = state; } = state;
return { return {
useBlockie, useBlockie,
useTokenDetection,
tokenList,
}; };
}; };

View File

@ -15,6 +15,8 @@ export default class Jazzicon extends PureComponent {
className: PropTypes.string, className: PropTypes.string,
diameter: PropTypes.number, diameter: PropTypes.number,
style: PropTypes.object, style: PropTypes.object,
useTokenDetection: PropTypes.bool,
tokenList: PropTypes.object,
}; };
static defaultProps = { static defaultProps = {
@ -46,8 +48,13 @@ export default class Jazzicon extends PureComponent {
} }
appendJazzicon() { appendJazzicon() {
const { address, diameter } = this.props; const { address, diameter, useTokenDetection, tokenList } = this.props;
const image = iconFactory.iconForAddress(address, diameter); const image = iconFactory.iconForAddress(
address,
diameter,
useTokenDetection,
tokenList,
);
this.container.current.appendChild(image); this.container.current.appendChild(image);
} }

View File

@ -1,8 +1,7 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import abi from 'human-standard-token-abi'; import abi from 'human-standard-token-abi';
import contractMap from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util'; import { addHexPrefix } from 'ethereumjs-util';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { import {
conversionGreaterThan, conversionGreaterThan,
@ -39,6 +38,8 @@ import {
getTargetAccount, getTargetAccount,
getIsNonStandardEthChain, getIsNonStandardEthChain,
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
getUseTokenDetection,
getTokenList,
} from '../../selectors'; } from '../../selectors';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
@ -71,6 +72,7 @@ import {
isDefaultMetaMaskChain, isDefaultMetaMaskChain,
isOriginContractAddress, isOriginContractAddress,
isValidDomainName, isValidDomainName,
isEqualCaseInsensitive,
} from '../../helpers/utils/util'; } from '../../helpers/utils/util';
import { import {
getGasEstimateType, getGasEstimateType,
@ -517,6 +519,8 @@ export const initializeSendState = createAsyncThunk(
gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)), gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)),
gasEstimatePollToken, gasEstimatePollToken,
eip1559support, eip1559support,
useTokenDetection: getUseTokenDetection(state),
tokenAddressList: Object.keys(getTokenList(state)),
}; };
}, },
); );
@ -986,7 +990,7 @@ const slice = createSlice({
recipient.warning = null; recipient.warning = null;
} else { } else {
const isSendingToken = asset.type === ASSET_TYPES.TOKEN; const isSendingToken = asset.type === ASSET_TYPES.TOKEN;
const { chainId, tokens } = action.payload; const { chainId, tokens, tokenAddressList } = action.payload;
if ( if (
isBurnAddress(recipient.userInput) || isBurnAddress(recipient.userInput) ||
(!isValidHexAddress(recipient.userInput, { (!isValidHexAddress(recipient.userInput, {
@ -1005,11 +1009,12 @@ const slice = createSlice({
} else { } else {
recipient.error = null; recipient.error = null;
} }
if ( if (
isSendingToken && isSendingToken &&
isValidHexAddress(recipient.userInput) && isValidHexAddress(recipient.userInput) &&
(toChecksumAddress(recipient.userInput) in contractMap || (tokenAddressList.find((address) =>
isEqualCaseInsensitive(address, recipient.userInput),
) ||
checkExistingAddresses(recipient.userInput, tokens)) checkExistingAddresses(recipient.userInput, tokens))
) { ) {
recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING; recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING;
@ -1210,6 +1215,8 @@ const slice = createSlice({
payload: { payload: {
chainId: action.payload.chainId, chainId: action.payload.chainId,
tokens: action.payload.tokens, tokens: action.payload.tokens,
useTokenDetection: action.payload.useTokenDetection,
tokenAddressList: action.payload.tokenAddressList,
}, },
}); });
} }
@ -1395,7 +1402,14 @@ export function updateRecipientUserInput(userInput) {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
const tokens = getTokens(state); const tokens = getTokens(state);
debouncedValidateRecipientUserInput(dispatch, { chainId, tokens }); const useTokenDetection = getUseTokenDetection(state);
const tokenAddressList = Object.keys(getTokenList(state));
debouncedValidateRecipientUserInput(dispatch, {
chainId,
tokens,
useTokenDetection,
tokenAddressList,
});
}; };
} }

View File

@ -628,6 +628,8 @@ describe('Send Slice', () => {
payload: { payload: {
chainId: '', chainId: '',
tokens: [], tokens: [],
useTokenDetection: true,
tokenAddressList: [],
}, },
}; };
@ -649,6 +651,8 @@ describe('Send Slice', () => {
payload: { payload: {
chainId: '0x55', chainId: '0x55',
tokens: [], tokens: [],
useTokenDetection: true,
tokenAddressList: [],
}, },
}; };
@ -671,6 +675,8 @@ describe('Send Slice', () => {
payload: { payload: {
chainId: '', chainId: '',
tokens: [], tokens: [],
useTokenDetection: true,
tokenAddressList: [],
}, },
}; };
@ -698,6 +704,8 @@ describe('Send Slice', () => {
payload: { payload: {
chainId: '0x4', chainId: '0x4',
tokens: [], tokens: [],
useTokenDetection: true,
tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'],
}, },
}; };
@ -1111,6 +1119,32 @@ describe('Send Slice', () => {
provider: { provider: {
chainId: '0x4', chainId: '0x4',
}, },
useTokenDetection: true,
tokenList: {
0x514910771af9ca656af840dff83e8264ecf986ca: {
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
name: 'Chainlink',
iconUrl:
'https://s3.amazonaws.com/airswap-token-images/LINK.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
},
}, },
send: initialState, send: initialState,
gas: { gas: {
@ -1484,6 +1518,31 @@ describe('Send Slice', () => {
chainId: '', chainId: '',
}, },
tokens: [], tokens: [],
useTokenDetection: true,
tokenList: {
'0x514910771af9ca656af840dff83e8264ecf986ca': {
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
name: 'Chainlink',
iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
},
}, },
}; };
@ -1512,6 +1571,8 @@ describe('Send Slice', () => {
expect(store.getActions()[1].payload).toStrictEqual({ expect(store.getActions()[1].payload).toStrictEqual({
chainId: '', chainId: '',
tokens: [], tokens: [],
useTokenDetection: true,
tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'],
}); });
}); });
}); });
@ -1736,6 +1797,32 @@ describe('Send Slice', () => {
chainId: '', chainId: '',
}, },
tokens: [], tokens: [],
useTokenDetection: true,
tokenList: {
0x514910771af9ca656af840dff83e8264ecf986ca: {
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
name: 'Chainlink',
iconUrl:
'https://s3.amazonaws.com/airswap-token-images/LINK.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
},
}, },
send: { send: {
asset: { asset: {

View File

@ -1,4 +1,3 @@
import contractMap from '@metamask/contract-metadata';
import { import {
isValidHexAddress, isValidHexAddress,
toChecksumHexAddress, toChecksumHexAddress,
@ -18,11 +17,18 @@ function IconFactory(jazzicon) {
this.cache = {}; this.cache = {};
} }
IconFactory.prototype.iconForAddress = function (address, diameter) { IconFactory.prototype.iconForAddress = function (
const addr = toChecksumHexAddress(address); address,
diameter,
if (iconExistsFor(addr)) { useTokenDetection,
return imageElFor(addr); tokenList,
) {
// When useTokenDetection flag is true the tokenList contains tokens with non-checksum address from the dynamic token service api,
// When useTokenDetection flag is false the tokenList contains tokens with checksum addresses from contract-metadata.
// So the flag indicates whether the address of tokens currently on the tokenList is checksum or not.
const addr = useTokenDetection ? address : toChecksumHexAddress(address);
if (iconExistsFor(addr, tokenList)) {
return imageElFor(addr, useTokenDetection, tokenList);
} }
return this.generateIdenticonSvg(address, diameter); return this.generateIdenticonSvg(address, diameter);
@ -49,18 +55,22 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) {
// util // util
function iconExistsFor(address) { function iconExistsFor(address, tokenList) {
return ( return (
contractMap[address] && tokenList[address] &&
isValidHexAddress(address, { allowNonPrefixed: false }) && isValidHexAddress(address, { allowNonPrefixed: false }) &&
contractMap[address].logo tokenList[address].iconUrl
); );
} }
function imageElFor(address) { function imageElFor(address, useTokenDetection, tokenList) {
const contract = contractMap[address]; const tokenMetadata = tokenList[address];
const fileName = contract.logo; const fileName = tokenMetadata?.iconUrl;
const path = `images/contract/${fileName}`; // token from dynamic api list is fetched when useTokenDetection is true
// In the static list, the iconUrl will be holding only a filename for the image,
// the corresponding images will be available in the `images/contract/` location when the contract-metadata package was added to the extension
// so that it can be accessed using the filename in iconUrl.
const path = useTokenDetection ? fileName : `images/contract/${fileName}`;
const img = document.createElement('img'); const img = document.createElement('img');
img.src = path; img.src = path;
img.style.width = '100%'; img.style.width = '100%';

View File

@ -1,6 +1,5 @@
import log from 'loglevel'; import log from 'loglevel';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import contractMap from '@metamask/contract-metadata';
import { import {
conversionUtil, conversionUtil,
multiplyCurrencies, multiplyCurrencies,
@ -8,13 +7,6 @@ import {
import * as util from './util'; import * as util from './util';
import { formatCurrency } from './confirm-tx.util'; import { formatCurrency } from './confirm-tx.util';
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
...acc,
[base.toLowerCase()]: contractMap[base],
};
}, {});
const DEFAULT_SYMBOL = ''; const DEFAULT_SYMBOL = '';
async function getSymbolFromContract(tokenAddress) { async function getSymbolFromContract(tokenAddress) {
@ -48,15 +40,21 @@ async function getDecimalsFromContract(tokenAddress) {
} }
} }
function getContractMetadata(tokenAddress) { function getTokenMetadata(tokenAddress, tokenList) {
return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]; const casedTokenList = Object.keys(tokenList).reduce((acc, base) => {
return {
...acc,
[base.toLowerCase()]: tokenList[base],
};
}, {});
return tokenAddress && casedTokenList[tokenAddress.toLowerCase()];
} }
async function getSymbol(tokenAddress) { async function getSymbol(tokenAddress, tokenList) {
let symbol = await getSymbolFromContract(tokenAddress); let symbol = await getSymbolFromContract(tokenAddress);
if (!symbol) { if (!symbol) {
const contractMetadataInfo = getContractMetadata(tokenAddress); const contractMetadataInfo = getTokenMetadata(tokenAddress, tokenList);
if (contractMetadataInfo) { if (contractMetadataInfo) {
symbol = contractMetadataInfo.symbol; symbol = contractMetadataInfo.symbol;
@ -66,11 +64,11 @@ async function getSymbol(tokenAddress) {
return symbol; return symbol;
} }
async function getDecimals(tokenAddress) { async function getDecimals(tokenAddress, tokenList) {
let decimals = await getDecimalsFromContract(tokenAddress); let decimals = await getDecimalsFromContract(tokenAddress);
if (!decimals || decimals === '0') { if (!decimals || decimals === '0') {
const contractMetadataInfo = getContractMetadata(tokenAddress); const contractMetadataInfo = getTokenMetadata(tokenAddress, tokenList);
if (contractMetadataInfo) { if (contractMetadataInfo) {
decimals = contractMetadataInfo.decimals?.toString(); decimals = contractMetadataInfo.decimals?.toString();
@ -80,23 +78,12 @@ async function getDecimals(tokenAddress) {
return decimals; return decimals;
} }
export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) { export async function getSymbolAndDecimals(tokenAddress, tokenList) {
const existingToken = existingTokens.find(
({ address }) => tokenAddress === address,
);
if (existingToken) {
return {
symbol: existingToken.symbol,
decimals: existingToken.decimals,
};
}
let symbol, decimals; let symbol, decimals;
try { try {
symbol = await getSymbol(tokenAddress); symbol = await getSymbol(tokenAddress, tokenList);
decimals = await getDecimals(tokenAddress); decimals = await getDecimals(tokenAddress, tokenList);
} catch (error) { } catch (error) {
log.warn( log.warn(
`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, `symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`,
@ -113,12 +100,12 @@ export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) {
export function tokenInfoGetter() { export function tokenInfoGetter() {
const tokens = {}; const tokens = {};
return async (address) => { return async (address, tokenList) => {
if (tokens[address]) { if (tokens[address]) {
return tokens[address]; return tokens[address];
} }
tokens[address] = await getSymbolAndDecimals(address); tokens[address] = await getSymbolAndDecimals(address, tokenList);
return tokens[address]; return tokens[address];
}; };

View File

@ -56,6 +56,12 @@ export function isDefaultMetaMaskChain(chainId) {
return false; return false;
} }
// Both inputs should be strings. This method is currently used to compare tokenAddress hex strings.
export function isEqualCaseInsensitive(value1, value2) {
if (typeof value1 !== 'string' || typeof value2 !== 'string') return false;
return value1.toLowerCase() === value2.toLowerCase();
}
export function valuesFor(obj) { export function valuesFor(obj) {
if (!obj) { if (!obj) {
return []; return [];

View File

@ -9,6 +9,8 @@ import {
getCurrentCurrency, getCurrentCurrency,
getSwapsDefaultToken, getSwapsDefaultToken,
getCurrentChainId, getCurrentChainId,
getUseTokenDetection,
getTokenList,
} from '../selectors'; } from '../selectors';
import { getConversionRate } from '../ducks/metamask/metamask'; import { getConversionRate } from '../ducks/metamask/metamask';
@ -17,7 +19,7 @@ import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { useEqualityCheck } from './useEqualityCheck'; import { useEqualityCheck } from './useEqualityCheck';
const tokenList = shuffle( const shuffledContractMap = shuffle(
Object.entries(contractMap) Object.entries(contractMap)
.map(([address, tokenData]) => ({ .map(([address, tokenData]) => ({
...tokenData, ...tokenData,
@ -32,9 +34,14 @@ export function getRenderableTokenData(
conversionRate, conversionRate,
currentCurrency, currentCurrency,
chainId, chainId,
tokenList,
useTokenDetection,
) { ) {
const { symbol, name, address, iconUrl, string, balance, decimals } = token; const { symbol, name, address, iconUrl, string, balance, decimals } = token;
// token from dynamic api list is fetched when useTokenDetection is true
const tokenAddress = useTokenDetection
? address
: toChecksumHexAddress(address);
const formattedFiat = const formattedFiat =
getTokenFiatAmount( getTokenFiatAmount(
isSwapsDefaultTokenSymbol(symbol, chainId) isSwapsDefaultTokenSymbol(symbol, chainId)
@ -59,12 +66,12 @@ export function getRenderableTokenData(
) || ''; ) || '';
const usedIconUrl = const usedIconUrl =
iconUrl || iconUrl ||
(contractMap[toChecksumHexAddress(address)] && (tokenList[tokenAddress] &&
`images/contract/${contractMap[toChecksumHexAddress(address)].logo}`); `images/contract/${tokenList[tokenAddress].iconUrl}`);
return { return {
...token, ...token,
primaryLabel: symbol, primaryLabel: symbol,
secondaryLabel: name || contractMap[toChecksumHexAddress(address)]?.name, secondaryLabel: name || tokenList[tokenAddress]?.name,
rightPrimaryLabel: rightPrimaryLabel:
string && `${new BigNumber(string).round(6).toString()} ${symbol}`, string && `${new BigNumber(string).round(6).toString()} ${symbol}`,
rightSecondaryLabel: formattedFiat, rightSecondaryLabel: formattedFiat,
@ -72,18 +79,27 @@ export function getRenderableTokenData(
identiconAddress: usedIconUrl ? null : address, identiconAddress: usedIconUrl ? null : address,
balance, balance,
decimals, decimals,
name: name || contractMap[toChecksumHexAddress(address)]?.name, name: name || tokenList[tokenAddress]?.name,
rawFiat, rawFiat,
}; };
} }
export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { export function useTokensToSearch({
usersTokens = [],
topTokens = {},
shuffledTokensList,
}) {
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate); const conversionRate = useSelector(getConversionRate);
const currentCurrency = useSelector(getCurrentCurrency); const currentCurrency = useSelector(getCurrentCurrency);
const defaultSwapsToken = useSelector(getSwapsDefaultToken); const defaultSwapsToken = useSelector(getSwapsDefaultToken);
const tokenList = useSelector(getTokenList);
const useTokenDetection = useSelector(getUseTokenDetection);
// token from dynamic api list is fetched when useTokenDetection is true
const shuffledTokenList = useTokenDetection
? shuffledTokensList
: shuffledContractMap;
const memoizedTopTokens = useEqualityCheck(topTokens); const memoizedTopTokens = useEqualityCheck(topTokens);
const memoizedUsersToken = useEqualityCheck(usersTokens); const memoizedUsersToken = useEqualityCheck(usersTokens);
@ -93,6 +109,8 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
conversionRate, conversionRate,
currentCurrency, currentCurrency,
chainId, chainId,
tokenList,
useTokenDetection,
); );
const memoizedDefaultToken = useEqualityCheck(defaultToken); const memoizedDefaultToken = useEqualityCheck(defaultToken);
@ -102,7 +120,7 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
? swapsTokens ? swapsTokens
: [ : [
memoizedDefaultToken, memoizedDefaultToken,
...tokenList.filter( ...shuffledTokenList.filter(
(token) => token.symbol !== memoizedDefaultToken.symbol, (token) => token.symbol !== memoizedDefaultToken.symbol,
), ),
]; ];
@ -132,6 +150,8 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
conversionRate, conversionRate,
currentCurrency, currentCurrency,
chainId, chainId,
tokenList,
useTokenDetection,
); );
if ( if (
isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) || isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) ||
@ -166,5 +186,7 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
currentCurrency, currentCurrency,
memoizedTopTokens, memoizedTopTokens,
chainId, chainId,
tokenList,
useTokenDetection,
]); ]);
} }

View File

@ -40,6 +40,11 @@ class AddToken extends Component {
mostRecentOverviewPage: PropTypes.string.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired,
chainId: PropTypes.string, chainId: PropTypes.string,
rpcPrefs: PropTypes.object, rpcPrefs: PropTypes.object,
tokenList: PropTypes.object,
};
static defaultProps = {
tokenList: {},
}; };
state = { state = {
@ -140,7 +145,10 @@ class AddToken extends Component {
return; return;
} }
const { setPendingTokens, history } = this.props; const { setPendingTokens, history, tokenList } = this.props;
const tokenAddressList = Object.keys(tokenList).map((address) =>
address.toLowerCase(),
);
const { const {
customAddress: address, customAddress: address,
customSymbol: symbol, customSymbol: symbol,
@ -154,12 +162,16 @@ class AddToken extends Component {
decimals, decimals,
}; };
setPendingTokens({ customToken, selectedTokens }); setPendingTokens({ customToken, selectedTokens, tokenAddressList });
history.push(CONFIRM_ADD_TOKEN_ROUTE); history.push(CONFIRM_ADD_TOKEN_ROUTE);
} }
async attemptToAutoFillTokenParams(address) { async attemptToAutoFillTokenParams(address) {
const { symbol = '', decimals } = await this.tokenInfoGetter(address); const { tokenList } = this.props;
const { symbol = '', decimals } = await this.tokenInfoGetter(
address,
tokenList,
);
const symbolAutoFilled = Boolean(symbol); const symbolAutoFilled = Boolean(symbol);
const decimalAutoFilled = Boolean(decimals); const decimalAutoFilled = Boolean(decimals);
@ -358,8 +370,8 @@ class AddToken extends Component {
} }
renderSearchToken() { renderSearchToken() {
const { tokenList } = this.props;
const { tokenSelectorError, selectedTokens, searchResults } = this.state; const { tokenSelectorError, selectedTokens, searchResults } = this.state;
return ( return (
<div className="add-token__search-token"> <div className="add-token__search-token">
<TokenSearch <TokenSearch
@ -367,6 +379,7 @@ class AddToken extends Component {
this.setState({ searchResults: results }) this.setState({ searchResults: results })
} }
error={tokenSelectorError} error={tokenSelectorError}
tokenList={tokenList}
/> />
<div className="add-token__token-list"> <div className="add-token__token-list">
<TokenList <TokenList

View File

@ -5,6 +5,7 @@ import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { import {
getIsMainnet, getIsMainnet,
getRpcPrefsForCurrentProvider, getRpcPrefsForCurrentProvider,
getTokenList,
} from '../../selectors/selectors'; } from '../../selectors/selectors';
import AddToken from './add-token.component'; import AddToken from './add-token.component';
@ -25,6 +26,7 @@ const mapStateToProps = (state) => {
showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true', showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true',
chainId, chainId,
rpcPrefs: getRpcPrefsForCurrentProvider(state), rpcPrefs: getRpcPrefsForCurrentProvider(state),
tokenList: getTokenList(state),
}; };
}; };

View File

@ -26,6 +26,7 @@ describe('Add Token', () => {
identities: {}, identities: {},
mostRecentOverviewPage: '/', mostRecentOverviewPage: '/',
showSearchTab: true, showSearchTab: true,
tokenList: {},
}; };
describe('Add Token', () => { describe('Add Token', () => {

View File

@ -14,6 +14,7 @@ export default class TokenList extends Component {
results: PropTypes.array, results: PropTypes.array,
selectedTokens: PropTypes.object, selectedTokens: PropTypes.object,
onToggleToken: PropTypes.func, onToggleToken: PropTypes.func,
useTokenDetection: PropTypes.bool,
}; };
render() { render() {
@ -22,6 +23,7 @@ export default class TokenList extends Component {
selectedTokens = {}, selectedTokens = {},
onToggleToken, onToggleToken,
tokens = [], tokens = [],
useTokenDetection,
} = this.props; } = this.props;
return results.length === 0 ? ( return results.length === 0 ? (
@ -35,13 +37,17 @@ export default class TokenList extends Component {
{Array(6) {Array(6)
.fill(undefined) .fill(undefined)
.map((_, i) => { .map((_, i) => {
const { logo, symbol, name, address } = results[i] || {}; const { iconUrl, symbol, name, address } = results[i] || {};
// token from dynamic api list is fetched when useTokenDetection is true
const iconPath = useTokenDetection
? iconUrl
: `images/contract/${iconUrl}`;
const tokenAlreadyAdded = checkExistingAddresses(address, tokens); const tokenAlreadyAdded = checkExistingAddresses(address, tokens);
const onClick = () => const onClick = () =>
!tokenAlreadyAdded && onToggleToken(results[i]); !tokenAlreadyAdded && onToggleToken(results[i]);
return ( return (
Boolean(logo || symbol || name) && ( Boolean(iconUrl || symbol || name) && (
<div <div
className={classnames('token-list__token', { className={classnames('token-list__token', {
'token-list__token--selected': selectedTokens[address], 'token-list__token--selected': selectedTokens[address],
@ -55,7 +61,7 @@ export default class TokenList extends Component {
<div <div
className="token-list__token-icon" className="token-list__token-icon"
style={{ style={{
backgroundImage: logo && `url(images/contract/${logo})`, backgroundImage: iconUrl && `url(${iconPath})`,
}} }}
/> />
<div className="token-list__token-data"> <div className="token-list__token-data">

View File

@ -2,9 +2,10 @@ import { connect } from 'react-redux';
import TokenList from './token-list.component'; import TokenList from './token-list.component';
const mapStateToProps = ({ metamask }) => { const mapStateToProps = ({ metamask }) => {
const { tokens } = metamask; const { tokens, useTokenDetection } = metamask;
return { return {
tokens, tokens,
useTokenDetection,
}; };
}; };

View File

@ -1,26 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import contractMap from '@metamask/contract-metadata';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import InputAdornment from '@material-ui/core/InputAdornment'; import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '../../../components/ui/text-field'; import TextField from '../../../components/ui/text-field';
import { isEqualCaseInsensitive } from '../../../helpers/utils/util';
const contractList = Object.entries(contractMap)
.map(([address, tokenData]) => ({ ...tokenData, address }))
.filter((tokenData) => Boolean(tokenData.erc20));
const fuse = new Fuse(contractList, {
shouldSort: true,
threshold: 0.45,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{ name: 'name', weight: 0.5 },
{ name: 'symbol', weight: 0.5 },
],
});
export default class TokenSearch extends Component { export default class TokenSearch extends Component {
static contextTypes = { static contextTypes = {
@ -34,17 +17,40 @@ export default class TokenSearch extends Component {
static propTypes = { static propTypes = {
onSearch: PropTypes.func, onSearch: PropTypes.func,
error: PropTypes.string, error: PropTypes.string,
tokenList: PropTypes.object,
}; };
state = { state = {
searchQuery: '', searchQuery: '',
}; };
constructor(props) {
super(props);
const { tokenList } = this.props;
this.tokenList = Object.values(tokenList);
this.tokenSearchFuse = new Fuse(this.tokenList, {
shouldSort: true,
threshold: 0.45,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{ name: 'name', weight: 0.5 },
{ name: 'symbol', weight: 0.5 },
],
});
}
handleSearch(searchQuery) { handleSearch(searchQuery) {
this.setState({ searchQuery }); this.setState({ searchQuery });
const fuseSearchResult = fuse.search(searchQuery); const fuseSearchResult = this.tokenSearchFuse.search(searchQuery);
const addressSearchResult = contractList.filter((token) => { const addressSearchResult = this.tokenList.filter((token) => {
return token.address.toLowerCase() === searchQuery.toLowerCase(); return (
token.address &&
searchQuery &&
isEqualCaseInsensitive(token.address, searchQuery)
);
}); });
const results = [...addressSearchResult, ...fuseSearchResult]; const results = [...addressSearchResult, ...fuseSearchResult];
this.props.onSearch({ searchQuery, results }); this.props.onSearch({ searchQuery, results });

View File

@ -1,7 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { compose } from 'redux'; import { compose } from 'redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import contractMap from '@metamask/contract-metadata';
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { import {
@ -30,6 +29,8 @@ import {
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
getPreferences, getPreferences,
getAccountType, getAccountType,
getUseTokenDetection,
getTokenList,
} from '../../selectors'; } from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { import {
@ -47,13 +48,6 @@ import {
import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app'; import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app';
import ConfirmTransactionBase from './confirm-transaction-base.component'; import ConfirmTransactionBase from './confirm-transaction-base.component';
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
...acc,
[base.toLowerCase()]: contractMap[base],
};
}, {});
let customNonceValue = ''; let customNonceValue = '';
const customNonceMerge = (txData) => const customNonceMerge = (txData) =>
customNonceValue customNonceValue
@ -109,9 +103,19 @@ const mapStateToProps = (state, ownProps) => {
const { name: fromName } = identities[fromAddress]; const { name: fromName } = identities[fromAddress];
const toAddress = propsToAddress || txParamsToAddress; const toAddress = propsToAddress || txParamsToAddress;
const tokenList = getTokenList(state);
const useTokenDetection = getUseTokenDetection(state);
const casedTokenList = useTokenDetection
? tokenList
: Object.keys(tokenList).reduce((acc, base) => {
return {
...acc,
[base.toLowerCase()]: tokenList[base],
};
}, {});
const toName = const toName =
identities[toAddress]?.name || identities[toAddress]?.name ||
casedContractMap[toAddress]?.name || casedTokenList[toAddress]?.name ||
shortenAddress(toChecksumHexAddress(toAddress)); shortenAddress(toChecksumHexAddress(toAddress));
const checksummedAddress = toChecksumHexAddress(toAddress); const checksummedAddress = toChecksumHexAddress(toAddress);

View File

@ -36,6 +36,8 @@ import {
getCurrentCurrency, getCurrentCurrency,
getCurrentChainId, getCurrentChainId,
getRpcPrefsForCurrentProvider, getRpcPrefsForCurrentProvider,
getUseTokenDetection,
getTokenList,
} from '../../../selectors'; } from '../../../selectors';
import { import {
@ -83,6 +85,7 @@ export default function BuildQuote({
selectedAccountAddress, selectedAccountAddress,
isFeatureFlagLoaded, isFeatureFlagLoaded,
tokenFromError, tokenFromError,
shuffledTokensList,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -105,6 +108,8 @@ export default function BuildQuote({
const defaultSwapsToken = useSelector(getSwapsDefaultToken); const defaultSwapsToken = useSelector(getSwapsDefaultToken);
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const tokenList = useSelector(getTokenList);
const useTokenDetection = useSelector(getUseTokenDetection);
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate); const conversionRate = useSelector(getConversionRate);
@ -138,11 +143,14 @@ export default function BuildQuote({
conversionRate, conversionRate,
currentCurrency, currentCurrency,
chainId, chainId,
tokenList,
useTokenDetection,
); );
const tokensToSearch = useTokensToSearch({ const tokensToSearch = useTokensToSearch({
usersTokens: memoizedUsersTokens, usersTokens: memoizedUsersTokens,
topTokens: topAssets, topTokens: topAssets,
shuffledTokensList,
}); });
const selectedToToken = const selectedToToken =
tokensToSearch.find(({ address }) => address === toToken?.address) || tokensToSearch.find(({ address }) => address === toToken?.address) ||
@ -611,4 +619,5 @@ BuildQuote.propTypes = {
selectedAccountAddress: PropTypes.string, selectedAccountAddress: PropTypes.string,
isFeatureFlagLoaded: PropTypes.bool.isRequired, isFeatureFlagLoaded: PropTypes.bool.isRequired,
tokenFromError: PropTypes.string, tokenFromError: PropTypes.string,
shuffledTokensList: PropTypes.array,
}; };

View File

@ -19,6 +19,7 @@ const createProps = (customProps = {}) => {
maxSlippage: 15, maxSlippage: 15,
selectedAccountAddress: 'selectedAccountAddress', selectedAccountAddress: 'selectedAccountAddress',
isFeatureFlagLoaded: false, isFeatureFlagLoaded: false,
shuffledTokensList: [],
...customProps, ...customProps,
}; };
}; };

View File

@ -8,6 +8,7 @@ import {
Redirect, Redirect,
} from 'react-router-dom'; } from 'react-router-dom';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { shuffle } from 'lodash';
import { I18nContext } from '../../contexts/i18n'; import { I18nContext } from '../../contexts/i18n';
import { import {
getSelectedAccount, getSelectedAccount,
@ -15,6 +16,7 @@ import {
getIsSwapsChain, getIsSwapsChain,
isHardwareWallet, isHardwareWallet,
getHardwareWalletType, getHardwareWalletType,
getTokenList,
} from '../../selectors/selectors'; } from '../../selectors/selectors';
import { import {
getQuotes, getQuotes,
@ -119,6 +121,8 @@ export default function Swap() {
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
); );
const fromToken = useSelector(getFromToken); const fromToken = useSelector(getFromToken);
const tokenList = useSelector(getTokenList);
const listTokenValues = shuffle(Object.values(tokenList));
if (networkAndAccountSupports1559) { if (networkAndAccountSupports1559) {
// This will pre-load gas fees before going to the View Quote page. // This will pre-load gas fees before going to the View Quote page.
@ -336,6 +340,7 @@ export default function Swap() {
maxSlippage={maxSlippage} maxSlippage={maxSlippage}
isFeatureFlagLoaded={isFeatureFlagLoaded} isFeatureFlagLoaded={isFeatureFlagLoaded}
tokenFromError={tokenFromError} tokenFromError={tokenFromError}
shuffledTokensList={listTokenValues}
/> />
); );
}} }}

View File

@ -599,3 +599,21 @@ export function getShowRecoveryPhraseReminder(state) {
return currentTime - recoveryPhraseReminderLastShown >= frequency; return currentTime - recoveryPhraseReminderLastShown >= frequency;
} }
/**
* To get the useTokenDetection flag which determines whether a static or dynamic token list is used
* @param {*} state
* @returns Boolean
*/
export function getUseTokenDetection(state) {
return Boolean(state.metamask.useTokenDetection);
}
/**
* To retrieve the tokenList produced by TokenListcontroller
* @param {*} state
* @returns {Object}
*/
export function getTokenList(state) {
return state.metamask.tokenList;
}

View File

@ -129,4 +129,60 @@ describe('Selectors', () => {
const totalUnapprovedCount = selectors.getTotalUnapprovedCount(mockState); const totalUnapprovedCount = selectors.getTotalUnapprovedCount(mockState);
expect(totalUnapprovedCount).toStrictEqual(1); expect(totalUnapprovedCount).toStrictEqual(1);
}); });
it('#getUseTokenDetection', () => {
const useTokenDetection = selectors.getUseTokenDetection(mockState);
expect(useTokenDetection).toStrictEqual(true);
});
it('#getTokenList', () => {
const tokenList = selectors.getTokenList(mockState);
expect(tokenList).toStrictEqual({
'0x2260fac5e5542a773aa44fbcfedf7c193bc2c599': {
address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
symbol: 'WBTC',
decimals: 8,
name: 'Wrapped Bitcoin',
iconUrl: 'https://s3.amazonaws.com/airswap-token-images/WBTC.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
'0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e': {
address: '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e',
symbol: 'YFI',
decimals: 18,
name: 'yearn.finance',
iconUrl:
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e/logo.png',
aggregators: [
'airswapLight',
'bancor',
'cmc',
'coinGecko',
'kleros',
'oneInch',
'paraswap',
'pmm',
'totle',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 12,
},
});
});
}); });

View File

@ -9,6 +9,7 @@ import {
} from '../helpers/utils/i18n-helper'; } from '../helpers/utils/i18n-helper';
import { getMethodDataAsync } from '../helpers/utils/transactions.util'; import { getMethodDataAsync } from '../helpers/utils/transactions.util';
import { getSymbolAndDecimals } from '../helpers/utils/token-util'; import { getSymbolAndDecimals } from '../helpers/utils/token-util';
import { isEqualCaseInsensitive } from '../helpers/utils/util';
import switchDirection from '../helpers/utils/switch-direction'; import switchDirection from '../helpers/utils/switch-direction';
import { import {
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
@ -21,11 +22,11 @@ import {
getMetaMaskAccounts, getMetaMaskAccounts,
getPermittedAccountsForCurrentTab, getPermittedAccountsForCurrentTab,
getSelectedAddress, getSelectedAddress,
getTokenList,
} from '../selectors'; } from '../selectors';
import { computeEstimatedGasLimit, resetSendState } from '../ducks/send'; import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'; import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import * as actionConstants from './actionConstants'; import * as actionConstants from './actionConstants';
@ -2039,11 +2040,11 @@ export function setUsePhishDetect(val) {
}; };
} }
export function setUseStaticTokenList(val) { export function setUseTokenDetection(val) {
return (dispatch) => { return (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
log.debug(`background.setUseStaticTokenList`); log.debug(`background.setUseTokenDetection`);
background.setUseStaticTokenList(val, (err) => { background.setUseTokenDetection(val, (err) => {
dispatch(hideLoadingIndication()); dispatch(hideLoadingIndication());
if (err) { if (err) {
dispatch(displayWarning(err.message)); dispatch(displayWarning(err.message));
@ -2100,7 +2101,11 @@ export function setCurrentLocale(locale, messages) {
} }
export function setPendingTokens(pendingTokens) { export function setPendingTokens(pendingTokens) {
const { customToken = {}, selectedTokens = {} } = pendingTokens; const {
customToken = {},
selectedTokens = {},
tokenAddressList = [],
} = pendingTokens;
const { address, symbol, decimals } = customToken; const { address, symbol, decimals } = customToken;
const tokens = const tokens =
address && symbol && decimals >= 0 <= 36 address && symbol && decimals >= 0 <= 36
@ -2114,8 +2119,8 @@ export function setPendingTokens(pendingTokens) {
: selectedTokens; : selectedTokens;
Object.keys(tokens).forEach((tokenAddress) => { Object.keys(tokens).forEach((tokenAddress) => {
tokens[tokenAddress].unlisted = !LISTED_CONTRACT_ADDRESSES.includes( tokens[tokenAddress].unlisted = !tokenAddressList.find((addr) =>
tokenAddress.toLowerCase(), isEqualCaseInsensitive(addr, tokenAddress),
); );
}); });
@ -2522,6 +2527,7 @@ export function loadingTokenParamsFinished() {
export function getTokenParams(tokenAddress) { export function getTokenParams(tokenAddress) {
return (dispatch, getState) => { return (dispatch, getState) => {
const tokenList = getTokenList(getState());
const existingTokens = getState().metamask.tokens; const existingTokens = getState().metamask.tokens;
const existingToken = existingTokens.find( const existingToken = existingTokens.find(
({ address }) => tokenAddress === address, ({ address }) => tokenAddress === address,
@ -2537,10 +2543,12 @@ export function getTokenParams(tokenAddress) {
dispatch(loadingTokenParamsStarted()); dispatch(loadingTokenParamsStarted());
log.debug(`loadingTokenParams`); log.debug(`loadingTokenParams`);
return getSymbolAndDecimals(tokenAddress).then(({ symbol, decimals }) => { return getSymbolAndDecimals(tokenAddress, tokenList).then(
({ symbol, decimals }) => {
dispatch(addToken(tokenAddress, symbol, Number(decimals))); dispatch(addToken(tokenAddress, symbol, Number(decimals)));
dispatch(loadingTokenParamsFinished()); dispatch(loadingTokenParamsFinished());
}); },
);
}; };
} }