1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +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 contracts from '@metamask/contract-metadata';
import { warn } from 'loglevel';
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 { MINUTE } from '../../../shared/constants/time';
import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util';
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = MINUTE * 3;
@ -24,57 +23,13 @@ export default class DetectTokensController {
preferences,
network,
keyringMemStore,
tokenList,
} = {}) {
this.preferences = preferences;
this.interval = interval;
this.network = network;
this.keyringMemStore = keyringMemStore;
}
/**
* 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,
);
}
});
this.tokenList = tokenList;
}
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
* in case of address change or user session initialization.
@ -138,9 +150,13 @@ export default class DetectTokensController {
});
this.hiddenTokens = hiddenTokens;
});
preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) {
preferences.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
this.selectedAddress !== selectedAddress ||
this.useTokenDetection !== useTokenDetection
) {
this.selectedAddress = selectedAddress;
this.useTokenDetection = useTokenDetection;
this.restartTokenDetection();
}
});
@ -176,6 +192,16 @@ export default class DetectTokensController {
});
}
/**
* @type {Object}
*/
set tokenList(tokenList) {
if (!tokenList) {
return;
}
this._tokenList = tokenList;
}
/**
* Internal isActive state
* @type {Object}

View File

@ -1,15 +1,19 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import nock from 'nock';
import { ObservableStore } from '@metamask/obs-store';
import contracts from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js';
import {
ControllerMessenger,
TokenListController,
} from '@metamask/controllers';
import { MAINNET, ROPSTEN } from '../../../shared/constants/network';
import DetectTokensController from './detect-tokens';
import NetworkController from './network';
import PreferencesController from './preferences';
describe('DetectTokensController', function () {
let tokenListController;
const sandbox = sinon.createSandbox();
let keyringMemStore, network, preferences, provider;
@ -36,6 +40,87 @@ describe('DetectTokensController', function () {
sandbox
.stub(preferences, '_detectIsERC721')
.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 () {
@ -56,6 +141,7 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = true;
@ -75,10 +161,22 @@ describe('DetectTokensController', function () {
it('should not check tokens while on test network', async function () {
sandbox.useFakeTimers();
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({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = true;
@ -96,17 +194,16 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = true;
const contractAddresses = Object.keys(contracts);
const erc20ContractAddresses = contractAddresses.filter(
(contractAddress) => contracts[contractAddress].erc20 === true,
);
const { tokenList } = tokenListController.state;
const erc20ContractAddresses = Object.keys(tokenList);
const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = contracts[existingTokenAddress];
const existingToken = tokenList[existingTokenAddress];
await preferences.addToken(
existingTokenAddress,
existingToken.symbol,
@ -144,17 +241,16 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = true;
const contractAddresses = Object.keys(contracts);
const erc20ContractAddresses = contractAddresses.filter(
(contractAddress) => contracts[contractAddress].erc20 === true,
);
const { tokenList } = tokenListController.state;
const erc20ContractAddresses = Object.keys(tokenList);
const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = contracts[existingTokenAddress];
const existingToken = tokenList[existingTokenAddress];
await preferences.addToken(
existingTokenAddress,
existingToken.symbol,
@ -162,16 +258,16 @@ describe('DetectTokensController', function () {
);
const tokenAddressToAdd = erc20ContractAddresses[1];
const tokenToAdd = contracts[tokenAddressToAdd];
const tokenToAdd = tokenList[tokenAddressToAdd];
const contractAddresssesToDetect = contractAddresses.filter(
const contractAddressesToDetect = erc20ContractAddresses.filter(
(address) => address !== existingTokenAddress,
);
const indexOfTokenToAdd = contractAddresssesToDetect.indexOf(
const indexOfTokenToAdd = contractAddressesToDetect.indexOf(
tokenAddressToAdd,
);
const balances = new Array(contractAddresssesToDetect.length);
const balances = new Array(contractAddressesToDetect.length);
balances[indexOfTokenToAdd] = new BigNumber(10);
sandbox
@ -203,17 +299,16 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = true;
const contractAddresses = Object.keys(contracts);
const erc20ContractAddresses = contractAddresses.filter(
(contractAddress) => contracts[contractAddress].erc20 === true,
);
const { tokenList } = tokenListController.state;
const erc20ContractAddresses = Object.keys(tokenList);
const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = contracts[existingTokenAddress];
const existingToken = tokenList[existingTokenAddress];
await preferences.addToken(
existingTokenAddress,
existingToken.symbol,
@ -221,16 +316,16 @@ describe('DetectTokensController', function () {
);
const tokenAddressToAdd = erc20ContractAddresses[1];
const tokenToAdd = contracts[tokenAddressToAdd];
const tokenToAdd = tokenList[tokenAddressToAdd];
const contractAddresssesToDetect = contractAddresses.filter(
const contractAddressesToDetect = erc20ContractAddresses.filter(
(address) => address !== existingTokenAddress,
);
const indexOfTokenToAdd = contractAddresssesToDetect.indexOf(
const indexOfTokenToAdd = contractAddressesToDetect.indexOf(
tokenAddressToAdd,
);
const balances = new Array(contractAddresssesToDetect.length);
const balances = new Array(contractAddressesToDetect.length);
balances[indexOfTokenToAdd] = new BigNumber(10);
sandbox
@ -261,6 +356,7 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = true;
@ -277,6 +373,7 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.selectedAddress = '0x0';
@ -292,6 +389,7 @@ describe('DetectTokensController', function () {
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
});
controller.isOpen = true;
controller.isUnlocked = false;

View File

@ -51,7 +51,10 @@ export default class PreferencesController {
useNonceField: false,
usePhishDetect: true,
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.
// 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
*
*/
setUseStaticTokenList(val) {
this.store.updateState({ useStaticTokenList: val });
setUseTokenDetection(val) {
this.store.updateState({ useTokenDetection: val });
}
/**

View File

@ -869,22 +869,22 @@ describe('preferences controller', function () {
);
});
});
describe('setUseStaticTokenList', function () {
it('should default to false', function () {
describe('setUseTokenDetection', function () {
it('should default to true', function () {
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(
preferencesController.store.getState().useStaticTokenList,
false,
);
preferencesController.setUseStaticTokenList(true);
assert.equal(
preferencesController.store.getState().useStaticTokenList,
preferencesController.store.getState().useTokenDetection,
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 EthQuery from 'eth-query';
import nanoid from 'nanoid';
import contractMap from '@metamask/contract-metadata';
import {
AddressBookController,
ApprovalController,
@ -238,8 +237,8 @@ export default class MetamaskController extends EventEmitter {
});
this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()),
useStaticTokenList: this.preferencesController.store.getState()
.useStaticTokenList,
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
@ -251,11 +250,17 @@ export default class MetamaskController extends EventEmitter {
};
return cb(modifiedNetworkState);
}),
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
this.preferencesController.store,
),
onPreferencesStateChange: (cb) =>
this.preferencesController.store.subscribe((preferencesState) => {
const modifiedPreferencesState = {
...preferencesState,
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
};
return cb(modifiedPreferencesState);
}),
messenger: tokenListMessenger,
state: initState.tokenListController,
state: initState.TokenListController,
});
this.phishingController = new PhishingController();
@ -372,6 +377,7 @@ export default class MetamaskController extends EventEmitter {
preferences: this.preferencesController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
});
this.addressBookController = new AddressBookController(
@ -775,8 +781,8 @@ export default class MetamaskController extends EventEmitter {
setUseBlockie: this.setUseBlockie.bind(this),
setUseNonceField: this.setUseNonceField.bind(this),
setUsePhishDetect: this.setUsePhishDetect.bind(this),
setUseStaticTokenList: nodeify(
this.preferencesController.setUseStaticTokenList,
setUseTokenDetection: nodeify(
this.preferencesController.setUseTokenDetection,
this.preferencesController,
),
setIpfsGateway: this.setIpfsGateway.bind(this),
@ -1297,28 +1303,8 @@ export default class MetamaskController extends EventEmitter {
tokens,
} = 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 = {
accountTokens: filteredAccountTokens,
accountTokens,
currentLocale,
frequentRpcList,
identities,

View File

@ -25,5 +25,49 @@
"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,
"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": [
{
"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": {
"accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
@ -123,7 +478,8 @@
"tokens": [],
"useBlockie": false,
"useNonceField": false,
"usePhishDetect": true
"usePhishDetect": true,
"useTokenDetection": true
},
"config": {},
"firstTimeInfo": {

View File

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

View File

@ -52,6 +52,12 @@ async function setupFetchMocking(driver) {
if (url.match(/featureFlags$/u)) {
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);
};

View File

@ -222,6 +222,76 @@ export const createSwapsMockStore = () => {
swapsFeatureIsLive: 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: {
modal: {

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import abi from 'human-standard-token-abi';
import contractMap from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js';
import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util';
import { addHexPrefix } from 'ethereumjs-util';
import { debounce } from 'lodash';
import {
conversionGreaterThan,
@ -39,6 +38,8 @@ import {
getTargetAccount,
getIsNonStandardEthChain,
checkNetworkAndAccountSupports1559,
getUseTokenDetection,
getTokenList,
} from '../../selectors';
import {
disconnectGasFeeEstimatePoller,
@ -71,6 +72,7 @@ import {
isDefaultMetaMaskChain,
isOriginContractAddress,
isValidDomainName,
isEqualCaseInsensitive,
} from '../../helpers/utils/util';
import {
getGasEstimateType,
@ -517,6 +519,8 @@ export const initializeSendState = createAsyncThunk(
gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)),
gasEstimatePollToken,
eip1559support,
useTokenDetection: getUseTokenDetection(state),
tokenAddressList: Object.keys(getTokenList(state)),
};
},
);
@ -986,7 +990,7 @@ const slice = createSlice({
recipient.warning = null;
} else {
const isSendingToken = asset.type === ASSET_TYPES.TOKEN;
const { chainId, tokens } = action.payload;
const { chainId, tokens, tokenAddressList } = action.payload;
if (
isBurnAddress(recipient.userInput) ||
(!isValidHexAddress(recipient.userInput, {
@ -1005,11 +1009,12 @@ const slice = createSlice({
} else {
recipient.error = null;
}
if (
isSendingToken &&
isValidHexAddress(recipient.userInput) &&
(toChecksumAddress(recipient.userInput) in contractMap ||
(tokenAddressList.find((address) =>
isEqualCaseInsensitive(address, recipient.userInput),
) ||
checkExistingAddresses(recipient.userInput, tokens))
) {
recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING;
@ -1210,6 +1215,8 @@ const slice = createSlice({
payload: {
chainId: action.payload.chainId,
tokens: action.payload.tokens,
useTokenDetection: action.payload.useTokenDetection,
tokenAddressList: action.payload.tokenAddressList,
},
});
}
@ -1395,7 +1402,14 @@ export function updateRecipientUserInput(userInput) {
const state = getState();
const chainId = getCurrentChainId(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: {
chainId: '',
tokens: [],
useTokenDetection: true,
tokenAddressList: [],
},
};
@ -649,6 +651,8 @@ describe('Send Slice', () => {
payload: {
chainId: '0x55',
tokens: [],
useTokenDetection: true,
tokenAddressList: [],
},
};
@ -671,6 +675,8 @@ describe('Send Slice', () => {
payload: {
chainId: '',
tokens: [],
useTokenDetection: true,
tokenAddressList: [],
},
};
@ -698,6 +704,8 @@ describe('Send Slice', () => {
payload: {
chainId: '0x4',
tokens: [],
useTokenDetection: true,
tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'],
},
};
@ -1111,6 +1119,32 @@ describe('Send Slice', () => {
provider: {
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,
gas: {
@ -1484,6 +1518,31 @@ describe('Send Slice', () => {
chainId: '',
},
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({
chainId: '',
tokens: [],
useTokenDetection: true,
tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'],
});
});
});
@ -1736,6 +1797,32 @@ describe('Send Slice', () => {
chainId: '',
},
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: {
asset: {

View File

@ -1,4 +1,3 @@
import contractMap from '@metamask/contract-metadata';
import {
isValidHexAddress,
toChecksumHexAddress,
@ -18,11 +17,18 @@ function IconFactory(jazzicon) {
this.cache = {};
}
IconFactory.prototype.iconForAddress = function (address, diameter) {
const addr = toChecksumHexAddress(address);
if (iconExistsFor(addr)) {
return imageElFor(addr);
IconFactory.prototype.iconForAddress = function (
address,
diameter,
useTokenDetection,
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);
@ -49,18 +55,22 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) {
// util
function iconExistsFor(address) {
function iconExistsFor(address, tokenList) {
return (
contractMap[address] &&
tokenList[address] &&
isValidHexAddress(address, { allowNonPrefixed: false }) &&
contractMap[address].logo
tokenList[address].iconUrl
);
}
function imageElFor(address) {
const contract = contractMap[address];
const fileName = contract.logo;
const path = `images/contract/${fileName}`;
function imageElFor(address, useTokenDetection, tokenList) {
const tokenMetadata = tokenList[address];
const fileName = tokenMetadata?.iconUrl;
// 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');
img.src = path;
img.style.width = '100%';

View File

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

View File

@ -56,6 +56,12 @@ export function isDefaultMetaMaskChain(chainId) {
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) {
if (!obj) {
return [];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import contractMap from '@metamask/contract-metadata';
import Fuse from 'fuse.js';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '../../../components/ui/text-field';
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 },
],
});
import { isEqualCaseInsensitive } from '../../../helpers/utils/util';
export default class TokenSearch extends Component {
static contextTypes = {
@ -34,17 +17,40 @@ export default class TokenSearch extends Component {
static propTypes = {
onSearch: PropTypes.func,
error: PropTypes.string,
tokenList: PropTypes.object,
};
state = {
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) {
this.setState({ searchQuery });
const fuseSearchResult = fuse.search(searchQuery);
const addressSearchResult = contractList.filter((token) => {
return token.address.toLowerCase() === searchQuery.toLowerCase();
const fuseSearchResult = this.tokenSearchFuse.search(searchQuery);
const addressSearchResult = this.tokenList.filter((token) => {
return (
token.address &&
searchQuery &&
isEqualCaseInsensitive(token.address, searchQuery)
);
});
const results = [...addressSearchResult, ...fuseSearchResult];
this.props.onSearch({ searchQuery, results });

View File

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

View File

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

View File

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

View File

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

View File

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