Skip to content

Commit

Permalink
feat: multichain token autodetection
Browse files Browse the repository at this point in the history
  • Loading branch information
salimtb committed Nov 21, 2024
1 parent f455a6e commit 8325697
Show file tree
Hide file tree
Showing 20 changed files with 541 additions and 132 deletions.
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

0 comments on commit 8325697

Please sign in to comment.