Update DetectTokenController for token detection V2 (#14216)

feature/default_network_editable
Niranjana Binoy 3 years ago committed by GitHub
parent dc3f14ffbb
commit 455d4a9825
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/_locales/en/messages.json
  2. 107
      app/scripts/controllers/detect-tokens.js
  3. 150
      app/scripts/metamask-controller.js
  4. 26
      shared/modules/network.utils.js
  5. 1
      ui/components/app/app-components.scss
  6. 36
      ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.js
  7. 5
      ui/components/app/asset-list/detetcted-tokens-link/index.scss
  8. 8
      ui/selectors/selectors.js
  9. 59
      ui/store/actions.js

@ -2229,6 +2229,10 @@
"notifications9Title": {
"message": "👓 We are making transactions easier to read."
},
"numberOfNewTokensDetected": {
"message": "$1 new tokens found in this account",
"description": "$1 is the number of new tokens detected"
},
"ofTextNofM": {
"message": "of"
},

@ -4,6 +4,7 @@ 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 { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { isTokenDetectionEnabledForNetwork } from '../../../shared/modules/network.utils';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
// By default, poll every 3 minutes
@ -24,6 +25,7 @@ export default class DetectTokensController {
* @param config.keyringMemStore
* @param config.tokenList
* @param config.tokensController
* @param config.assetsContractController
*/
constructor({
interval = DEFAULT_INTERVAL,
@ -32,7 +34,9 @@ export default class DetectTokensController {
keyringMemStore,
tokenList,
tokensController,
assetsContractController = null,
} = {}) {
this.assetsContractController = assetsContractController;
this.tokensController = tokensController;
this.preferences = preferences;
this.interval = interval;
@ -44,6 +48,9 @@ export default class DetectTokensController {
return token.address;
});
this.hiddenTokens = this.tokensController?.state.ignoredTokens;
this.detectedTokens = process.env.TOKEN_DETECTION_V2
? this.tokensController?.state.detectedTokens
: [];
preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
@ -55,14 +62,24 @@ export default class DetectTokensController {
this.restartTokenDetection();
}
});
tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address;
});
this.hiddenTokens = ignoredTokens;
});
tokensController?.subscribe(
({ tokens = [], ignoredTokens = [], detectedTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address;
});
this.hiddenTokens = ignoredTokens;
this.detectedTokens = process.env.TOKEN_DETECTION_V2
? detectedTokens
: [];
},
);
}
/**
* TODO: Remove during TOKEN_DETECTION_V2 feature flag clean up
*
* @param tokens
*/
async _getTokenBalances(tokens) {
const ethContract = this.web3.eth
.contract(SINGLE_CALL_BALANCES_ABI)
@ -84,14 +101,23 @@ export default class DetectTokensController {
if (!this.isActive) {
return;
}
if (
process.env.TOKEN_DETECTION_V2 &&
(!this.useTokenDetection ||
!isTokenDetectionEnabledForNetwork(
this._network.store.getState().provider.chainId,
))
) {
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
!process.env.TOKEN_DETECTION_V2 &&
(this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID ||
Object.keys(tokenList).length === 0)
) {
return;
}
@ -105,6 +131,9 @@ export default class DetectTokensController {
) &&
!this.hiddenTokens.find((address) =>
isEqualCaseInsensitive(address, tokenAddress),
) &&
!this.detectedTokens.find(({ address }) =>
isEqualCaseInsensitive(address, tokenAddress),
)
) {
tokensToDetect.push(tokenAddress);
@ -117,7 +146,12 @@ export default class DetectTokensController {
for (const tokensSlice of sliceOfTokensToDetect) {
let result;
try {
result = await this._getTokenBalances(tokensSlice);
result = process.env.TOKEN_DETECTION_V2
? await this.assetsContractController.getBalancesInSingleCall(
this.selectedAddress,
tokensSlice,
)
: await this._getTokenBalances(tokensSlice);
} catch (error) {
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
@ -126,20 +160,45 @@ export default class DetectTokensController {
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,
);
}),
);
let tokensWithBalance = [];
if (process.env.TOKEN_DETECTION_V2) {
if (result) {
const nonZeroTokenAddresses = Object.keys(result);
for (const nonZeroTokenAddress of nonZeroTokenAddresses) {
const {
address,
symbol,
decimals,
iconUrl,
aggregators,
} = tokenList[nonZeroTokenAddress];
tokensWithBalance.push({
address,
symbol,
decimals,
image: iconUrl,
aggregators,
});
}
if (tokensWithBalance.length > 0) {
await this.tokensController.addDetectedTokens(tokensWithBalance);
}
}
} else {
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,
);
}),
);
}
}
}

