Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: token autodetection multi chain #28553

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .storybook/test-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,13 @@ const state = {
decimals: 18,
},
],
tokenBalances: {
'0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': {
'0x1': {
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x25e4bc',
},
},
},
allDetectedTokens: {
'0xaa36a7': {
'0x9d0ba4ddac06032527b140912ec808ab9451b788': [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
diff --git a/dist/assetsUtil.cjs b/dist/assetsUtil.cjs
index 48571b8c1b78e94d88e1837e986b5f8735ac651b..61246f51500c8cab48f18296a73629fb73454caa 100644
--- a/dist/assetsUtil.cjs
+++ b/dist/assetsUtil.cjs
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }
exports.fetchTokenContractExchangeRates = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedStakedBalanceNetworks = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.hasNewCollectionFields = exports.compareNftMetadata = exports.TOKEN_PRICES_BATCH_SIZE = void 0;
const controller_utils_1 = require("@metamask/controller-utils");
const utils_1 = require("@metamask/utils");
@@ -233,7 +234,7 @@ async function getIpfsCIDv1AndPath(ipfsUrl) {
const index = url.indexOf('/');
const cid = index !== -1 ? url.substring(0, index) : url;
const path = index !== -1 ? url.substring(index) : undefined;
- const { CID } = await import("multiformats");
+ const { CID } = _interopRequireWildcard(require("multiformats"));
// We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats)
// because most cid v0s appear to be incompatible with IPFS subdomains
return {
diff --git a/dist/token-prices-service/codefi-v2.mjs b/dist/token-prices-service/codefi-v2.mjs
index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..bf8ec7819f678c2f185d6a85d7e3ea81f055a309 100644
--- a/dist/token-prices-service/codefi-v2.mjs
+++ b/dist/token-prices-service/codefi-v2.mjs
@@ -12,8 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
var _CodefiTokenPricesServiceV2_tokenPricePolicy;
import { handleFetch } from "@metamask/controller-utils";
import { hexToNumber } from "@metamask/utils";
-import $cockatiel from "cockatiel";
-const { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } = $cockatiel;
+import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel"
/**
* The list of currencies that can be supplied as the `vsCurrency` parameter to
* the `/spot-prices` endpoint, in lowercase form.
diff --git a/dist/TokenDetectionController.cjs b/dist/TokenDetectionController.cjs
index 8fd5efde7a3c24080f8a43f79d10300e8c271245..a3c334ac7dd2e5698e6b54a73491b7145c2a9010 100644
--- a/dist/TokenDetectionController.cjs
+++ b/dist/TokenDetectionController.cjs
@@ -250,17 +250,20 @@ _TokenDetectionController_intervalId = new WeakMap(), _TokenDetectionController_
}
});
this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange',
- // TODO: Either fix this lint violation or explain why it's necessary to ignore.
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- async (selectedAccount) => {
- const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id;
- if (isSelectedAccountIdChanged) {
- __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f");
- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, {
- selectedAddress: selectedAccount.address,
- });
- }
- });
+ // TODO: Either fix this lint violation or explain why it's necessary to ignore.
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ async (selectedAccount) => {
+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
+ const chainIds = Object.keys(networkConfigurationsByChainId);
+ const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id;
+ if (isSelectedAccountIdChanged) {
+ __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f");
+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, {
+ selectedAddress: selectedAccount.address,
+ chainIds,
+ });
+ }
+ });
}, _TokenDetectionController_stopPolling = function _TokenDetectionController_stopPolling() {
if (__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")) {
clearInterval(__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f"));
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
"@metamask/address-book-controller": "^6.0.0",
"@metamask/announcement-controller": "^7.0.0",
"@metamask/approval-controller": "^7.0.0",
"@metamask/assets-controllers": "patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@npm%253A44.0.0%23~/.yarn/patches/@metamask-assets-controllers-npm-44.0.0-c223d56176.patch%3A%3Aversion=44.0.0&hash=5a94c2#~/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch",
"@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A44.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch",
"@metamask/base-controller": "^7.0.0",
"@metamask/bitcoin-wallet-snap": "^0.8.2",
"@metamask/browser-passworder": "^4.3.0",
Expand Down
34 changes: 26 additions & 8 deletions ui/components/app/assets/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import TokenList from '../token-list';
import { PRIMARY } from '../../../../helpers/constants/common';
import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency';
import {
getAllDetectedTokensForSelectedAddress,
getDetectedTokensInCurrentNetwork,
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
getPreferences,
getSelectedAccount,
} from '../../../../selectors';
import {
Expand Down Expand Up @@ -75,6 +77,8 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
const isTokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector(
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
);
const { tokenNetworkFilter } = useSelector(getPreferences);
const allNetworksFilterShown = Object.keys(tokenNetworkFilter ?? {}).length;

const [showFundingMethodModal, setShowFundingMethodModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
Expand All @@ -98,16 +102,30 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
// for EVM assets
const shouldShowTokensLinks = showTokensLinks ?? isEvm;

const detectedTokensMultichain = useSelector(
getAllDetectedTokensForSelectedAddress,
);

const totalTokens =
process.env.PORTFOLIO_VIEW && !allNetworksFilterShown
? (Object.values(detectedTokensMultichain).reduce(
// @ts-expect-error TS18046: 'tokenArray' is of type 'unknown'
(count, tokenArray) => count + tokenArray.length,
0,
) as number)
: detectedTokens.length;

return (
<>
{detectedTokens.length > 0 &&
!isTokenDetectionInactiveOnNonMainnetSupportedNetwork && (
<DetectedTokensBanner
className=""
actionButtonOnClick={() => setShowDetectedTokens(true)}
margin={4}
/>
)}
{totalTokens &&
totalTokens > 0 &&
!isTokenDetectionInactiveOnNonMainnetSupportedNetwork ? (
<DetectedTokensBanner
className=""
actionButtonOnClick={() => setShowDetectedTokens(true)}
margin={4}
/>
) : null}
<AssetListControlBar showTokensLinks={shouldShowTokensLinks} />
<TokenList
nativeToken={<NativeToken onClickAsset={onClickAsset} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ import DetectedTokenAddress from '../detected-token-address/detected-token-addre
import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators';
import { Display } from '../../../../helpers/constants/design-system';
import {
getCurrentNetwork,
getTestNetworkBackgroundColor,
getTokenList,
} from '../../../../selectors';
import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network';

const DetectedTokenDetails = ({
token,
handleTokenSelection,
tokensListDetected,
chainId,
}) => {
const tokenList = useSelector(getTokenList);
const tokenData = tokenList[token.address?.toLowerCase()];
const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor);
const currentNetwork = useSelector(getCurrentNetwork);

return (
<Box
Expand All @@ -39,8 +39,7 @@ const DetectedTokenDetails = ({
badge={
<AvatarNetwork
size={AvatarNetworkSize.Xs}
name={currentNetwork?.nickname || ''}
src={currentNetwork?.rpcPrefs?.imageUrl}
src={CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[chainId]}
backgroundColor={testNetworkBackgroundColor}
/>
}
Expand Down Expand Up @@ -84,6 +83,7 @@ DetectedTokenDetails.propTypes = {
}),
handleTokenSelection: PropTypes.func.isRequired,
tokensListDetected: PropTypes.object,
chainId: PropTypes.string,
};

export default DetectedTokenDetails;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';

Expand All @@ -10,8 +10,11 @@ import {
MetaMetricsTokenEventSource,
} from '../../../../../shared/constants/metametrics';
import {
getAllDetectedTokensForSelectedAddress,
getCurrentChainId,
getCurrentNetwork,
getDetectedTokensInCurrentNetwork,
getPreferences,
} from '../../../../selectors';

import Popover from '../../../ui/popover';
Expand All @@ -34,10 +37,30 @@ const DetectedTokenSelectionPopover = ({
const chainId = useSelector(getCurrentChainId);

const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
const { tokenNetworkFilter } = useSelector(getPreferences);
const allNetworksFilterShown = Object.keys(tokenNetworkFilter ?? {}).length;

const currentNetwork = useSelector(getCurrentNetwork);

const detectedTokensMultichain = useSelector(
getAllDetectedTokensForSelectedAddress,
);

const totalTokens = useMemo(() => {
return process.env.PORTFOLIO_VIEW && !allNetworksFilterShown
? Object.values(detectedTokensMultichain).reduce(
(count, tokenArray) => count + tokenArray.length,
0,
)
: detectedTokens.length;
}, [detectedTokensMultichain, detectedTokens, allNetworksFilterShown]);

const { selected: selectedTokens = [] } =
sortingBasedOnTokenSelection(tokensListDetected);

const onClose = () => {
const chainIds = Object.keys(detectedTokensMultichain);

setShowDetectedTokens(false);
const eventTokensDetails = detectedTokens.map(
({ address, symbol }) => `${symbol} - ${address}`,
Expand All @@ -47,8 +70,10 @@ const DetectedTokenSelectionPopover = ({
category: MetaMetricsEventCategory.Wallet,
properties: {
source_connection_method: MetaMetricsTokenEventSource.Detected,
chain_id: chainId,
tokens: eventTokensDetails,
...(process.env.PORTFOLIO_VIEW
? { chain_ids: chainIds }
: { chain_id: chainId }),
},
});
};
Expand Down Expand Up @@ -81,25 +106,44 @@ const DetectedTokenSelectionPopover = ({
<Popover
className="detected-token-selection-popover"
title={
detectedTokens.length === 1
totalTokens === 1
? t('tokenFoundTitle')
: t('tokensFoundTitle', [detectedTokens.length])
: t('tokensFoundTitle', [totalTokens])
}
onClose={onClose}
footer={footer}
>
<Box margin={3}>
{detectedTokens.map((token, index) => {
return (
<DetectedTokenDetails
key={index}
token={token}
handleTokenSelection={handleTokenSelection}
tokensListDetected={tokensListDetected}
/>
);
})}
</Box>
{process.env.PORTFOLIO_VIEW && !allNetworksFilterShown ? (
<Box margin={3}>
{Object.entries(detectedTokensMultichain).map(
([networkId, tokens]) => {
return tokens.map((token, index) => (
<DetectedTokenDetails
key={`${networkId}-${index}`}
token={token}
chainId={networkId}
handleTokenSelection={handleTokenSelection}
tokensListDetected={tokensListDetected}
/>
));
},
)}
</Box>
) : (
<Box margin={3}>
{detectedTokens.map((token, index) => {
return (
<DetectedTokenDetails
key={index}
token={token}
handleTokenSelection={handleTokenSelection}
tokensListDetected={tokensListDetected}
chainId={currentNetwork.chainId}
/>
);
})}
</Box>
)}
</Popover>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const store = configureStore({
...testData,
metamask: {
...testData.metamask,
currencyRates: {
SepoliaETH: {
conversionRate: 3910.28,
},
},
...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }),
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import {
TextColor,
TextVariant,
} from '../../../../helpers/constants/design-system';
import { useTokenTracker } from '../../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
import { getUseCurrencyRateCheck } from '../../../../selectors';
import {
getCurrentChainId,
getSelectedAddress,
getUseCurrencyRateCheck,
} from '../../../../selectors';
import { Box, Checkbox, Text } from '../../../component-library';
import { useTokenTracker } from '../../../../hooks/useTokenBalances';

const DetectedTokenValues = ({
token,
Expand All @@ -21,12 +25,25 @@ const DetectedTokenValues = ({
return tokensListDetected[token.address]?.selected;
});

const { tokensWithBalances } = useTokenTracker({ tokens: [token] });
const selectedAddress = useSelector(getSelectedAddress);
const currentChainId = useSelector(getCurrentChainId);
const chainId = token.chainId ?? currentChainId;

const { tokensWithBalances } = useTokenTracker({
chainId,
tokens: [token],
address: selectedAddress,
hideZeroBalanceTokens: false,
});

const balanceString = tokensWithBalances[0]?.string;
const formattedFiatBalance = useTokenFiatAmount(
token.address,
balanceString,
token.symbol,
{},
false,
chainId,
);

const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
Expand Down Expand Up @@ -73,6 +90,7 @@ DetectedTokenValues.propTypes = {
symbol: PropTypes.string,
iconUrl: PropTypes.string,
aggregators: PropTypes.array,
chainId: PropTypes.string,
}),
handleTokenSelection: PropTypes.func.isRequired,
tokensListDetected: PropTypes.object,
Expand Down
Loading
Loading