@metamask-previews/assets-controllers 65.0.0-preview-88a682b → 65.0.0-preview-4f323233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/AccountTrackerController.d.cts +2 -2
- package/dist/AccountTrackerController.d.mts +2 -2
- package/dist/CurrencyRateController.d.cts +2 -2
- package/dist/CurrencyRateController.d.mts +2 -2
- package/dist/DeFiPositionsController/DeFiPositionsController.d.cts +2 -2
- package/dist/DeFiPositionsController/DeFiPositionsController.d.mts +2 -2
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.d.cts +2 -2
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.d.mts +2 -2
- package/dist/NftController.cjs +126 -10
- package/dist/NftController.cjs.map +1 -1
- package/dist/NftController.d.cts +9 -1
- package/dist/NftController.d.cts.map +1 -1
- package/dist/NftController.d.mts +9 -1
- package/dist/NftController.d.mts.map +1 -1
- package/dist/NftController.mjs +126 -10
- package/dist/NftController.mjs.map +1 -1
- package/dist/TokenBalancesController.d.cts +2 -2
- package/dist/TokenBalancesController.d.mts +2 -2
- package/dist/TokenDetectionController.d.cts +2 -2
- package/dist/TokenDetectionController.d.mts +2 -2
- package/dist/TokenListController.d.cts +2 -2
- package/dist/TokenListController.d.mts +2 -2
- package/dist/TokenRatesController.d.cts +2 -2
- package/dist/TokenRatesController.d.mts +2 -2
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Add phishing protection for NFT metadata URLs in `NftController` ([#5598](https://github.com/MetaMask/core/pull/5598))
|
13
|
+
- NFT metadata URLs are now scanned for malicious content using the `PhishingController`
|
14
|
+
- Malicious URLs in NFT metadata fields (image, externalLink, etc.) are automatically sanitized
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
|
18
|
+
- **BREAKING:** Add peer dependency on `@metamask/phishing-controller` ^12.5.0 ([#5598](https://github.com/MetaMask/core/pull/5598))
|
19
|
+
|
10
20
|
## [65.0.0]
|
11
21
|
|
12
22
|
### Added
|
@@ -62,8 +62,8 @@ type AccountTrackerPollingInput = {
|
|
62
62
|
networkClientIds: NetworkClientId[];
|
63
63
|
};
|
64
64
|
declare const AccountTrackerController_base: (abstract new (...args: any[]) => {
|
65
|
-
readonly "__#
|
66
|
-
"__#
|
65
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
66
|
+
"__#14@#intervalLength": number | undefined;
|
67
67
|
setIntervalLength(intervalLength: number): void;
|
68
68
|
getIntervalLength(): number | undefined;
|
69
69
|
_startPolling(input: AccountTrackerPollingInput): void;
|
@@ -62,8 +62,8 @@ type AccountTrackerPollingInput = {
|
|
62
62
|
networkClientIds: NetworkClientId[];
|
63
63
|
};
|
64
64
|
declare const AccountTrackerController_base: (abstract new (...args: any[]) => {
|
65
|
-
readonly "__#
|
66
|
-
"__#
|
65
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
66
|
+
"__#14@#intervalLength": number | undefined;
|
67
67
|
setIntervalLength(intervalLength: number): void;
|
68
68
|
getIntervalLength(): number | undefined;
|
69
69
|
_startPolling(input: AccountTrackerPollingInput): void;
|
@@ -29,8 +29,8 @@ type CurrencyRatePollingInput = {
|
|
29
29
|
nativeCurrencies: string[];
|
30
30
|
};
|
31
31
|
declare const CurrencyRateController_base: (abstract new (...args: any[]) => {
|
32
|
-
readonly "__#
|
33
|
-
"__#
|
32
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
33
|
+
"__#14@#intervalLength": number | undefined;
|
34
34
|
setIntervalLength(intervalLength: number): void;
|
35
35
|
getIntervalLength(): number | undefined;
|
36
36
|
_startPolling(input: CurrencyRatePollingInput): void;
|
@@ -29,8 +29,8 @@ type CurrencyRatePollingInput = {
|
|
29
29
|
nativeCurrencies: string[];
|
30
30
|
};
|
31
31
|
declare const CurrencyRateController_base: (abstract new (...args: any[]) => {
|
32
|
-
readonly "__#
|
33
|
-
"__#
|
32
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
33
|
+
"__#14@#intervalLength": number | undefined;
|
34
34
|
setIntervalLength(intervalLength: number): void;
|
35
35
|
getIntervalLength(): number | undefined;
|
36
36
|
_startPolling(input: CurrencyRatePollingInput): void;
|
@@ -36,8 +36,8 @@ export type AllowedEvents = KeyringControllerUnlockEvent | KeyringControllerLock
|
|
36
36
|
*/
|
37
37
|
export type DeFiPositionsControllerMessenger = RestrictedMessenger<typeof controllerName, DeFiPositionsControllerActions | AllowedActions, DeFiPositionsControllerEvents | AllowedEvents, AllowedActions['type'], AllowedEvents['type']>;
|
38
38
|
declare const DeFiPositionsController_base: (abstract new (...args: any[]) => {
|
39
|
-
readonly "__#
|
40
|
-
"__#
|
39
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
40
|
+
"__#14@#intervalLength": number | undefined;
|
41
41
|
setIntervalLength(intervalLength: number): void;
|
42
42
|
getIntervalLength(): number | undefined;
|
43
43
|
_startPolling(input: import("@metamask/utils").Json): void;
|
@@ -36,8 +36,8 @@ export type AllowedEvents = KeyringControllerUnlockEvent | KeyringControllerLock
|
|
36
36
|
*/
|
37
37
|
export type DeFiPositionsControllerMessenger = RestrictedMessenger<typeof controllerName, DeFiPositionsControllerActions | AllowedActions, DeFiPositionsControllerEvents | AllowedEvents, AllowedActions['type'], AllowedEvents['type']>;
|
38
38
|
declare const DeFiPositionsController_base: (abstract new (...args: any[]) => {
|
39
|
-
readonly "__#
|
40
|
-
"__#
|
39
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
40
|
+
"__#14@#intervalLength": number | undefined;
|
41
41
|
setIntervalLength(intervalLength: number): void;
|
42
42
|
getIntervalLength(): number | undefined;
|
43
43
|
_startPolling(input: import("@metamask/utils").Json): void;
|
@@ -74,8 +74,8 @@ export type MultichainAssetsRatesPollingInput = {
|
|
74
74
|
accountId: string;
|
75
75
|
};
|
76
76
|
declare const MultichainAssetsRatesController_base: (abstract new (...args: any[]) => {
|
77
|
-
readonly "__#
|
78
|
-
"__#
|
77
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
78
|
+
"__#14@#intervalLength": number | undefined;
|
79
79
|
setIntervalLength(intervalLength: number): void;
|
80
80
|
getIntervalLength(): number | undefined;
|
81
81
|
_startPolling(input: MultichainAssetsRatesPollingInput): void;
|
@@ -74,8 +74,8 @@ export type MultichainAssetsRatesPollingInput = {
|
|
74
74
|
accountId: string;
|
75
75
|
};
|
76
76
|
declare const MultichainAssetsRatesController_base: (abstract new (...args: any[]) => {
|
77
|
-
readonly "__#
|
78
|
-
"__#
|
77
|
+
readonly "__#14@#intervalIds": Record<string, NodeJS.Timeout>;
|
78
|
+
"__#14@#intervalLength": number | undefined;
|
79
79
|
setIntervalLength(intervalLength: number): void;
|
80
80
|
getIntervalLength(): number | undefined;
|
81
81
|
_startPolling(input: MultichainAssetsRatesPollingInput): void;
|
package/dist/NftController.cjs
CHANGED
@@ -13,12 +13,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
13
13
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
14
14
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
15
15
|
};
|
16
|
-
var _NftController_instances, _NftController_mutex, _NftController_selectedAccountId, _NftController_chainId, _NftController_ipfsGateway, _NftController_openSeaEnabled, _NftController_useIpfsSubdomains, _NftController_isIpfsGatewayEnabled, _NftController_onNftAdded, _NftController_onNetworkControllerNetworkDidChange, _NftController_onPreferencesControllerStateChange, _NftController_onSelectedAccountChange, _NftController_updateNestedNftState, _NftController_getNftCollectionApi, _NftController_getNftInformationFromApi, _NftController_getNftInformationFromTokenURI, _NftController_getNftURIAndStandard, _NftController_getNftInformation, _NftController_getNftContractInformationFromContract, _NftController_getNftContractInformation, _NftController_addIndividualNft, _NftController_addNftContract, _NftController_removeAndIgnoreIndividualNft, _NftController_removeIndividualNft, _NftController_removeNftContract, _NftController_validateWatchNft, _NftController_getCorrectChainId, _NftController_getAddressOrSelectedAddress, _NftController_updateNftUpdateForAccount;
|
16
|
+
var _NftController_instances, _NftController_mutex, _NftController_selectedAccountId, _NftController_chainId, _NftController_ipfsGateway, _NftController_openSeaEnabled, _NftController_useIpfsSubdomains, _NftController_isIpfsGatewayEnabled, _NftController_onNftAdded, _NftController_onNetworkControllerNetworkDidChange, _NftController_onPreferencesControllerStateChange, _NftController_onSelectedAccountChange, _NftController_updateNestedNftState, _NftController_getNftCollectionApi, _NftController_getNftInformationFromApi, _NftController_getNftInformationFromTokenURI, _NftController_getNftURIAndStandard, _NftController_getNftInformation, _NftController_getNftContractInformationFromContract, _NftController_getNftContractInformation, _NftController_addIndividualNft, _NftController_addNftContract, _NftController_removeAndIgnoreIndividualNft, _NftController_removeIndividualNft, _NftController_removeNftContract, _NftController_validateWatchNft, _NftController_getCorrectChainId, _NftController_getAddressOrSelectedAddress, _NftController_updateNftUpdateForAccount, _NftController_bulkSanitizeNftMetadata, _NftController_sanitizeNftMetadata;
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
18
18
|
exports.NftController = exports.getDefaultNftControllerState = void 0;
|
19
19
|
const address_1 = require("@ethersproject/address");
|
20
20
|
const base_controller_1 = require("@metamask/base-controller");
|
21
21
|
const controller_utils_1 = require("@metamask/controller-utils");
|
22
|
+
const phishing_controller_1 = require("@metamask/phishing-controller");
|
22
23
|
const rpc_errors_1 = require("@metamask/rpc-errors");
|
23
24
|
const utils_1 = require("@metamask/utils");
|
24
25
|
const async_mutex_1 = require("async-mutex");
|
@@ -124,11 +125,13 @@ class NftController extends base_controller_1.BaseController {
|
|
124
125
|
}
|
125
126
|
await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_validateWatchNft).call(this, asset, type, addressToSearch);
|
126
127
|
const nftMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_getNftInformation).call(this, asset.address, asset.tokenId, networkClientId);
|
127
|
-
|
128
|
-
|
128
|
+
// Sanitize metadata
|
129
|
+
const sanitizedMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_sanitizeNftMetadata).call(this, nftMetadata);
|
130
|
+
if (sanitizedMetadata.standard && sanitizedMetadata.standard !== type) {
|
131
|
+
throw rpc_errors_1.rpcErrors.invalidInput(`Suggested NFT of type ${sanitizedMetadata.standard} does not match received type ${type}`);
|
129
132
|
}
|
130
133
|
const suggestedNftMeta = {
|
131
|
-
asset: { ...asset, ...
|
134
|
+
asset: { ...asset, ...sanitizedMetadata },
|
132
135
|
type,
|
133
136
|
id: (0, uuid_1.v4)(),
|
134
137
|
time: Date.now(),
|
@@ -137,7 +140,7 @@ class NftController extends base_controller_1.BaseController {
|
|
137
140
|
};
|
138
141
|
await this._requestApproval(suggestedNftMeta);
|
139
142
|
const { address, tokenId } = asset;
|
140
|
-
const { name, standard, description, image } =
|
143
|
+
const { name, standard, description, image } = sanitizedMetadata;
|
141
144
|
await this.addNft(address, tokenId, {
|
142
145
|
nftMetadata: {
|
143
146
|
name: name ?? null,
|
@@ -234,9 +237,15 @@ class NftController extends base_controller_1.BaseController {
|
|
234
237
|
const checksumHexAddress = (0, controller_utils_1.toChecksumHexAddress)(tokenAddress);
|
235
238
|
// TODO: revisit this with Solana support and instead of passing chainId, make sure chainId is read from nftMetadata
|
236
239
|
const chainIdToAddTo = chainId || __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_getCorrectChainId).call(this, { networkClientId });
|
237
|
-
nftMetadata
|
238
|
-
|
239
|
-
|
240
|
+
if (!nftMetadata) {
|
241
|
+
const fetchedMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_getNftInformation).call(this, checksumHexAddress, tokenId, networkClientId);
|
242
|
+
// Sanitize metadata
|
243
|
+
nftMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_sanitizeNftMetadata).call(this, fetchedMetadata);
|
244
|
+
}
|
245
|
+
else {
|
246
|
+
// Sanitize provided metadata
|
247
|
+
nftMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_sanitizeNftMetadata).call(this, nftMetadata);
|
248
|
+
}
|
240
249
|
const newNftContracts = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_addNftContract).call(this, {
|
241
250
|
tokenAddress: checksumHexAddress,
|
242
251
|
userAddress: addressToSearch,
|
@@ -275,13 +284,23 @@ class NftController extends base_controller_1.BaseController {
|
|
275
284
|
address: (0, controller_utils_1.toChecksumHexAddress)(nft.address),
|
276
285
|
};
|
277
286
|
});
|
278
|
-
|
287
|
+
// Get all unsanitized nft metadata
|
288
|
+
const unsanitizedResults = await Promise.all(nftsWithChecksumAdr.map(async (nft) => {
|
279
289
|
const resMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_getNftInformation).call(this, nft.address, nft.tokenId, networkClientId);
|
280
290
|
return {
|
281
291
|
nft,
|
282
292
|
newMetadata: resMetadata,
|
283
293
|
};
|
284
294
|
}));
|
295
|
+
// Extract metadata
|
296
|
+
const unsanitizedMetadata = unsanitizedResults.map((result) => result.newMetadata);
|
297
|
+
// Sanitize all metadata
|
298
|
+
const sanitizedMetadata = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_bulkSanitizeNftMetadata).call(this, unsanitizedMetadata);
|
299
|
+
// Reassemble the results with sanitized metadata
|
300
|
+
const nftMetadataResults = unsanitizedResults.map((result, index) => ({
|
301
|
+
nft: result.nft,
|
302
|
+
newMetadata: sanitizedMetadata[index],
|
303
|
+
}));
|
285
304
|
// We want to avoid updating the state if the state and fetched nft info are the same
|
286
305
|
const nftsWithDifferentMetadata = [];
|
287
306
|
const { allNfts } = this.state;
|
@@ -857,7 +876,7 @@ async function _NftController_getNftInformation(contractAddress, tokenId, networ
|
|
857
876
|
? (0, controller_utils_1.safelyExecute)(() => __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_getNftInformationFromApi).call(this, contractAddress, tokenId))
|
858
877
|
: undefined,
|
859
878
|
]);
|
860
|
-
|
879
|
+
const metadata = {
|
861
880
|
...nftApiMetadata,
|
862
881
|
name: blockchainMetadata?.name ?? nftApiMetadata?.name ?? null,
|
863
882
|
description: blockchainMetadata?.description ?? nftApiMetadata?.description ?? null,
|
@@ -865,6 +884,8 @@ async function _NftController_getNftInformation(contractAddress, tokenId, networ
|
|
865
884
|
standard: blockchainMetadata?.standard ?? nftApiMetadata?.standard ?? null,
|
866
885
|
tokenURI: blockchainMetadata?.tokenURI ?? null,
|
867
886
|
};
|
887
|
+
// Sanitize the metadata by checking external links against phishing protection
|
888
|
+
return await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_sanitizeNftMetadata).call(this, metadata);
|
868
889
|
}, _NftController_getNftContractInformationFromContract =
|
869
890
|
/**
|
870
891
|
* Request NFT contract information from the contract itself.
|
@@ -1176,6 +1197,101 @@ async function _NftController_addNftContract({ tokenAddress, userAddress, networ
|
|
1176
1197
|
userAddress: account.address,
|
1177
1198
|
});
|
1178
1199
|
}
|
1200
|
+
}, _NftController_bulkSanitizeNftMetadata =
|
1201
|
+
/**
|
1202
|
+
* Sanitizes multiple NFT metadata objects by checking external links against PhishingController in a single bulk request
|
1203
|
+
*
|
1204
|
+
* @param metadataList - Array of NFT metadata objects to sanitize
|
1205
|
+
* @returns Array of sanitized NFT metadata objects
|
1206
|
+
*/
|
1207
|
+
async function _NftController_bulkSanitizeNftMetadata(metadataList) {
|
1208
|
+
// Create a copy of the metadata list to avoid mutating the input
|
1209
|
+
const sanitizedMetadataList = metadataList.map((metadata) => ({
|
1210
|
+
...metadata,
|
1211
|
+
}));
|
1212
|
+
// Maps URL to a list of {metadataIndex, fieldName} to track where each URL is used
|
1213
|
+
const urlMap = {};
|
1214
|
+
const fieldsToCheck = [
|
1215
|
+
'externalLink',
|
1216
|
+
'image',
|
1217
|
+
'imagePreview',
|
1218
|
+
'imageThumbnail',
|
1219
|
+
'imageOriginal',
|
1220
|
+
'animation',
|
1221
|
+
'animationOriginal',
|
1222
|
+
];
|
1223
|
+
// Collect all URLs from all metadata objects
|
1224
|
+
sanitizedMetadataList.forEach((metadata, metadataIndex) => {
|
1225
|
+
// Check regular fields
|
1226
|
+
for (const field of fieldsToCheck) {
|
1227
|
+
const url = metadata[field];
|
1228
|
+
if (typeof url === 'string' && url && url.startsWith('http')) {
|
1229
|
+
if (!urlMap[url]) {
|
1230
|
+
urlMap[url] = [];
|
1231
|
+
}
|
1232
|
+
urlMap[url].push({ metadataIndex, fieldName: field });
|
1233
|
+
}
|
1234
|
+
}
|
1235
|
+
// Check collection links if they exist
|
1236
|
+
if (metadata.collection) {
|
1237
|
+
const { collection } = metadata;
|
1238
|
+
if ('externalLink' in collection &&
|
1239
|
+
typeof collection.externalLink === 'string') {
|
1240
|
+
const url = collection.externalLink;
|
1241
|
+
if (!urlMap[url]) {
|
1242
|
+
urlMap[url] = [];
|
1243
|
+
}
|
1244
|
+
urlMap[url].push({
|
1245
|
+
metadataIndex,
|
1246
|
+
fieldName: 'collection.externalLink',
|
1247
|
+
});
|
1248
|
+
}
|
1249
|
+
}
|
1250
|
+
});
|
1251
|
+
const urlsToCheck = Object.keys(urlMap);
|
1252
|
+
if (urlsToCheck.length === 0) {
|
1253
|
+
return sanitizedMetadataList;
|
1254
|
+
}
|
1255
|
+
try {
|
1256
|
+
// Use bulkScanUrls to check all URLs at once
|
1257
|
+
const bulkScanResponse = await this.messagingSystem.call('PhishingController:bulkScanUrls', urlsToCheck);
|
1258
|
+
// Apply scan results to all metadata objects
|
1259
|
+
Object.entries(bulkScanResponse.results).forEach(([url, result]) => {
|
1260
|
+
if (result.recommendedAction === phishing_controller_1.RecommendedAction.Block) {
|
1261
|
+
// Remove this URL from all metadata objects where it appears
|
1262
|
+
urlMap[url].forEach(({ metadataIndex, fieldName }) => {
|
1263
|
+
if (fieldName === 'collection.externalLink' &&
|
1264
|
+
sanitizedMetadataList[metadataIndex].collection // Check if collection exists
|
1265
|
+
) {
|
1266
|
+
const { collection } = sanitizedMetadataList[metadataIndex];
|
1267
|
+
// Ensure collection is not undefined again just to be safe before using 'in'
|
1268
|
+
if (collection && 'externalLink' in collection) {
|
1269
|
+
delete collection.externalLink;
|
1270
|
+
}
|
1271
|
+
}
|
1272
|
+
else {
|
1273
|
+
delete sanitizedMetadataList[metadataIndex][fieldName];
|
1274
|
+
}
|
1275
|
+
});
|
1276
|
+
}
|
1277
|
+
});
|
1278
|
+
}
|
1279
|
+
catch (error) {
|
1280
|
+
console.error('Error during bulk URL scanning:', error);
|
1281
|
+
// If bulk scan fails, we fall back to keeping all URLs
|
1282
|
+
}
|
1283
|
+
return sanitizedMetadataList;
|
1284
|
+
}, _NftController_sanitizeNftMetadata =
|
1285
|
+
/**
|
1286
|
+
* Sanitizes NFT metadata by checking external links against PhishingController
|
1287
|
+
*
|
1288
|
+
* @param metadata - The NFT metadata to sanitize
|
1289
|
+
* @returns Sanitized NFT metadata with potentially dangerous links removed
|
1290
|
+
*/
|
1291
|
+
async function _NftController_sanitizeNftMetadata(metadata) {
|
1292
|
+
// Use the bulk sanitize function with just a single metadata object
|
1293
|
+
const sanitized = await __classPrivateFieldGet(this, _NftController_instances, "m", _NftController_bulkSanitizeNftMetadata).call(this, [metadata]);
|
1294
|
+
return sanitized[0];
|
1179
1295
|
};
|
1180
1296
|
exports.default = NftController;
|
1181
1297
|
//# sourceMappingURL=NftController.cjs.map
|