@metamask-previews/phishing-controller 17.0.0-preview-17c76be24 → 17.0.0-preview-3ce0062c4

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 CHANGED
@@ -7,29 +7,11 @@ 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
- - Expose missing public `PhishingController` methods through its messenger ([#8269](https://github.com/MetaMask/core/pull/8269))
13
- - The following actions are now available:
14
- - `PhishingController:bypass`
15
- - `PhishingController:isBlockedRequest`
16
- - `PhishingController:scanUrl`
17
- - Corresponding action types (e.g. `PhishingControllerBypassAction`) are available as well.
18
-
19
10
  ### Changed
20
11
 
21
- - Deprecate `test` method in favor of `testOrigin` ([#8269](https://github.com/MetaMask/core/pull/8269))
22
- - The `test` method is now renamed to `testOrigin` to better reflect its purpose of testing a domain origin for phishing.
23
- - The old `test` method is still present but is now marked as deprecated and will be removed in a future release.
24
- - Deprecate action types in favor of `PhishingController...Action` types ([#8269](https://github.com/MetaMask/core/pull/8269))
25
- - The following action types have been renamed:
26
- - `TestOrigin` is now `PhishingControllerTestOriginAction`.
27
- - `MaybeUpdateState` is now `PhishingControllerMaybeUpdateStateAction`.
28
- - The old types are still exported but are now marked as deprecated and will
29
- be removed in a future release.
30
12
  - `PhishingController` no longer advances `c2DomainBlocklistLastFetched` when the C2 domain blocklist fetch fails, allowing the blocklist to be retried on the next update cycle ([#8250](https://github.com/MetaMask/core/pull/8250))
31
13
  - Reduce default cache TTL for `DEFAULT_URL_SCAN_CACHE_TTL`, `DEFAULT_TOKEN_SCAN_CACHE_TTL`, and `DEFAULT_ADDRESS_SCAN_CACHE_TTL` from 15 minutes to 1 minute ([#8254](https://github.com/MetaMask/core/pull/8254))
32
- - Bump `@metamask/transaction-controller` from `^63.0.0` to `^63.2.0` ([#8272](https://github.com/MetaMask/core/pull/8272), [#8301](https://github.com/MetaMask/core/pull/8301))
14
+ - Bump `@metamask/transaction-controller` from `^63.0.0` to `^63.1.0` ([#8272](https://github.com/MetaMask/core/pull/8272))
33
15
 
34
16
  ## [17.0.0]
35
17
 
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _PhishingController_instances, _PhishingController_detector, _PhishingController_stalelistRefreshInterval, _PhishingController_hotlistRefreshInterval, _PhishingController_c2DomainBlocklistRefreshInterval, _PhishingController_urlScanCache, _PhishingController_tokenScanCache, _PhishingController_addressScanCache, _PhishingController_inProgressHotlistUpdate, _PhishingController_inProgressStalelistUpdate, _PhishingController_isProgressC2DomainBlocklistUpdate, _PhishingController_transactionControllerStateChangeHandler, _PhishingController_subscribeToTransactionControllerStateChange, _PhishingController_isTransactionPatch, _PhishingController_isSimulationDataPatch, _PhishingController_onTransactionControllerStateChange, _PhishingController_getTokensFromTransaction, _PhishingController_scanTokensByChain, _PhishingController_fetchTokenScanBulkResults, _PhishingController_processBatch, _PhishingController_updateStalelist, _PhishingController_updateHotlist, _PhishingController_updateC2DomainBlocklist, _PhishingController_queryConfig;
13
+ var _PhishingController_instances, _PhishingController_detector, _PhishingController_stalelistRefreshInterval, _PhishingController_hotlistRefreshInterval, _PhishingController_c2DomainBlocklistRefreshInterval, _PhishingController_urlScanCache, _PhishingController_tokenScanCache, _PhishingController_addressScanCache, _PhishingController_inProgressHotlistUpdate, _PhishingController_inProgressStalelistUpdate, _PhishingController_isProgressC2DomainBlocklistUpdate, _PhishingController_transactionControllerStateChangeHandler, _PhishingController_subscribeToTransactionControllerStateChange, _PhishingController_registerMessageHandlers, _PhishingController_isTransactionPatch, _PhishingController_isSimulationDataPatch, _PhishingController_onTransactionControllerStateChange, _PhishingController_getTokensFromTransaction, _PhishingController_scanTokensByChain, _PhishingController_fetchTokenScanBulkResults, _PhishingController_processBatch, _PhishingController_updateStalelist, _PhishingController_updateHotlist, _PhishingController_updateC2DomainBlocklist, _PhishingController_queryConfig;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.PhishingController = exports.phishingListKeyNameMap = exports.ListNames = exports.ListKeys = exports.C2_DOMAIN_BLOCKLIST_URL = exports.METAMASK_HOTLIST_DIFF_URL = exports.METAMASK_STALELIST_URL = exports.STALELIST_REFRESH_INTERVAL = exports.HOTLIST_REFRESH_INTERVAL = exports.C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL = exports.DEFAULT_ADDRESS_SCAN_CACHE_MAX_SIZE = exports.DEFAULT_ADDRESS_SCAN_CACHE_TTL = exports.DEFAULT_TOKEN_SCAN_CACHE_MAX_SIZE = exports.DEFAULT_TOKEN_SCAN_CACHE_TTL = exports.DEFAULT_URL_SCAN_CACHE_MAX_SIZE = exports.DEFAULT_URL_SCAN_CACHE_TTL = exports.ADDRESS_SCAN_ENDPOINT = exports.TOKEN_BULK_SCANNING_ENDPOINT = exports.SECURITY_ALERTS_BASE_URL = exports.PHISHING_DETECTION_BULK_SCAN_ENDPOINT = exports.PHISHING_DETECTION_SCAN_ENDPOINT = exports.PHISHING_DETECTION_BASE_URL = exports.C2_DOMAIN_BLOCKLIST_ENDPOINT = exports.CLIENT_SIDE_DETECION_BASE_URL = exports.METAMASK_HOTLIST_DIFF_FILE = exports.METAMASK_STALELIST_FILE = exports.PHISHING_CONFIG_BASE_URL = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
@@ -149,16 +149,6 @@ const getDefaultState = () => {
149
149
  addressScanCache: {},
150
150
  };
151
151
  };
152
- const MESSENGER_EXPOSED_METHODS = [
153
- 'maybeUpdateState',
154
- 'testOrigin',
155
- 'isBlockedRequest',
156
- 'bypass',
157
- 'scanUrl',
158
- 'bulkScanUrls',
159
- 'bulkScanTokens',
160
- 'scanAddress',
161
- ];
162
152
  /**
163
153
  * Controller that manages community-maintained lists of approved and unapproved website origins.
164
154
  */
@@ -204,17 +194,151 @@ class PhishingController extends base_controller_1.BaseController {
204
194
  _PhishingController_isProgressC2DomainBlocklistUpdate.set(this, void 0);
205
195
  _PhishingController_transactionControllerStateChangeHandler.set(this, void 0);
206
196
  /**
207
- * Determines if a given origin is unapproved.
197
+ * Scan a URL for phishing. It will only scan the hostname of the URL. It also only supports
198
+ * web URLs.
208
199
  *
209
- * It is strongly recommended that you call {@link maybeUpdateState} before calling this,
210
- * to check whether the phishing configuration is up-to-date. It will be updated if necessary
211
- * by calling {@link updateStalelist} or {@link updateHotlist}.
200
+ * @param url - The URL to scan.
201
+ * @returns The phishing detection scan result.
202
+ */
203
+ this.scanUrl = async (url) => {
204
+ const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
205
+ if (!ok) {
206
+ return {
207
+ hostname: '',
208
+ recommendedAction: types_1.RecommendedAction.None,
209
+ fetchError: 'url is not a valid web URL',
210
+ };
211
+ }
212
+ const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
213
+ if (cachedResult) {
214
+ return cachedResult;
215
+ }
216
+ const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
217
+ const res = await fetch(`${exports.PHISHING_DETECTION_BASE_URL}/${exports.PHISHING_DETECTION_SCAN_ENDPOINT}?url=${encodeURIComponent(hostname)}`, {
218
+ method: 'GET',
219
+ headers: {
220
+ Accept: 'application/json',
221
+ },
222
+ });
223
+ if (!res.ok) {
224
+ return {
225
+ error: `${res.status} ${res.statusText}`,
226
+ };
227
+ }
228
+ const data = await res.json();
229
+ return data;
230
+ }, true, 8000);
231
+ // Need to do it this way because safelyExecuteWithTimeout returns undefined for both timeouts and errors.
232
+ if (!apiResponse) {
233
+ return {
234
+ hostname: '',
235
+ recommendedAction: types_1.RecommendedAction.None,
236
+ fetchError: 'timeout of 8000ms exceeded',
237
+ };
238
+ }
239
+ else if ('error' in apiResponse) {
240
+ return {
241
+ hostname: '',
242
+ recommendedAction: types_1.RecommendedAction.None,
243
+ fetchError: apiResponse.error,
244
+ };
245
+ }
246
+ const result = {
247
+ hostname,
248
+ recommendedAction: apiResponse.recommendedAction,
249
+ };
250
+ __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
251
+ return result;
252
+ };
253
+ /**
254
+ * Scan multiple URLs for phishing in bulk. It will only scan the hostnames of the URLs.
255
+ * It also only supports web URLs.
212
256
  *
213
- * @param origin - Domain origin of a website.
214
- * @returns Whether the origin is an unapproved origin.
215
- * @deprecated Use {@link testOrigin} instead. This method is exposed for backward compatibility and will be removed in a future release.
257
+ * @param urls - The URLs to scan.
258
+ * @returns A mapping of URLs to their phishing detection scan results and errors.
216
259
  */
217
- this.test = this.testOrigin.bind(this);
260
+ this.bulkScanUrls = async (urls) => {
261
+ if (!urls || urls.length === 0) {
262
+ return {
263
+ results: {},
264
+ errors: {},
265
+ };
266
+ }
267
+ // we are arbitrarily limiting the number of URLs to 250
268
+ const MAX_TOTAL_URLS = 250;
269
+ if (urls.length > MAX_TOTAL_URLS) {
270
+ return {
271
+ results: {},
272
+ errors: {
273
+ too_many_urls: [
274
+ `Maximum of ${MAX_TOTAL_URLS} URLs allowed per request`,
275
+ ],
276
+ },
277
+ };
278
+ }
279
+ const MAX_URL_LENGTH = 2048;
280
+ const combinedResponse = {
281
+ results: {},
282
+ errors: {},
283
+ };
284
+ // Extract hostnames from URLs and check for validity and length constraints
285
+ const urlsToHostnames = {};
286
+ const urlsToFetch = [];
287
+ for (const url of urls) {
288
+ if (url.length > MAX_URL_LENGTH) {
289
+ combinedResponse.errors[url] = [
290
+ `URL length must not exceed ${MAX_URL_LENGTH} characters`,
291
+ ];
292
+ continue;
293
+ }
294
+ const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
295
+ if (!ok) {
296
+ combinedResponse.errors[url] = ['url is not a valid web URL'];
297
+ continue;
298
+ }
299
+ // Check if result is already in cache
300
+ const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
301
+ if (cachedResult) {
302
+ // Use cached result
303
+ combinedResponse.results[url] = cachedResult;
304
+ }
305
+ else {
306
+ // Add to list of URLs to fetch
307
+ urlsToHostnames[url] = hostname;
308
+ urlsToFetch.push(url);
309
+ }
310
+ }
311
+ // If there are URLs to fetch, process them in batches
312
+ if (urlsToFetch.length > 0) {
313
+ // The API has a limit of 50 URLs per request, so we batch the requests
314
+ const MAX_URLS_PER_BATCH = 50;
315
+ const batches = [];
316
+ for (let i = 0; i < urlsToFetch.length; i += MAX_URLS_PER_BATCH) {
317
+ batches.push(urlsToFetch.slice(i, i + MAX_URLS_PER_BATCH));
318
+ }
319
+ // Process each batch in parallel
320
+ const batchResults = await Promise.all(batches.map((batchUrls) => __classPrivateFieldGet(this, _PhishingController_processBatch, "f").call(this, batchUrls)));
321
+ // Merge results and errors from all batches
322
+ batchResults.forEach((batchResponse) => {
323
+ // Add results to cache and combine with response
324
+ Object.entries(batchResponse.results).forEach(([url, result]) => {
325
+ const hostname = urlsToHostnames[url];
326
+ if (hostname) {
327
+ __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
328
+ }
329
+ combinedResponse.results[url] = result;
330
+ });
331
+ // Combine errors
332
+ Object.entries(batchResponse.errors).forEach(([key, messages]) => {
333
+ combinedResponse.errors[key] = [
334
+ ...(combinedResponse.errors[key] || []),
335
+ ...messages,
336
+ ];
337
+ });
338
+ });
339
+ }
340
+ return combinedResponse;
341
+ };
218
342
  /**
219
343
  * Fetch bulk token scan results from the security alerts API.
220
344
  *
@@ -258,6 +382,141 @@ class PhishingController extends base_controller_1.BaseController {
258
382
  }
259
383
  return apiResponse;
260
384
  });
385
+ /**
386
+ * Scan an address for security alerts.
387
+ *
388
+ * @param chainId - The chain ID in hex format (e.g., '0x1' for Ethereum).
389
+ * @param address - The address to scan.
390
+ * @returns The address scan result.
391
+ */
392
+ this.scanAddress = async (chainId, address) => {
393
+ if (!address || !chainId) {
394
+ return {
395
+ result_type: types_1.AddressScanResultType.ErrorResult,
396
+ label: '',
397
+ };
398
+ }
399
+ const normalizedChainId = chainId.toLowerCase();
400
+ const normalizedAddress = address.toLowerCase();
401
+ const chain = (0, utils_1.resolveChainName)(normalizedChainId);
402
+ if (!chain) {
403
+ return {
404
+ result_type: types_1.AddressScanResultType.ErrorResult,
405
+ label: '',
406
+ };
407
+ }
408
+ const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress);
409
+ const cachedResult = __classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").get(cacheKey);
410
+ if (cachedResult) {
411
+ return {
412
+ result_type: cachedResult.result_type,
413
+ label: cachedResult.label,
414
+ };
415
+ }
416
+ const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
417
+ const res = await fetch(`${exports.SECURITY_ALERTS_BASE_URL}${exports.ADDRESS_SCAN_ENDPOINT}`, {
418
+ method: 'POST',
419
+ headers: {
420
+ Accept: 'application/json',
421
+ 'Content-Type': 'application/json',
422
+ },
423
+ body: JSON.stringify({
424
+ chain,
425
+ address: normalizedAddress,
426
+ }),
427
+ });
428
+ if (!res.ok) {
429
+ return {
430
+ error: `${res.status} ${res.statusText}`,
431
+ };
432
+ }
433
+ const data = await res.json();
434
+ return data;
435
+ }, true, 5000);
436
+ if (!apiResponse) {
437
+ return {
438
+ result_type: types_1.AddressScanResultType.ErrorResult,
439
+ label: '',
440
+ };
441
+ }
442
+ else if ('error' in apiResponse) {
443
+ return {
444
+ result_type: types_1.AddressScanResultType.ErrorResult,
445
+ label: '',
446
+ };
447
+ }
448
+ const result = {
449
+ result_type: apiResponse.result_type,
450
+ label: apiResponse.label,
451
+ };
452
+ __classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").set(cacheKey, result);
453
+ return {
454
+ result_type: apiResponse.result_type,
455
+ label: apiResponse.label,
456
+ };
457
+ };
458
+ /**
459
+ * Scan multiple tokens for malicious activity in bulk.
460
+ *
461
+ * @param request - The bulk scan request containing chainId and tokens.
462
+ * @param request.chainId - The chain identifier. Accepts a hex chain ID for
463
+ * EVM chains (e.g. `'0x1'` for Ethereum) or a chain name for non-EVM chains
464
+ * (e.g. `'solana'`).
465
+ * @param request.tokens - Array of token addresses to scan.
466
+ * @returns A mapping of token addresses to their scan results. For EVM chains,
467
+ * addresses are lowercased; for non-EVM chains, original casing is preserved.
468
+ * Tokens that fail to scan are omitted.
469
+ */
470
+ this.bulkScanTokens = async (request) => {
471
+ const { chainId, tokens } = request;
472
+ if (!tokens || tokens.length === 0) {
473
+ return {};
474
+ }
475
+ const MAX_TOKENS_PER_REQUEST = 100;
476
+ if (tokens.length > MAX_TOKENS_PER_REQUEST) {
477
+ console.warn(`Maximum of ${MAX_TOKENS_PER_REQUEST} tokens allowed per request`);
478
+ return {};
479
+ }
480
+ const normalizedChainId = chainId.toLowerCase();
481
+ const chain = (0, utils_1.resolveChainName)(normalizedChainId);
482
+ if (!chain) {
483
+ console.warn(`Unknown chain ID: ${chainId}`);
484
+ return {};
485
+ }
486
+ // EVM addresses are case-insensitive; non-EVM addresses (e.g. Solana
487
+ // base58) are case-sensitive and must not be lowercased.
488
+ const caseSensitive = !normalizedChainId.startsWith('0x');
489
+ // Split tokens into cached results and tokens that need to be fetched
490
+ const { cachedResults, tokensToFetch } = (0, utils_1.splitCacheHits)(__classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f"), normalizedChainId, tokens, caseSensitive);
491
+ const results = { ...cachedResults };
492
+ // If there are tokens to fetch, call the bulk token scan API
493
+ if (tokensToFetch.length > 0) {
494
+ const apiResponse = await __classPrivateFieldGet(this, _PhishingController_fetchTokenScanBulkResults, "f").call(this, chain, tokensToFetch);
495
+ if (apiResponse?.results) {
496
+ // Process API results and update cache
497
+ for (const tokenAddress of tokensToFetch) {
498
+ const normalizedAddress = caseSensitive
499
+ ? tokenAddress
500
+ : tokenAddress.toLowerCase();
501
+ const tokenResult = apiResponse.results[normalizedAddress];
502
+ if (tokenResult?.result_type) {
503
+ const result = {
504
+ result_type: tokenResult.result_type,
505
+ chain: tokenResult.chain || normalizedChainId,
506
+ address: tokenResult.address || normalizedAddress,
507
+ };
508
+ // Update cache
509
+ const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress, caseSensitive);
510
+ __classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f").set(cacheKey, {
511
+ result_type: tokenResult.result_type,
512
+ });
513
+ results[normalizedAddress] = result;
514
+ }
515
+ }
516
+ }
517
+ }
518
+ return results;
519
+ };
261
520
  /**
262
521
  * Process a batch of URLs (up to 50) for phishing detection.
263
522
  *
@@ -340,7 +599,7 @@ class PhishingController extends base_controller_1.BaseController {
340
599
  });
341
600
  },
342
601
  }), "f");
343
- this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
602
+ __classPrivateFieldGet(this, _PhishingController_instances, "m", _PhishingController_registerMessageHandlers).call(this);
344
603
  this.updatePhishingDetector();
345
604
  __classPrivateFieldGet(this, _PhishingController_instances, "m", _PhishingController_subscribeToTransactionControllerStateChange).call(this);
346
605
  }
@@ -410,7 +669,7 @@ class PhishingController extends base_controller_1.BaseController {
410
669
  * @param origin - Domain origin of a website.
411
670
  * @returns Whether the origin is an unapproved origin.
412
671
  */
413
- testOrigin(origin) {
672
+ test(origin) {
414
673
  const punycodeOrigin = (0, punycode_js_1.toASCII)(origin);
415
674
  const hostname = (0, utils_1.getHostnameFromUrl)(punycodeOrigin);
416
675
  const hostnameWithPaths = hostname + (0, utils_1.getPathnameFromUrl)(origin);
@@ -524,291 +783,16 @@ class PhishingController extends base_controller_1.BaseController {
524
783
  __classPrivateFieldSet(this, _PhishingController_inProgressStalelistUpdate, undefined, "f");
525
784
  }
526
785
  }
527
- /**
528
- * Scan a URL for phishing. It will only scan the hostname of the URL. It also only supports
529
- * web URLs.
530
- *
531
- * @param url - The URL to scan.
532
- * @returns The phishing detection scan result.
533
- */
534
- async scanUrl(url) {
535
- const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
536
- if (!ok) {
537
- return {
538
- hostname: '',
539
- recommendedAction: types_1.RecommendedAction.None,
540
- fetchError: 'url is not a valid web URL',
541
- };
542
- }
543
- const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
544
- if (cachedResult) {
545
- return cachedResult;
546
- }
547
- const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
548
- const res = await fetch(`${exports.PHISHING_DETECTION_BASE_URL}/${exports.PHISHING_DETECTION_SCAN_ENDPOINT}?url=${encodeURIComponent(hostname)}`, {
549
- method: 'GET',
550
- headers: {
551
- Accept: 'application/json',
552
- },
553
- });
554
- if (!res.ok) {
555
- return {
556
- error: `${res.status} ${res.statusText}`,
557
- };
558
- }
559
- const data = await res.json();
560
- return data;
561
- }, true, 8000);
562
- // Need to do it this way because safelyExecuteWithTimeout returns undefined for both timeouts and errors.
563
- if (!apiResponse) {
564
- return {
565
- hostname: '',
566
- recommendedAction: types_1.RecommendedAction.None,
567
- fetchError: 'timeout of 8000ms exceeded',
568
- };
569
- }
570
- else if ('error' in apiResponse) {
571
- return {
572
- hostname: '',
573
- recommendedAction: types_1.RecommendedAction.None,
574
- fetchError: apiResponse.error,
575
- };
576
- }
577
- const result = {
578
- hostname,
579
- recommendedAction: apiResponse.recommendedAction,
580
- };
581
- __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
582
- return result;
583
- }
584
- /**
585
- * Scan multiple URLs for phishing in bulk. It will only scan the hostnames of the URLs.
586
- * It also only supports web URLs.
587
- *
588
- * @param urls - The URLs to scan.
589
- * @returns A mapping of URLs to their phishing detection scan results and errors.
590
- */
591
- async bulkScanUrls(urls) {
592
- if (!urls || urls.length === 0) {
593
- return {
594
- results: {},
595
- errors: {},
596
- };
597
- }
598
- // we are arbitrarily limiting the number of URLs to 250
599
- const MAX_TOTAL_URLS = 250;
600
- if (urls.length > MAX_TOTAL_URLS) {
601
- return {
602
- results: {},
603
- errors: {
604
- too_many_urls: [
605
- `Maximum of ${MAX_TOTAL_URLS} URLs allowed per request`,
606
- ],
607
- },
608
- };
609
- }
610
- const MAX_URL_LENGTH = 2048;
611
- const combinedResponse = {
612
- results: {},
613
- errors: {},
614
- };
615
- // Extract hostnames from URLs and check for validity and length constraints
616
- const urlsToHostnames = {};
617
- const urlsToFetch = [];
618
- for (const url of urls) {
619
- if (url.length > MAX_URL_LENGTH) {
620
- combinedResponse.errors[url] = [
621
- `URL length must not exceed ${MAX_URL_LENGTH} characters`,
622
- ];
623
- continue;
624
- }
625
- const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
626
- if (!ok) {
627
- combinedResponse.errors[url] = ['url is not a valid web URL'];
628
- continue;
629
- }
630
- // Check if result is already in cache
631
- const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
632
- if (cachedResult) {
633
- // Use cached result
634
- combinedResponse.results[url] = cachedResult;
635
- }
636
- else {
637
- // Add to list of URLs to fetch
638
- urlsToHostnames[url] = hostname;
639
- urlsToFetch.push(url);
640
- }
641
- }
642
- // If there are URLs to fetch, process them in batches
643
- if (urlsToFetch.length > 0) {
644
- // The API has a limit of 50 URLs per request, so we batch the requests
645
- const MAX_URLS_PER_BATCH = 50;
646
- const batches = [];
647
- for (let i = 0; i < urlsToFetch.length; i += MAX_URLS_PER_BATCH) {
648
- batches.push(urlsToFetch.slice(i, i + MAX_URLS_PER_BATCH));
649
- }
650
- // Process each batch in parallel
651
- const batchResults = await Promise.all(batches.map((batchUrls) => __classPrivateFieldGet(this, _PhishingController_processBatch, "f").call(this, batchUrls)));
652
- // Merge results and errors from all batches
653
- batchResults.forEach((batchResponse) => {
654
- // Add results to cache and combine with response
655
- Object.entries(batchResponse.results).forEach(([url, result]) => {
656
- const hostname = urlsToHostnames[url];
657
- if (hostname) {
658
- __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
659
- }
660
- combinedResponse.results[url] = result;
661
- });
662
- // Combine errors
663
- Object.entries(batchResponse.errors).forEach(([key, messages]) => {
664
- combinedResponse.errors[key] = [
665
- ...(combinedResponse.errors[key] || []),
666
- ...messages,
667
- ];
668
- });
669
- });
670
- }
671
- return combinedResponse;
672
- }
673
- /**
674
- * Scan an address for security alerts.
675
- *
676
- * @param chainId - The chain ID in hex format (e.g., '0x1' for Ethereum).
677
- * @param address - The address to scan.
678
- * @returns The address scan result.
679
- */
680
- async scanAddress(chainId, address) {
681
- if (!address || !chainId) {
682
- return {
683
- result_type: types_1.AddressScanResultType.ErrorResult,
684
- label: '',
685
- };
686
- }
687
- const normalizedChainId = chainId.toLowerCase();
688
- const normalizedAddress = address.toLowerCase();
689
- const chain = (0, utils_1.resolveChainName)(normalizedChainId);
690
- if (!chain) {
691
- return {
692
- result_type: types_1.AddressScanResultType.ErrorResult,
693
- label: '',
694
- };
695
- }
696
- const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress);
697
- const cachedResult = __classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").get(cacheKey);
698
- if (cachedResult) {
699
- return {
700
- result_type: cachedResult.result_type,
701
- label: cachedResult.label,
702
- };
703
- }
704
- const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
705
- const res = await fetch(`${exports.SECURITY_ALERTS_BASE_URL}${exports.ADDRESS_SCAN_ENDPOINT}`, {
706
- method: 'POST',
707
- headers: {
708
- Accept: 'application/json',
709
- 'Content-Type': 'application/json',
710
- },
711
- body: JSON.stringify({
712
- chain,
713
- address: normalizedAddress,
714
- }),
715
- });
716
- if (!res.ok) {
717
- return {
718
- error: `${res.status} ${res.statusText}`,
719
- };
720
- }
721
- const data = await res.json();
722
- return data;
723
- }, true, 5000);
724
- if (!apiResponse) {
725
- return {
726
- result_type: types_1.AddressScanResultType.ErrorResult,
727
- label: '',
728
- };
729
- }
730
- else if ('error' in apiResponse) {
731
- return {
732
- result_type: types_1.AddressScanResultType.ErrorResult,
733
- label: '',
734
- };
735
- }
736
- const result = {
737
- result_type: apiResponse.result_type,
738
- label: apiResponse.label,
739
- };
740
- __classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").set(cacheKey, result);
741
- return {
742
- result_type: apiResponse.result_type,
743
- label: apiResponse.label,
744
- };
745
- }
746
- /**
747
- * Scan multiple tokens for malicious activity in bulk.
748
- *
749
- * @param request - The bulk scan request containing chainId and tokens.
750
- * @param request.chainId - The chain identifier. Accepts a hex chain ID for
751
- * EVM chains (e.g. `'0x1'` for Ethereum) or a chain name for non-EVM chains
752
- * (e.g. `'solana'`).
753
- * @param request.tokens - Array of token addresses to scan.
754
- * @returns A mapping of token addresses to their scan results. For EVM chains,
755
- * addresses are lowercased; for non-EVM chains, original casing is preserved.
756
- * Tokens that fail to scan are omitted.
757
- */
758
- async bulkScanTokens(request) {
759
- const { chainId, tokens } = request;
760
- if (!tokens || tokens.length === 0) {
761
- return {};
762
- }
763
- const MAX_TOKENS_PER_REQUEST = 100;
764
- if (tokens.length > MAX_TOKENS_PER_REQUEST) {
765
- console.warn(`Maximum of ${MAX_TOKENS_PER_REQUEST} tokens allowed per request`);
766
- return {};
767
- }
768
- const normalizedChainId = chainId.toLowerCase();
769
- const chain = (0, utils_1.resolveChainName)(normalizedChainId);
770
- if (!chain) {
771
- console.warn(`Unknown chain ID: ${chainId}`);
772
- return {};
773
- }
774
- // EVM addresses are case-insensitive; non-EVM addresses (e.g. Solana
775
- // base58) are case-sensitive and must not be lowercased.
776
- const caseSensitive = !normalizedChainId.startsWith('0x');
777
- // Split tokens into cached results and tokens that need to be fetched
778
- const { cachedResults, tokensToFetch } = (0, utils_1.splitCacheHits)(__classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f"), normalizedChainId, tokens, caseSensitive);
779
- const results = { ...cachedResults };
780
- // If there are tokens to fetch, call the bulk token scan API
781
- if (tokensToFetch.length > 0) {
782
- const apiResponse = await __classPrivateFieldGet(this, _PhishingController_fetchTokenScanBulkResults, "f").call(this, chain, tokensToFetch);
783
- if (apiResponse?.results) {
784
- // Process API results and update cache
785
- for (const tokenAddress of tokensToFetch) {
786
- const normalizedAddress = caseSensitive
787
- ? tokenAddress
788
- : tokenAddress.toLowerCase();
789
- const tokenResult = apiResponse.results[normalizedAddress];
790
- if (tokenResult?.result_type) {
791
- const result = {
792
- result_type: tokenResult.result_type,
793
- chain: tokenResult.chain || normalizedChainId,
794
- address: tokenResult.address || normalizedAddress,
795
- };
796
- // Update cache
797
- const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress, caseSensitive);
798
- __classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f").set(cacheKey, {
799
- result_type: tokenResult.result_type,
800
- });
801
- results[normalizedAddress] = result;
802
- }
803
- }
804
- }
805
- }
806
- return results;
807
- }
808
786
  }
