mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
3732c5f71e
ESLint rules have been added to enforce our JSDoc conventions. These rules were introduced by updating `@metamask/eslint-config` to v9. Some of the rules have been disabled because the effort to fix all lint errors was too high. It might be easiest to enable these rules one directory at a time, or one rule at a time. Most of the changes in this PR were a result of running `yarn lint:fix`. There were a handful of manual changes that seemed obvious and simple to make. Anything beyond that and the rule was left disabled.
224 lines
6.0 KiB
JavaScript
224 lines
6.0 KiB
JavaScript
import Web3 from 'web3';
|
|
import { warn } from 'loglevel';
|
|
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
|
|
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
|
|
import { MINUTE } from '../../../shared/constants/time';
|
|
import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util';
|
|
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
|
|
|
|
// 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
|
|
*/
|
|
constructor({
|
|
interval = DEFAULT_INTERVAL,
|
|
preferences,
|
|
network,
|
|
keyringMemStore,
|
|
tokenList,
|
|
tokensController,
|
|
} = {}) {
|
|
this.tokensController = tokensController;
|
|
this.preferences = preferences;
|
|
this.interval = interval;
|
|
this.network = network;
|
|
this.keyringMemStore = keyringMemStore;
|
|
this.tokenList = tokenList;
|
|
this.selectedAddress = this.preferences?.store.getState().selectedAddress;
|
|
this.tokenAddresses = this.tokensController?.state.tokens.map((token) => {
|
|
return token.address;
|
|
});
|
|
this.hiddenTokens = this.tokensController?.state.ignoredTokens;
|
|
|
|
preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => {
|
|
if (
|
|
this.selectedAddress !== selectedAddress ||
|
|
this.useTokenDetection !== useTokenDetection
|
|
) {
|
|
this.selectedAddress = selectedAddress;
|
|
this.useTokenDetection = useTokenDetection;
|
|
this.restartTokenDetection();
|
|
}
|
|
});
|
|
tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => {
|
|
this.tokenAddresses = tokens.map((token) => {
|
|
return token.address;
|
|
});
|
|
this.hiddenTokens = ignoredTokens;
|
|
});
|
|
}
|
|
|
|
async _getTokenBalances(tokens) {
|
|
const ethContract = this.web3.eth
|
|
.contract(SINGLE_CALL_BALANCES_ABI)
|
|
.at(SINGLE_CALL_BALANCES_ADDRESS);
|
|
return new Promise((resolve, reject) => {
|
|
ethContract.balances([this.selectedAddress], tokens, (error, result) => {
|
|
if (error) {
|
|
return reject(error);
|
|
}
|
|
return resolve(result);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* For each token in the tokenlist provided by the TokenListController, check selectedAddress balance.
|
|
*/
|
|
async detectNewTokens() {
|
|
if (!this.isActive) {
|
|
return;
|
|
}
|
|
|
|
const { tokenList } = this._tokenList.state;
|
|
// since the token detection is currently enabled only on Mainnet
|
|
// we can use the chainId check to ensure token detection is not triggered for any other network
|
|
// but once the balance check contract for other networks are deploayed and ready to use, we need to update this check.
|
|
if (
|
|
this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID ||
|
|
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;
|
|
}
|
|
|
|
const tokensWithBalance = tokensSlice.filter((_, index) => {
|
|
const balance = result[index];
|
|
return balance && !balance.isZero();
|
|
});
|
|
|
|
await Promise.all(
|
|
tokensWithBalance.map((tokenAddress) => {
|
|
return this.tokensController.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.
|
|
*
|
|
*/
|
|
restartTokenDetection() {
|
|
if (!(this.isActive && this.selectedAddress)) {
|
|
return;
|
|
}
|
|
this.detectNewTokens();
|
|
this.interval = DEFAULT_INTERVAL;
|
|
}
|
|
|
|
/* eslint-disable accessor-pairs */
|
|
/**
|
|
* @type {number}
|
|
*/
|
|
set interval(interval) {
|
|
this._handle && clearInterval(this._handle);
|
|
if (!interval) {
|
|
return;
|
|
}
|
|
this._handle = setInterval(() => {
|
|
this.detectNewTokens();
|
|
}, interval);
|
|
}
|
|
|
|
/**
|
|
* @type {Object}
|
|
*/
|
|
set network(network) {
|
|
if (!network) {
|
|
return;
|
|
}
|
|
this._network = network;
|
|
this.web3 = new Web3(network._provider);
|
|
}
|
|
|
|
/**
|
|
* 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 */
|
|
}
|