1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-29 23:58:06 +01:00
metamask-extension/app/scripts/controllers/detect-tokens.test.js
Mark Stacey b87c1b8526
Make setProviderType async (#18604)
The network controller method `setProviderType` is now async, and the
async operation `_setProviderConfig` called at the end of the method is
now awaited.

Because the only async operation was the last step, this should have no
impact upon the flow of execution. The only functional change is that
now any callers have the option of waiting until the network switch
operation has completed.

One such change was made, in the `switch-ethereum-chain` middleware. As
a result, an error thrown while the network is switching will now
be thrown in this middleware and returned to the dapp as an internal
error.

Relates to https://github.com/MetaMask/metamask-extension/issues/18587
2023-04-17 15:45:01 -02:30

529 lines
15 KiB
JavaScript

import { strict as assert } from 'assert';
import sinon from 'sinon';
import nock from 'nock';
import { ObservableStore } from '@metamask/obs-store';
import BigNumber from 'bignumber.js';
import { ControllerMessenger } from '@metamask/base-controller';
import {
TokenListController,
TokensController,
AssetsContractController,
} from '@metamask/assets-controllers';
import { convertHexToDecimal } from '@metamask/controller-utils';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';
import { NetworkController, NetworkControllerEventType } from './network';
import PreferencesController from './preferences';
describe('DetectTokensController', function () {
let sandbox,
assetsContractController,
keyringMemStore,
network,
preferences,
provider,
tokensController,
tokenListController;
const noop = () => undefined;
const networkControllerProviderConfig = {
getAccounts: noop,
};
const infuraProjectId = 'infura-project-id';
beforeEach(async function () {
sandbox = sinon.createSandbox();
// Disable all requests, even those to localhost
nock.disableNetConnect();
nock('https://mainnet.infura.io')
.post(`/v3/${infuraProjectId}`)
.reply(200, (_uri, requestBody) => {
if (requestBody.method === 'eth_getBlockByNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: {
number: '0x42',
},
};
}
if (requestBody.method === 'eth_blockNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '0x42',
};
}
throw new Error(`(Infura) Mock not defined for ${requestBody.method}`);
})
.persist();
nock('https://sepolia.infura.io')
.post(`/v3/${infuraProjectId}`)
.reply(200, (_uri, requestBody) => {
if (requestBody.method === 'eth_getBlockByNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: {
number: '0x42',
},
};
}
if (requestBody.method === 'eth_blockNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '0x42',
};
}
throw new Error(`(Infura) Mock not defined for ${requestBody.method}`);
})
.persist();
nock('http://localhost:8545')
.post('/')
.reply(200, (_uri, requestBody) => {
if (requestBody.method === 'eth_getBlockByNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: {
number: '0x42',
},
};
}
if (requestBody.method === 'eth_blockNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '0x42',
};
}
if (requestBody.method === 'net_version') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '1337',
};
}
throw new Error(
`(localhost) Mock not defined for ${requestBody.method}`,
);
})
.persist();
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();
keyringMemStore = new ObservableStore({ isUnlocked: false });
const networkControllerMessenger = new ControllerMessenger();
network = new NetworkController({
messenger: networkControllerMessenger,
infuraProjectId,
});
await network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
const tokenListMessenger = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
tokenListController = new TokenListController({
chainId: '1',
preventPollingOnNetworkRestart: false,
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessenger,
});
await tokenListController.start();
preferences = new PreferencesController({
network,
provider,
tokenListController,
onInfuraIsBlocked: sinon.stub(),
onInfuraIsUnblocked: sinon.stub(),
});
preferences.setAddresses([
'0x7e57e2',
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
]);
preferences.setUseTokenDetection(true);
tokensController = new TokensController({
config: { provider },
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: (cb) =>
network.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
},
};
return cb(modifiedNetworkState);
}),
});
assetsContractController = new AssetsContractController({
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: (cb) =>
networkControllerMessenger.subscribe(
NetworkControllerEventType.NetworkDidChange,
() => {
const networkState = network.store.getState();
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: convertHexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
},
),
});
});
afterEach(function () {
nock.enableNetConnect('localhost');
sandbox.restore();
});
it('should poll on correct interval', async function () {
const stub = sinon.stub(global, 'setInterval');
new DetectTokensController({ interval: 1337 }); // eslint-disable-line no-new
assert.strictEqual(stub.getCall(0).args[1], 1337);
stub.restore();
});
it('should be called on every polling period', async function () {
const clock = sandbox.useFakeTimers();
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
});
controller.isOpen = true;
controller.isUnlocked = true;
const stub = sandbox.stub(controller, 'detectNewTokens');
clock.tick(1);
sandbox.assert.notCalled(stub);
clock.tick(180000);
sandbox.assert.called(stub);
clock.tick(180000);
sandbox.assert.calledTwice(stub);
clock.tick(180000);
sandbox.assert.calledThrice(stub);
});
it('should not check and add tokens while on unsupported networks', async function () {
sandbox.useFakeTimers();
await network.setProviderType(NETWORK_TYPES.SEPOLIA);
const tokenListMessengerSepolia = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
tokenListController = new TokenListController({
chainId: '11155111',
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessengerSepolia,
});
await tokenListController.start();
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
});
controller.isOpen = true;
controller.isUnlocked = true;
const stub = sandbox.stub(
assetsContractController,
'getBalancesInSingleCall',
);
await controller.detectNewTokens();
sandbox.assert.notCalled(stub);
});
it('should skip adding tokens listed in ignoredTokens array', async function () {
sandbox.useFakeTimers();
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
trackMetaMetricsEvent: noop,
});
controller.isOpen = true;
controller.isUnlocked = true;
const { tokenList } = tokenListController.state;
const tokenValues = Object.values(tokenList);
await tokensController.addDetectedTokens([
{
address: tokenValues[0].address,
symbol: tokenValues[0].symbol,
decimals: tokenValues[0].decimals,
aggregators: undefined,
image: undefined,
isERC721: undefined,
},
]);
sandbox
.stub(assetsContractController, 'getBalancesInSingleCall')
.callsFake((tokensToDetect) =>
tokensToDetect.map((token) =>
token.address === tokenValues[1].address ? new BigNumber(10) : 0,
),
);
await tokensController.ignoreTokens([tokenValues[1].address]);
await controller.detectNewTokens();
assert.deepEqual(tokensController.state.detectedTokens, [
{
address: toChecksumHexAddress(tokenValues[0].address),
decimals: tokenValues[0].decimals,
symbol: tokenValues[0].symbol,
aggregators: undefined,
image: undefined,
isERC721: undefined,
},
]);
});
it('should check and add tokens while on supported networks', async function () {
sandbox.useFakeTimers();
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
trackMetaMetricsEvent: noop,
});
controller.isOpen = true;
controller.isUnlocked = true;
const { tokenList } = tokenListController.state;
const erc20ContractAddresses = Object.keys(tokenList);
const existingTokenAddress = erc20ContractAddresses[0];
const existingToken = tokenList[existingTokenAddress];
await tokensController.addDetectedTokens([
{
address: existingToken.address,
symbol: existingToken.symbol,
decimals: existingToken.decimals,
aggregators: undefined,
image: undefined,
isERC721: undefined,
},
]);
const tokenAddressToAdd = erc20ContractAddresses[1];
const tokenToAdd = tokenList[tokenAddressToAdd];
sandbox
.stub(assetsContractController, 'getBalancesInSingleCall')
.callsFake(() =>
Promise.resolve({ [tokenAddressToAdd]: new BigNumber(10) }),
);
await controller.detectNewTokens();
assert.deepEqual(tokensController.state.detectedTokens, [
{
address: toChecksumHexAddress(existingTokenAddress),
decimals: existingToken.decimals,
symbol: existingToken.symbol,
aggregators: undefined,
image: undefined,
isERC721: undefined,
},
{
address: toChecksumHexAddress(tokenAddressToAdd),
decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol,
aggregators: undefined,
image: undefined,
isERC721: undefined,
},
]);
});
it('should trigger detect new tokens when change address', async function () {
sandbox.useFakeTimers();
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
});
controller.isOpen = true;
controller.isUnlocked = true;
const stub = sandbox.stub(controller, 'detectNewTokens');
await preferences.setSelectedAddress(
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
);
sandbox.assert.called(stub);
});
it('should trigger detect new tokens when submit password', async function () {
sandbox.useFakeTimers();
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
});
controller.isOpen = true;
controller.selectedAddress = '0x0';
const stub = sandbox.stub(controller, 'detectNewTokens');
await controller._keyringMemStore.updateState({ isUnlocked: true });
sandbox.assert.called(stub);
});
it('should not trigger detect new tokens when not unlocked', async function () {
const clock = sandbox.useFakeTimers();
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokenList: tokenListController,
tokensController,
assetsContractController,
});
controller.isOpen = true;
controller.isUnlocked = false;
const stub = sandbox.stub(
assetsContractController,
'getBalancesInSingleCall',
);
clock.tick(180000);
sandbox.assert.notCalled(stub);
});
it('should not trigger detect new tokens when not open', async function () {
const clock = sandbox.useFakeTimers();
await network.setProviderType(NETWORK_TYPES.MAINNET);
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
tokensController,
assetsContractController,
});
// trigger state update from preferences controller
await preferences.setSelectedAddress(
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
);
controller.isOpen = false;
controller.isUnlocked = true;
const stub = sandbox.stub(
assetsContractController,
'getBalancesInSingleCall',
);
clock.tick(180000);
sandbox.assert.notCalled(stub);
});
});