mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-23 10:30:04 +01:00
89cec5335f
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.
279 lines
8.4 KiB
JavaScript
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 */
|
|
}
|