@ -238,16 +238,35 @@ export default class MetamaskController extends EventEmitter {
config: { provider: this.provider },
state: initState.TokensController,
});
this.assetsContractController = new AssetsContractController(
{
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
},
{
provider: this.provider,
},
);
process.env.TOKEN_DETECTION_V2
? (this.assetsContractController = new AssetsContractController({
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
provider: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
config: {
provider: this.provider,
},
state: initState.AssetsContractController,
}))
: (this.assetsContractController = new AssetsContractController(
{
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
},
{
provider: this.provider,
},
));
this.collectiblesController = new CollectiblesController(
{
@ -388,33 +407,50 @@ export default class MetamaskController extends EventEmitter {
const tokenListMessenger = this.controllerMessenger.getRestricted({
name: 'TokenListController',
});
this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()),
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
provider: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
onPreferencesStateChange: (cb) =>
this.preferencesController.store.subscribe((preferencesState) => {
const modifiedPreferencesState = {
...preferencesState,
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
};
return cb(modifiedPreferencesState);
}),
messenger: tokenListMessenger,
state: initState.TokenListController,
});
process.env.TOKEN_DETECTION_V2
? (this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
provider: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
messenger: tokenListMessenger,
state: initState.TokenListController,
}))
: (this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()),
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
provider: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
onPreferencesStateChange: (cb) =>
this.preferencesController.store.subscribe((preferencesState) => {
const modifiedPreferencesState = {
...preferencesState,
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
};
return cb(modifiedPreferencesState);
}),
messenger: tokenListMessenger,
state: initState.TokenListController,
}));
this.phishingController = new PhishingController();
@ -665,13 +701,22 @@ export default class MetamaskController extends EventEmitter {
});
///: END:ONLY_INCLUDE_IN
this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
tokensController: this.tokensController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
});
process.env.TOKEN_DETECTION_V2
? (this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
tokensController: this.tokensController,
assetsContractController: this.assetsContractController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
}))
: (this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
tokensController: this.tokensController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
}));
this.addressBookController = new AddressBookController(
undefined,
@ -1337,6 +1382,7 @@ export default class MetamaskController extends EventEmitter {
tokensController,
smartTransactionsController,
txController,
assetsContractController,
} = this;
return {
@ -1817,6 +1863,22 @@ export default class MetamaskController extends EventEmitter {
collectibleDetectionController,
)
: null,
/** Token Detection V2 */
addDetectedTokens: process.env.TOKEN_DETECTION_V2
? tokensController.addDetectedTokens.bind(tokensController)
: null,
importTokens: process.env.TOKEN_DETECTION_V2
? tokensController.importTokens.bind(tokensController)
: null,
ignoreTokens: process.env.TOKEN_DETECTION_V2
? tokensController.ignoreTokens.bind(tokensController)
: null,
getBalancesInSingleCall: process.env.TOKEN_DETECTION_V2
? assetsContractController.getBalancesInSingleCall.bind(
assetsContractController,
)
: null,
};
}

