mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-25 03:20:23 +01:00
Integrating the TokenListController to Extension (#11398)
This commit is contained in:
parent
5cf3e19910
commit
eb987a47b5
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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": {
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,13 @@ import Identicon from './identicon.component';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
metamask: { useBlockie },
|
||||
metamask: { useBlockie, useTokenDetection, tokenList },
|
||||
} = state;
|
||||
|
||||
return {
|
||||
useBlockie,
|
||||
useTokenDetection,
|
||||
tokenList,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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%';
|
||||
|
@ -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];
|
||||
};
|
||||
|
@ -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 [];
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -26,6 +26,7 @@ describe('Add Token', () => {
|
||||
identities: {},
|
||||
mostRecentOverviewPage: '/',
|
||||
showSearchTab: true,
|
||||
tokenList: {},
|
||||
};
|
||||
|
||||
describe('Add Token', () => {
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 });
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ const createProps = (customProps = {}) => {
|
||||
maxSlippage: 15,
|
||||
selectedAccountAddress: 'selectedAccountAddress',
|
||||
isFeatureFlagLoaded: false,
|
||||
shuffledTokensList: [],
|
||||
...customProps,
|
||||
};
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 }) => {
|
||||
return getSymbolAndDecimals(tokenAddress, tokenList).then(
|
||||
({ symbol, decimals }) => {
|
||||
dispatch(addToken(tokenAddress, symbol, Number(decimals)));
|
||||
dispatch(loadingTokenParamsFinished());
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user