Skip to content

Commit b963d20

Browse files
committed
Merge branch 'salim/token-autodetection-multi-chain' into salim/multichain-token-detection-final
2 parents 1a8cb68 + ef4f2aa commit b963d20

20 files changed

+574
-132
lines changed

.storybook/test-data.js

+7
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,13 @@ const state = {
525525
decimals: 18,
526526
},
527527
],
528+
tokenBalances: {
529+
'0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': {
530+
'0x1': {
531+
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x25e4bc',
532+
},
533+
},
534+
},
528535
allDetectedTokens: {
529536
'0xaa36a7': {
530537
'0x9d0ba4ddac06032527b140912ec808ab9451b788': [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
diff --git a/dist/assetsUtil.cjs b/dist/assetsUtil.cjs
2+
index 48571b8c1b78e94d88e1837e986b5f8735ac651b..61246f51500c8cab48f18296a73629fb73454caa 100644
3+
--- a/dist/assetsUtil.cjs
4+
+++ b/dist/assetsUtil.cjs
5+
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6+
return (mod && mod.__esModule) ? mod : { "default": mod };
7+
};
8+
Object.defineProperty(exports, "__esModule", { value: true });
9+
+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; } }
10+
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;
11+
const controller_utils_1 = require("@metamask/controller-utils");
12+
const utils_1 = require("@metamask/utils");
13+
@@ -233,7 +234,7 @@ async function getIpfsCIDv1AndPath(ipfsUrl) {
14+
const index = url.indexOf('/');
15+
const cid = index !== -1 ? url.substring(0, index) : url;
16+
const path = index !== -1 ? url.substring(index) : undefined;
17+
- const { CID } = await import("multiformats");
18+
+ const { CID } = _interopRequireWildcard(require("multiformats"));
19+
// We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats)
20+
// because most cid v0s appear to be incompatible with IPFS subdomains
21+
return {
22+
diff --git a/dist/token-prices-service/codefi-v2.mjs b/dist/token-prices-service/codefi-v2.mjs
23+
index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..bf8ec7819f678c2f185d6a85d7e3ea81f055a309 100644
24+
--- a/dist/token-prices-service/codefi-v2.mjs
25+
+++ b/dist/token-prices-service/codefi-v2.mjs
26+
@@ -12,8 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
27+
var _CodefiTokenPricesServiceV2_tokenPricePolicy;
28+
import { handleFetch } from "@metamask/controller-utils";
29+
import { hexToNumber } from "@metamask/utils";
30+
-import $cockatiel from "cockatiel";
31+
-const { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } = $cockatiel;
32+
+import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel"
33+
/**
34+
* The list of currencies that can be supplied as the `vsCurrency` parameter to
35+
* the `/spot-prices` endpoint, in lowercase form.
36+
diff --git a/dist/TokenDetectionController.cjs b/dist/TokenDetectionController.cjs
37+
index 8fd5efde7a3c24080f8a43f79d10300e8c271245..a3c334ac7dd2e5698e6b54a73491b7145c2a9010 100644
38+
--- a/dist/TokenDetectionController.cjs
39+
+++ b/dist/TokenDetectionController.cjs
40+
@@ -250,17 +250,20 @@ _TokenDetectionController_intervalId = new WeakMap(), _TokenDetectionController_
41+
}
42+
});
43+
this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange',
44+
- // TODO: Either fix this lint violation or explain why it's necessary to ignore.
45+
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
46+
- async (selectedAccount) => {
47+
- const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id;
48+
- if (isSelectedAccountIdChanged) {
49+
- __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f");
50+
- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, {
51+
- selectedAddress: selectedAccount.address,
52+
- });
53+
- }
54+
- });
55+
+ // TODO: Either fix this lint violation or explain why it's necessary to ignore.
56+
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
57+
+ async (selectedAccount) => {
58+
+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
59+
+ const chainIds = Object.keys(networkConfigurationsByChainId);
60+
+ const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id;
61+
+ if (isSelectedAccountIdChanged) {
62+
+ __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f");
63+
+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, {
64+
+ selectedAddress: selectedAccount.address,
65+
+ chainIds,
66+
+ });
67+
+ }
68+
+ });
69+
}, _TokenDetectionController_stopPolling = function _TokenDetectionController_stopPolling() {
70+
if (__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")) {
71+
clearInterval(__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f"));

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@
293293
"@metamask/address-book-controller": "^6.0.0",
294294
"@metamask/announcement-controller": "^7.0.0",
295295
"@metamask/approval-controller": "^7.0.0",
296-
"@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",
296+
"@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A44.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-44.1.0-012aa448d8.patch",
297297
"@metamask/base-controller": "^7.0.0",
298298
"@metamask/bitcoin-wallet-snap": "^0.8.2",
299299
"@metamask/browser-passworder": "^4.3.0",

ui/components/app/assets/asset-list/asset-list.tsx

+36-8
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import TokenList from '../token-list';
44
import { PRIMARY } from '../../../../helpers/constants/common';
55
import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency';
66
import {
7+
getAllDetectedTokensForSelectedAddress,
78
getDetectedTokensInCurrentNetwork,
89
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
10+
getNetworkConfigurationsByChainId,
11+
getPreferences,
912
getSelectedAccount,
1013
} from '../../../../selectors';
1114
import {
@@ -76,6 +79,17 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
7679
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
7780
);
7881

82+
const allNetworks = useSelector(getNetworkConfigurationsByChainId);
83+
const { tokenNetworkFilter } = useSelector(getPreferences);
84+
const allOpts: Record<string, boolean> = {};
85+
Object.keys(allNetworks || {}).forEach((chainId) => {
86+
allOpts[chainId] = true;
87+
});
88+
89+
const allNetworksFilterShown =
90+
Object.keys(tokenNetworkFilter || {}).length !==
91+
Object.keys(allOpts || {}).length;
92+
7993
const [showFundingMethodModal, setShowFundingMethodModal] = useState(false);
8094
const [showReceiveModal, setShowReceiveModal] = useState(false);
8195

@@ -98,16 +112,30 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
98112
// for EVM assets
99113
const shouldShowTokensLinks = showTokensLinks ?? isEvm;
100114

115+
const detectedTokensMultichain = useSelector(
116+
getAllDetectedTokensForSelectedAddress,
117+
);
118+
119+
const totalTokens =
120+
process.env.PORTFOLIO_VIEW && !allNetworksFilterShown
121+
? (Object.values(detectedTokensMultichain).reduce(
122+
// @ts-expect-error TS18046: 'tokenArray' is of type 'unknown'
123+
(count, tokenArray) => count + tokenArray.length,
124+
0,
125+
) as number)
126+
: detectedTokens.length;
127+
101128
return (
102129
<>
103-
{detectedTokens.length > 0 &&
104-
!isTokenDetectionInactiveOnNonMainnetSupportedNetwork && (
105-
<DetectedTokensBanner
106-
className=""
107-
actionButtonOnClick={() => setShowDetectedTokens(true)}
108-
margin={4}
109-
/>
110-
)}
130+
{totalTokens &&
131+
totalTokens > 0 &&
132+
!isTokenDetectionInactiveOnNonMainnetSupportedNetwork ? (
133+
<DetectedTokensBanner
134+
className=""
135+
actionButtonOnClick={() => setShowDetectedTokens(true)}
136+
margin={4}
137+
/>
138+
) : null}
111139
<AssetListControlBar showTokensLinks={shouldShowTokensLinks} />
112140
<TokenList
113141
// nativeToken is still needed to avoid breaking flask build's support for bitcoin

ui/components/app/detected-token/detected-token-details/detected-token-details.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ import DetectedTokenAddress from '../detected-token-address/detected-token-addre
1414
import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators';
1515
import { Display } from '../../../../helpers/constants/design-system';
1616
import {
17-
getCurrentNetwork,
1817
getTestNetworkBackgroundColor,
1918
getTokenList,
2019
} from '../../../../selectors';
20+
import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network';
2121

2222
const DetectedTokenDetails = ({
2323
token,
2424
handleTokenSelection,
2525
tokensListDetected,
26+
chainId,
2627
}) => {
2728
const tokenList = useSelector(getTokenList);
2829
const tokenData = tokenList[token.address?.toLowerCase()];
2930
const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor);
30-
const currentNetwork = useSelector(getCurrentNetwork);
3131

3232
return (
3333
<Box
@@ -39,8 +39,7 @@ const DetectedTokenDetails = ({
3939
badge={
4040
<AvatarNetwork
4141
size={AvatarNetworkSize.Xs}
42-
name={currentNetwork?.nickname || ''}
43-
src={currentNetwork?.rpcPrefs?.imageUrl}
42+
src={CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[chainId]}
4443
backgroundColor={testNetworkBackgroundColor}
4544
/>
4645
}
@@ -84,6 +83,7 @@ DetectedTokenDetails.propTypes = {
8483
}),
8584
handleTokenSelection: PropTypes.func.isRequired,
8685
tokensListDetected: PropTypes.object,
86+
chainId: PropTypes.string,
8787
};
8888

8989
export default DetectedTokenDetails;

ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js

+69-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useMemo } from 'react';
22
import PropTypes from 'prop-types';
33
import { useSelector } from 'react-redux';
44

@@ -10,8 +10,12 @@ import {
1010
MetaMetricsTokenEventSource,
1111
} from '../../../../../shared/constants/metametrics';
1212
import {
13+
getAllDetectedTokensForSelectedAddress,
1314
getCurrentChainId,
15+
getCurrentNetwork,
1416
getDetectedTokensInCurrentNetwork,
17+
getNetworkConfigurationsByChainId,
18+
getPreferences,
1519
} from '../../../../selectors';
1620

1721
import Popover from '../../../ui/popover';
@@ -34,10 +38,38 @@ const DetectedTokenSelectionPopover = ({
3438
const chainId = useSelector(getCurrentChainId);
3539

3640
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
41+
const allNetworks = useSelector(getNetworkConfigurationsByChainId);
42+
const { tokenNetworkFilter } = useSelector(getPreferences);
43+
const allOpts = {};
44+
Object.keys(allNetworks || {}).forEach((networkId) => {
45+
allOpts[networkId] = true;
46+
});
47+
48+
const allNetworksFilterShown =
49+
Object.keys(tokenNetworkFilter || {}).length !==
50+
Object.keys(allOpts || {}).length;
51+
52+
const currentNetwork = useSelector(getCurrentNetwork);
53+
54+
const detectedTokensMultichain = useSelector(
55+
getAllDetectedTokensForSelectedAddress,
56+
);
57+
58+
const totalTokens = useMemo(() => {
59+
return process.env.PORTFOLIO_VIEW && !allNetworksFilterShown
60+
? Object.values(detectedTokensMultichain).reduce(
61+
(count, tokenArray) => count + tokenArray.length,
62+
0,
63+
)
64+
: detectedTokens.length;
65+
}, [detectedTokensMultichain, detectedTokens, allNetworksFilterShown]);
66+
3767
const { selected: selectedTokens = [] } =
3868
sortingBasedOnTokenSelection(tokensListDetected);
3969

4070
const onClose = () => {
71+
const chainIds = Object.keys(detectedTokensMultichain);
72+
4173
setShowDetectedTokens(false);
4274
const eventTokensDetails = detectedTokens.map(
4375
({ address, symbol }) => `${symbol} - ${address}`,
@@ -47,8 +79,10 @@ const DetectedTokenSelectionPopover = ({
4779
category: MetaMetricsEventCategory.Wallet,
4880
properties: {
4981
source_connection_method: MetaMetricsTokenEventSource.Detected,
50-
chain_id: chainId,
5182
tokens: eventTokensDetails,
83+
...(process.env.PORTFOLIO_VIEW
84+
? { chain_ids: chainIds }
85+
: { chain_id: chainId }),
5286
},
5387
});
5488
};
@@ -81,25 +115,44 @@ const DetectedTokenSelectionPopover = ({
81115
<Popover
82116
className="detected-token-selection-popover"
83117
title={
84-
detectedTokens.length === 1
118+
totalTokens === 1
85119
? t('tokenFoundTitle')
86-
: t('tokensFoundTitle', [detectedTokens.length])
120+
: t('tokensFoundTitle', [totalTokens])
87121
}
88122
onClose={onClose}
89123
footer={footer}
90124
>
91-
<Box margin={3}>
92-
{detectedTokens.map((token, index) => {
93-
return (
94-
<DetectedTokenDetails
95-
key={index}
96-
token={token}
97-
handleTokenSelection={handleTokenSelection}
98-
tokensListDetected={tokensListDetected}
99-
/>
100-
);
101-
})}
102-
</Box>
125+
{process.env.PORTFOLIO_VIEW && !allNetworksFilterShown ? (
126+
<Box margin={3}>
127+
{Object.entries(detectedTokensMultichain).map(
128+
([networkId, tokens]) => {
129+
return tokens.map((token, index) => (
130+
<DetectedTokenDetails
131+
key={`${networkId}-${index}`}
132+
token={token}
133+
chainId={networkId}
134+
handleTokenSelection={handleTokenSelection}
135+
tokensListDetected={tokensListDetected}
136+
/>
137+
));
138+
},
139+
)}
140+
</Box>
141+
) : (
142+
<Box margin={3}>
143+
{detectedTokens.map((token, index) => {
144+
return (
145+
<DetectedTokenDetails
146+
key={index}
147+
token={token}
148+
handleTokenSelection={handleTokenSelection}
149+
tokensListDetected={tokensListDetected}
150+
chainId={currentNetwork.chainId}
151+
/>
152+
);
153+
})}
154+
</Box>
155+
)}
103156
</Popover>
104157
);
105158
};

ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.stories.js

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const store = configureStore({
1111
...testData,
1212
metamask: {
1313
...testData.metamask,
14+
currencyRates: {
15+
SepoliaETH: {
16+
conversionRate: 3910.28,
17+
},
18+
},
1419
...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }),
1520
},
1621
});

ui/components/app/detected-token/detected-token-values/detected-token-values.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import {
77
TextColor,
88
TextVariant,
99
} from '../../../../helpers/constants/design-system';
10-
import { useTokenTracker } from '../../../../hooks/useTokenTracker';
1110
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
12-
import { getUseCurrencyRateCheck } from '../../../../selectors';
11+
import {
12+
getCurrentChainId,
13+
getSelectedAddress,
14+
getUseCurrencyRateCheck,
15+
} from '../../../../selectors';
1316
import { Box, Checkbox, Text } from '../../../component-library';
17+
import { useTokenTracker } from '../../../../hooks/useTokenBalances';
1418

1519
const DetectedTokenValues = ({
1620
token,
@@ -21,12 +25,25 @@ const DetectedTokenValues = ({
2125
return tokensListDetected[token.address]?.selected;
2226
});
2327

24-
const { tokensWithBalances } = useTokenTracker({ tokens: [token] });
28+
const selectedAddress = useSelector(getSelectedAddress);
29+
const currentChainId = useSelector(getCurrentChainId);
30+
const chainId = token.chainId ?? currentChainId;
31+
32+
const { tokensWithBalances } = useTokenTracker({
33+
chainId,
34+
tokens: [token],
35+
address: selectedAddress,
36+
hideZeroBalanceTokens: false,
37+
});
38+
2539
const balanceString = tokensWithBalances[0]?.string;
2640
const formattedFiatBalance = useTokenFiatAmount(
2741
token.address,
2842
balanceString,
2943
token.symbol,
44+
{},
45+
false,
46+
chainId,
3047
);
3148

3249
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
@@ -73,6 +90,7 @@ DetectedTokenValues.propTypes = {
7390
symbol: PropTypes.string,
7491
iconUrl: PropTypes.string,
7592
aggregators: PropTypes.array,
93+
chainId: PropTypes.string,
7694
}),
7795
handleTokenSelection: PropTypes.func.isRequired,
7896
tokensListDetected: PropTypes.object,

0 commit comments

Comments
 (0)