809
787
  exports.PhishingController = PhishingController;
810
788
  _PhishingController_detector = new WeakMap(), _PhishingController_stalelistRefreshInterval = new WeakMap(), _PhishingController_hotlistRefreshInterval = new WeakMap(), _PhishingController_c2DomainBlocklistRefreshInterval = new WeakMap(), _PhishingController_urlScanCache = new WeakMap(), _PhishingController_tokenScanCache = new WeakMap(), _PhishingController_addressScanCache = new WeakMap(), _PhishingController_inProgressHotlistUpdate = new WeakMap(), _PhishingController_inProgressStalelistUpdate = new WeakMap(), _PhishingController_isProgressC2DomainBlocklistUpdate = new WeakMap(), _PhishingController_transactionControllerStateChangeHandler = new WeakMap(), _PhishingController_fetchTokenScanBulkResults = new WeakMap(), _PhishingController_processBatch = new WeakMap(), _PhishingController_instances = new WeakSet(), _PhishingController_subscribeToTransactionControllerStateChange = function _PhishingController_subscribeToTransactionControllerStateChange() {
811
789
  this.messenger.subscribe('TransactionController:stateChange', __classPrivateFieldGet(this, _PhishingController_transactionControllerStateChangeHandler, "f"));
790
+ }, _PhishingController_registerMessageHandlers = function _PhishingController_registerMessageHandlers() {
791
+ this.messenger.registerActionHandler(`${controllerName}:maybeUpdateState`, this.maybeUpdateState.bind(this));
792
+ this.messenger.registerActionHandler(`${controllerName}:testOrigin`, this.test.bind(this));
793
+ this.messenger.registerActionHandler(`${controllerName}:bulkScanUrls`, this.bulkScanUrls.bind(this));
794
+ this.messenger.registerActionHandler(`${controllerName}:bulkScanTokens`, this.bulkScanTokens.bind(this));
795
+ this.messenger.registerActionHandler(`${controllerName}:scanAddress`, this.scanAddress.bind(this));
812
796
  }, _PhishingController_isTransactionPatch = function _PhishingController_isTransactionPatch(patch) {
813
797
  const { path } = patch;
814
798
  return (path.length === 2 &&