1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-01 13:47:06 +01:00
metamask-extension/app/scripts/controllers/detect-tokens.test.js
Elliot Winkler 89cec5335f
Replace NetworkController w/ core version (#19486)
This commit fulfills a long-standing desire to get the extension using
the same network controller as mobile by removing NetworkController from
this repo and replacing it with NetworkController from the
`@metamask/network-controller` package.

The new version of NetworkController is different the old one in a few
ways:

- The new controller inherits from BaseControllerV2, so the `state`
  property is used to access the state instead of `store.getState()`.
  All references of the latter have been replaced with the former.
- As the new controller no longer has a `store` property, it cannot be
  subscribed to; the controller takes a messenger which can be
  subscribed to instead. There were various places within
  MetamaskController where the old way of subscribing has been replaced
  with the new way. In addition, DetectTokensController has been updated
  to take a messenger object so that it can listen for NetworkController
  state changes.
- The state of the new controller is not updatable from the outside.
  This affected BackupController, which dumps state from
  NetworkController (among other controllers), but also loads the same
  state into NetworkController on import. A method `loadBackup` has been
  added to NetworkController to facilitate this use case, and
  BackupController is now using this method instead of attempting to
  call `update` on NetworkController.
- The new controller does not have a `getCurrentChainId` method;
  instead, the chain ID can be read from the provider config in state.
  This affected MmiController. (MmiController was also updated to read
  custom networks from the new network controller instead of the
  preferences controller).
- The default network that the new controller is set to is always
  Mainnet (previously it could be either localhost or Goerli in test
  mode, depending on environment variables). This has been addressed
  by feeding the NetworkController initial state using the old logic, so
  this should not apply.
2023-06-22 12:46:09 -06:00

527 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 { toHex } from '@metamask/controller-utils';
import { NetworkController } from '@metamask/network-controller';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';
import PreferencesController from './preferences';
function buildMessenger() {
return new ControllerMessenger().getRestricted({
name: 'DetectTokensController',
allowedEvents: ['NetworkController:stateChange'],
});
}
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: toHex(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: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
'NetworkController:stateChange',
),
});
assetsContractController = new AssetsContractController({
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
'NetworkController:stateChange',
),
});
});
afterEach(function () {
nock.enableNetConnect('localhost');
sandbox.restore();
});
it('should poll on correct interval', async function () {
const stub = sinon.stub(global, 'setInterval');
new DetectTokensController({ messenger: buildMessenger(), 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({
messenger: buildMessenger(),
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: toHex(11155111),
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessengerSepolia,
});
await tokenListController.start();
const controller = new DetectTokensController({
messenger: buildMessenger(),
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({
messenger: buildMessenger(),
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({
messenger: buildMessenger(),
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({
messenger: buildMessenger(),
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({
messenger: buildMessenger(),
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({
messenger: buildMessenger(),
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({
messenger: buildMessenger(),
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);
});
});