1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-23 10:30:04 +01:00
metamask-extension/app/scripts/controllers/detect-tokens.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

279 lines
8.4 KiB
JavaScript

import { warn } from 'loglevel';
import { MINUTE } from '../../../shared/constants/time';
import { CHAIN_IDS } from '../../../shared/constants/network';
import { STATIC_MAINNET_TOKEN_LIST } from '../../../shared/constants/tokens';
import { isTokenDetectionEnabledForNetwork } from '../../../shared/modules/network.utils';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import {
AssetType,
TokenStandard,
} from '../../../shared/constants/transaction';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../shared/constants/metametrics';
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = MINUTE * 3;
/**
* A controller that polls for token exchange
* rates based on a user's current token list
*/
export default class DetectTokensController {
/**
* Creates a DetectTokensController
*
* @param {object} [config] - Options to configure controller
* @param config.interval
* @param config.preferences
* @param config.network
* @param config.keyringMemStore
* @param config.tokenList
* @param config.tokensController
* @param config.assetsContractController
* @param config.trackMetaMetricsEvent
* @param config.messenger
*/
constructor({
messenger,
interval = DEFAULT_INTERVAL,
preferences,
network,
keyringMemStore,
tokenList,
tokensController,
assetsContractController = null,
trackMetaMetricsEvent,
} = {}) {
this.messenger = messenger;
this.assetsContractController = assetsContractController;
this.tokensController = tokensController;
this.preferences = preferences;
this.interval = interval;
this.network = network;
this.keyringMemStore = keyringMemStore;
this.tokenList = tokenList;
this.useTokenDetection =
this.preferences?.store.getState().useTokenDetection;
this.selectedAddress = this.preferences?.store.getState().selectedAddress;
this.tokenAddresses = this.tokensController?.state.tokens.map((token) => {
return token.address;
});
this.hiddenTokens = this.tokensController?.state.ignoredTokens;
this.detectedTokens = this.tokensController?.state.detectedTokens;
this.chainId = this.getChainIdFromNetworkStore();
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
this.selectedAddress !== selectedAddress ||
this.useTokenDetection !== useTokenDetection
) {
this.selectedAddress = selectedAddress;
this.useTokenDetection = useTokenDetection;
this.restartTokenDetection({ selectedAddress });
}
});
tokensController?.subscribe(
({ tokens = [], ignoredTokens = [], detectedTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address;
});
this.hiddenTokens = ignoredTokens;
this.detectedTokens = detectedTokens;
},
);
messenger.subscribe('NetworkController:stateChange', () => {
if (this.chainId !== this.getChainIdFromNetworkStore()) {
const chainId = this.getChainIdFromNetworkStore();
this.chainId = chainId;
this.restartTokenDetection({ chainId: this.chainId });
}
});
}
/**
* For each token in the tokenlist provided by the TokenListController, check selectedAddress balance.
*
* @param options
* @param options.selectedAddress - the selectedAddress against which to detect for token balances
* @param options.chainId - the chainId against which to detect for token balances
*/
async detectNewTokens({ selectedAddress, chainId } = {}) {
const addressAgainstWhichToDetect = selectedAddress ?? this.selectedAddress;
const chainIdAgainstWhichToDetect =
chainId ?? this.getChainIdFromNetworkStore();
if (!this.isActive) {
return;
}
if (!isTokenDetectionEnabledForNetwork(chainIdAgainstWhichToDetect)) {
return;
}
if (
!this.useTokenDetection &&
chainIdAgainstWhichToDetect !== CHAIN_IDS.MAINNET
) {
return;
}
const isTokenDetectionInactiveInMainnet =
!this.useTokenDetection &&
chainIdAgainstWhichToDetect === CHAIN_IDS.MAINNET;
const { tokenList } = this._tokenList.state;
const tokenListUsed = isTokenDetectionInactiveInMainnet
? STATIC_MAINNET_TOKEN_LIST
: tokenList;
const tokensToDetect = [];
for (const tokenAddress in tokenListUsed) {
if (
!this.tokenAddresses.find((address) =>
isEqualCaseInsensitive(address, tokenAddress),
) &&
!this.hiddenTokens.find((address) =>
isEqualCaseInsensitive(address, tokenAddress),
) &&
!this.detectedTokens.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.assetsContractController.getBalancesInSingleCall(
addressAgainstWhichToDetect,
tokensSlice,
);
} catch (error) {
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
error,
);
return;
}
const tokensWithBalance = [];
const eventTokensDetails = [];
if (result) {
const nonZeroTokenAddresses = Object.keys(result);
for (const nonZeroTokenAddress of nonZeroTokenAddresses) {
const { address, symbol, decimals } =
tokenListUsed[nonZeroTokenAddress];
eventTokensDetails.push(`${symbol} - ${address}`);
tokensWithBalance.push({
address,
symbol,
decimals,
});
}
if (tokensWithBalance.length > 0) {
this._trackMetaMetricsEvent({
event: MetaMetricsEventName.TokenDetected,
category: MetaMetricsEventCategory.Wallet,
properties: {
tokens: eventTokensDetails,
token_standard: TokenStandard.ERC20,
asset_type: AssetType.token,
},
});
await this.tokensController.addDetectedTokens(tokensWithBalance, {
selectedAddress: addressAgainstWhichToDetect,
chainId: chainIdAgainstWhichToDetect,
});
}
}
}
}
/**
* Restart token detection polling period and call detectNewTokens
* in case of address change or user session initialization.
*
* @param options
* @param options.selectedAddress - the selectedAddress against which to detect for token balances
* @param options.chainId - the chainId against which to detect for token balances
*/
restartTokenDetection({ selectedAddress, chainId } = {}) {
const addressAgainstWhichToDetect = selectedAddress ?? this.selectedAddress;
const chainIdAgainstWhichToDetect = chainId ?? this.chainId;
if (!(this.isActive && addressAgainstWhichToDetect)) {
return;
}
this.detectNewTokens({
selectedAddress: addressAgainstWhichToDetect,
chainId: chainIdAgainstWhichToDetect,
});
this.interval = DEFAULT_INTERVAL;
}
getChainIdFromNetworkStore() {
return this.network?.state.providerConfig.chainId;
}
/* eslint-disable accessor-pairs */
/**
* @type {number}
*/
set interval(interval) {
this._handle && clearInterval(this._handle);
if (!interval) {
return;
}
this._handle = setInterval(() => {
this.detectNewTokens();
}, interval);
}
/**
* In setter when isUnlocked is updated to true, detectNewTokens and restart polling
*
* @type {object}
*/
set keyringMemStore(keyringMemStore) {
if (!keyringMemStore) {
return;
}
this._keyringMemStore = keyringMemStore;
this._keyringMemStore.subscribe(({ isUnlocked }) => {
if (this.isUnlocked !== isUnlocked) {
this.isUnlocked = isUnlocked;
if (isUnlocked) {
this.restartTokenDetection();
}
}
});
}
/**
* @type {object}
*/
set tokenList(tokenList) {
if (!tokenList) {
return;
}
this._tokenList = tokenList;
}
/**
* Internal isActive state
*
* @type {object}
*/
get isActive() {
return this.isOpen && this.isUnlocked;
}
/* eslint-enable accessor-pairs */
}