@ -1,4 +1,10 @@
import { MAX_SAFE_CHAIN_ID } from '../constants/network';
import {
MAX_SAFE_CHAIN_ID,
BSC_CHAIN_ID,
POLYGON_CHAIN_ID,
AVALANCHE_CHAIN_ID,
MAINNET_CHAIN_ID,
} from '../constants/network';
/**
* Checks whether the given number primitive chain ID is safe.
@ -28,3 +34,21 @@ export function isPrefixedFormattedHexString(value) {
}
return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value);
}
/**
* Check if token detection is enabled for certain networks
*
* @param chainId - ChainID of network
* @returns Whether the current network supports token detection
*/
export function isTokenDetectionEnabledForNetwork(chainId) {
switch (chainId) {
case MAINNET_CHAIN_ID:
case BSC_CHAIN_ID:
case POLYGON_CHAIN_ID:
case AVALANCHE_CHAIN_ID:
return true;
default:
return false;
}
}

@ -84,6 +84,7 @@
@import 'advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index';
@import 'advanced-gas-fee-popover/advanced-gas-fee-defaults/index';
@import 'currency-input/index';
@import 'asset-list/detetcted-tokens-link/index';
@import 'detected-token/detected-token-address/index';
@import 'detected-token/detected-token-aggregators/index';
@import 'detected-token/detected-token-values/index';

@ -0,0 +1,36 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Box from '../../../ui/box/box';
import Button from '../../../ui/button';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { getDetectedTokensInCurrentNetwork } from '../../../../selectors';
const DetectedTokensLink = ({ className = '', onClick }) => {
const t = useI18nContext();
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
return (
<Box
className={classNames('detected-tokens-link', className)}
marginTop={1}
>
<Button
type="link"
className="detected-tokens-link__link"
onClick={onClick}
>
{t('numberOfNewTokensDetected', [detectedTokens.length])}
</Button>
</Box>
);
};
DetectedTokensLink.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
};
export default DetectedTokensLink;

@ -0,0 +1,5 @@
.detected-tokens-link {
& &__link {
@include H6;
}
}

@ -946,10 +946,6 @@ export function getIsTokenDetectionSupported(state) {
].includes(chainId);
}
export function getTokenDetectionNoticeDismissed(state) {
return state.metamask.tokenDetectionNoticeDismissed;
}
export function getTokenDetectionWarningDismissed(state) {
return state.metamask.tokenDetectionWarningDismissed;
export function getDetectedTokensInCurrentNetwork(state) {
return state.metamask.detectedTokens;
}

@ -1419,6 +1419,65 @@ export function addToken(
}
};
}
/**
* To add detected tokens to state
*
* @param newDetectedTokens
*/
export function addDetectedTokens(newDetectedTokens) {
return async (dispatch) => {
try {
await promisifiedBackground.addDetectedTokens(newDetectedTokens);
} catch (error) {
log.error(error);
} finally {
await forceUpdateMetamaskState(dispatch);
}
};
}
/**
* To add the tokens user selected to state
*
* @param tokensToImport
*/
export function importTokens(tokensToImport) {
return async (dispatch) => {
try {
await promisifiedBackground.importTokens(tokensToImport);
} catch (error) {
log.error(error);
} finally {
await forceUpdateMetamaskState(dispatch);
}
};
}
/**
* To add ignored tokens to state
*
* @param tokensToIgnore
*/
export function ignoreTokens(tokensToIgnore) {
return async (dispatch) => {
try {
await promisifiedBackground.ignoreTokens(tokensToIgnore);
} catch (error) {
log.error(error);
} finally {
await forceUpdateMetamaskState(dispatch);
}
};
}
/**
* To fetch the ERC20 tokens with non-zero balance in a single call
*
* @param tokens
*/
export async function getBalancesInSingleCall(tokens) {
return await promisifiedBackground.getBalancesInSingleCall(tokens);
}
export function addCollectible(address, tokenID, dontShowLoadingIndicator) {
return async (dispatch) => {

Loading…
Cancel
Save