Skip to content

refactor: migrate PhishingController data fetching to PhishingDataService - #9914

Open
0xOhm wants to merge 34 commits into
mainfrom
refactor/psafe-593-phishing-data-service
Open

refactor: migrate PhishingController data fetching to PhishingDataService#9914
0xOhm wants to merge 34 commits into
mainfrom
refactor/psafe-593-phishing-data-service

Conversation

@0xOhm

@0xOhm 0xOhm commented Aug 20, 2026

Copy link
Copy Markdown

Description

Extracts all remote data fetching from PhishingController into a new PhishingDataService built on the BaseDataService pattern (PSAFE-593). CacheManager is removed in favor of the service's built-in caching, persistence, and request coalescing.

Changes

  • New PhishingDataService owning all four remote APIs (stalelist/hotlist, request-blocklist/C2, dapp scanning, token/address scanning) with generated method action types
  • PhishingController slimmed to detection/state logic; queries data through the service via the messenger
  • CacheManager removed — caching, persistence (StorageService), and request coalescing now come from BaseDataService
  • Response validation intentionally kept loose (e.g. token scan result_type as string, not enum) to match pre-refactor behavior — production already returns values outside the documented enum (Verified)

Verification

  • 501 unit tests / 7 suites, 100% stmt/fn/line coverage, 99.29% branch
  • Full monorepo build clean against the new API
  • Live production API contract: 9/9 endpoints accepted by validation
  • Outage simulation: detection fails open to stale lists (never off), bounded retries
  • Persistence lifecycle: rehydration after simulated MV3 SW restart with zero network; maxAge-expired verdicts discarded
  • Coalescing parity: 120-token chunking, miss-only fetch, zero-fetch cached batches

Client integration PRs

  • Extension and mobile integration PRs to follow (wiring + state migrations); they depend on this merging and publishing first.

🤖 Generated with Claude Code


Note

High Risk
Breaking changes to phishing/security fetch and caching paths require client wiring and state migrations; incorrect integration could weaken or delay detection until lists and scans refresh.

Overview
Moves all phishing-related remote I/O out of PhishingController into a new PhishingDataService built on BaseDataService, so scans and list updates go through messenger actions, TanStack query caching, optional StorageService persistence, and shared retry/circuit-breaker policy instead of in-controller fetch and CacheManager.

Breaking integration: clients must register PhishingDataService, delegate its method actions on the controller messenger, call init when persistence is enabled, and migrate persisted state by dropping urlScanCache, tokenScanCache, and addressScanCache plus the old scan-cache TTL/size options. Scan freshness is now driven by the service’s stale/GC times; list payloads still live in controller state.

BaseDataService gains executeWithPolicy, hydrationTimeout / DEFAULT_HYDRATION_TIMEOUT, and shouldHydrateQuery, and queries after init wait for rehydration (with a timeout) while malformed persisted caches are removed instead of hydrated.

Behavior tweaks include stricter response validation with per-item bulk error handling, partial bulk results on mixed success/failure, address normalization for cache keys, abort/timeouts on hung requests, and hotlist diffs for unknown list types are skipped rather than failing the whole update.

Reviewed by Cursor Bugbot for commit a9d3e6d. Bugbot is set up for automated code reviews on this repo. Configure here.

…vice

Extract all remote data fetching from PhishingController into a new
PhishingDataService built on the BaseDataService pattern. Removes
CacheManager in favor of the service's built-in caching, persistence,
and request coalescing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@0xOhm

0xOhm commented Aug 25, 2026

Copy link
Copy Markdown
Author

@metamaskbot publish-previews

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.6.1-preview-4758e65ff
@metamask-previews/accounts-controller@39.1.0-preview-4758e65ff
@metamask-previews/address-book-controller@7.1.2-preview-4758e65ff
@metamask-previews/ai-controllers@0.8.0-preview-4758e65ff
@metamask-previews/analytics-controller@2.0.0-preview-4758e65ff
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-4758e65ff
@metamask-previews/announcement-controller@8.1.0-preview-4758e65ff
@metamask-previews/app-metadata-controller@2.0.1-preview-4758e65ff
@metamask-previews/approval-controller@9.0.2-preview-4758e65ff
@metamask-previews/assets-controller@13.1.2-preview-4758e65ff
@metamask-previews/assets-controllers@111.1.0-preview-4758e65ff
@metamask-previews/authenticated-user-storage@3.0.1-preview-4758e65ff
@metamask-previews/base-controller@9.1.0-preview-4758e65ff
@metamask-previews/base-data-service@0.1.3-preview-4758e65ff
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-4758e65ff
@metamask-previews/bridge-controller@79.1.0-preview-4758e65ff
@metamask-previews/bridge-status-controller@75.0.0-preview-4758e65ff
@metamask-previews/build-utils@3.0.4-preview-4758e65ff
@metamask-previews/chain-agnostic-permission@1.7.0-preview-4758e65ff
@metamask-previews/chomp-api-service@4.0.0-preview-4758e65ff
@metamask-previews/claims-controller@0.6.0-preview-4758e65ff
@metamask-previews/client-controller@1.0.1-preview-4758e65ff
@metamask-previews/client-utils@2.0.1-preview-4758e65ff
@metamask-previews/compliance-controller@2.1.0-preview-4758e65ff
@metamask-previews/composable-controller@12.0.1-preview-4758e65ff
@metamask-previews/config-registry-controller@2.0.1-preview-4758e65ff
@metamask-previews/connectivity-controller@0.3.0-preview-4758e65ff
@metamask-previews/controller-utils@12.3.0-preview-4758e65ff
@metamask-previews/core-backend@8.1.1-preview-4758e65ff
@metamask-previews/delegation-controller@3.0.2-preview-4758e65ff
@metamask-previews/earn-controller@12.2.4-preview-4758e65ff
@metamask-previews/eip-5792-middleware@3.0.5-preview-4758e65ff
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-4758e65ff
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-4758e65ff
@metamask-previews/ens-controller@19.1.6-preview-4758e65ff
@metamask-previews/eth-block-tracker@15.0.1-preview-4758e65ff
@metamask-previews/eth-json-rpc-middleware@24.0.0-preview-4758e65ff
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-4758e65ff
@metamask-previews/foundryup@1.0.1-preview-4758e65ff
@metamask-previews/gas-fee-controller@26.3.1-preview-4758e65ff
@metamask-previews/gator-permissions-controller@5.0.1-preview-4758e65ff
@metamask-previews/geolocation-controller@1.0.0-preview-4758e65ff
@metamask-previews/java-tron-up@1.0.0-preview-4758e65ff
@metamask-previews/json-rpc-engine@10.5.0-preview-4758e65ff
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-4758e65ff
@metamask-previews/keyring-controller@27.1.1-preview-4758e65ff
@metamask-previews/kyc-controller@0.0.0-preview-4758e65ff
@metamask-previews/local-node-utils@1.0.0-preview-4758e65ff
@metamask-previews/logging-controller@8.0.2-preview-4758e65ff
@metamask-previews/message-manager@14.1.2-preview-4758e65ff
@metamask-previews/messenger@2.0.0-preview-4758e65ff
@metamask-previews/messenger-cli@0.2.0-preview-4758e65ff
@metamask-previews/money-account-api-data-service@0.4.0-preview-4758e65ff
@metamask-previews/money-account-balance-service@2.4.1-preview-4758e65ff
@metamask-previews/money-account-controller@1.0.0-preview-4758e65ff
@metamask-previews/money-account-upgrade-controller@3.0.1-preview-4758e65ff
@metamask-previews/money-account-utils@1.1.0-preview-4758e65ff
@metamask-previews/multichain-account-service@13.0.1-preview-4758e65ff
@metamask-previews/multichain-api-middleware@4.0.2-preview-4758e65ff
@metamask-previews/multichain-network-controller@3.2.3-preview-4758e65ff
@metamask-previews/multichain-transactions-controller@7.1.2-preview-4758e65ff
@metamask-previews/name-controller@9.1.2-preview-4758e65ff
@metamask-previews/network-connection-banner-controller@0.1.2-preview-4758e65ff
@metamask-previews/network-controller@35.0.1-preview-4758e65ff
@metamask-previews/network-enablement-controller@6.0.3-preview-4758e65ff
@metamask-previews/notification-services-controller@26.0.1-preview-4758e65ff
@metamask-previews/passkey-controller@3.0.0-preview-4758e65ff
@metamask-previews/permission-controller@13.1.1-preview-4758e65ff
@metamask-previews/permission-log-controller@5.1.0-preview-4758e65ff
@metamask-previews/perps-controller@11.0.0-preview-4758e65ff
@metamask-previews/phishing-controller@17.3.1-preview-4758e65ff
@metamask-previews/platform-api-docs@0.0.0-preview-4758e65ff
@metamask-previews/polling-controller@16.0.9-preview-4758e65ff
@metamask-previews/preferences-controller@23.1.0-preview-4758e65ff
@metamask-previews/profile-metrics-controller@4.0.3-preview-4758e65ff
@metamask-previews/profile-sync-controller@29.0.0-preview-4758e65ff
@metamask-previews/ramps-controller@20.0.0-preview-4758e65ff
@metamask-previews/rate-limit-controller@7.0.1-preview-4758e65ff
@metamask-previews/react-data-query@0.2.2-preview-4758e65ff
@metamask-previews/remote-feature-flag-controller@5.0.0-preview-4758e65ff
@metamask-previews/sample-controllers@5.0.4-preview-4758e65ff
@metamask-previews/seedless-onboarding-controller@10.1.1-preview-4758e65ff
@metamask-previews/selected-network-controller@26.1.6-preview-4758e65ff
@metamask-previews/sentinel-api-service@1.0.0-preview-4758e65ff
@metamask-previews/shield-controller@6.0.0-preview-4758e65ff
@metamask-previews/signature-controller@39.2.9-preview-4758e65ff
@metamask-previews/smart-transactions-controller@25.1.0-preview-4758e65ff
@metamask-previews/snap-account-service@2.1.2-preview-4758e65ff
@metamask-previews/social-controllers@2.7.1-preview-4758e65ff
@metamask-previews/solana-test-validator-up@1.0.0-preview-4758e65ff
@metamask-previews/stellar-quickstart-up@0.0.0-preview-4758e65ff
@metamask-previews/storage-service@1.0.2-preview-4758e65ff
@metamask-previews/subscription-controller@7.0.0-preview-4758e65ff
@metamask-previews/transaction-controller@69.5.2-preview-4758e65ff
@metamask-previews/transaction-pay-controller@26.3.0-preview-4758e65ff
@metamask-previews/user-operation-controller@41.2.8-preview-4758e65ff
@metamask-previews/wallet@10.0.0-preview-4758e65ff
@metamask-previews/wallet-cli@0.0.0-preview-4758e65ff

Resolve conflicts from base-data-service 1.0.0 and phishing-controller 17.4.0.

- Bump `@metamask/base-data-service` to ^1.0.0 and `@tanstack/query-core`
  to ^5.62.16 to match main. query-core v4 had no lockfile resolution on
  main, which broke `yarn install --immutable` in CI.
- Rename `cacheTime` to `gcTime` in `getApprovals` for the query-core v5 API.
- Keep main's `getAddressScanSupportedChain` and
  `isAddressScanSupportedChainId`; drop `splitCacheHits`, whose
  `buildCacheKey` dependency this refactor removes and which was never
  exported or used.
- Take main's `@metamask/transaction-controller` ^69.6.1 and move the
  ^69.5.2 bump entry under the released 17.4.0 changelog section.
@0xOhm

0xOhm commented Aug 26, 2026

Copy link
Copy Markdown
Author

@metamaskbot publish-previews

0xOhm added a commit to MetaMask/metamask-extension that referenced this pull request Aug 26, 2026
…tion

Resolve the migration-number collision and data-service registry conflict.

- Renumber our PhishingController cache-removal migration from 222 to 224.
  Main shipped 222 (delete persisted EnsController state) and 223 (move
  StorageService data to IndexedDB) in the meantime, so 222 was taken.
  `oldVersion` in the test moves from 221 to 223 to match.
- Register 223 and 224 in `migrations/index.js`.
- `DATA_SERVICES` keeps main's `MoneyAccountBalanceService` and
  `MoneyAccountApiDataService` plus its `createUIQueryClient` comment, and
  adds `PhishingDataService`.

Still pending, tracked separately: `@metamask/phishing-controller` is not
bumped yet, so this cannot pass typecheck against the published package
until MetaMask/core#9914 merges and releases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adonesky1

Copy link
Copy Markdown
Contributor

@metamaskbot publish-previews

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@8.0.0-preview-5b293ee50
@metamask-previews/accounts-controller@39.1.1-preview-5b293ee50
@metamask-previews/address-book-controller@7.1.2-preview-5b293ee50
@metamask-previews/ai-controllers@0.8.0-preview-5b293ee50
@metamask-previews/analytics-controller@2.0.0-preview-5b293ee50
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-5b293ee50
@metamask-previews/announcement-controller@8.1.0-preview-5b293ee50
@metamask-previews/app-metadata-controller@2.0.1-preview-5b293ee50
@metamask-previews/approval-controller@9.0.2-preview-5b293ee50
@metamask-previews/assets-controller@14.0.2-preview-5b293ee50
@metamask-previews/assets-controllers@111.1.3-preview-5b293ee50
@metamask-previews/authenticated-user-storage@3.0.2-preview-5b293ee50
@metamask-previews/base-controller@9.1.0-preview-5b293ee50
@metamask-previews/base-data-service@1.0.0-preview-5b293ee50
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-5b293ee50
@metamask-previews/bridge-controller@79.3.1-preview-5b293ee50
@metamask-previews/bridge-status-controller@75.2.1-preview-5b293ee50
@metamask-previews/build-utils@3.0.4-preview-5b293ee50
@metamask-previews/chain-agnostic-permission@1.7.0-preview-5b293ee50
@metamask-previews/chomp-api-service@4.0.1-preview-5b293ee50
@metamask-previews/claims-controller@0.6.1-preview-5b293ee50
@metamask-previews/client-controller@1.0.1-preview-5b293ee50
@metamask-previews/client-utils@2.1.1-preview-5b293ee50
@metamask-previews/compliance-controller@2.1.0-preview-5b293ee50
@metamask-previews/composable-controller@12.0.1-preview-5b293ee50
@metamask-previews/config-registry-controller@3.1.0-preview-5b293ee50
@metamask-previews/connectivity-controller@0.3.0-preview-5b293ee50
@metamask-previews/controller-utils@12.3.0-preview-5b293ee50
@metamask-previews/core-backend@9.0.0-preview-5b293ee50
@metamask-previews/delegation-controller@3.0.2-preview-5b293ee50
@metamask-previews/earn-controller@12.2.6-preview-5b293ee50
@metamask-previews/eip-5792-middleware@3.0.5-preview-5b293ee50
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-5b293ee50
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-5b293ee50
@metamask-previews/eth-block-tracker@15.0.1-preview-5b293ee50
@metamask-previews/eth-json-rpc-middleware@24.0.1-preview-5b293ee50
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-5b293ee50
@metamask-previews/foundryup@1.0.1-preview-5b293ee50
@metamask-previews/gas-fee-controller@26.3.2-preview-5b293ee50
@metamask-previews/gator-permissions-controller@5.0.2-preview-5b293ee50
@metamask-previews/geolocation-controller@1.0.0-preview-5b293ee50
@metamask-previews/java-tron-up@1.0.0-preview-5b293ee50
@metamask-previews/json-rpc-engine@10.5.0-preview-5b293ee50
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-5b293ee50
@metamask-previews/keyring-controller@27.1.1-preview-5b293ee50
@metamask-previews/kyc-controller@0.0.0-preview-5b293ee50
@metamask-previews/local-node-utils@1.0.0-preview-5b293ee50
@metamask-previews/logging-controller@9.0.0-preview-5b293ee50
@metamask-previews/message-manager@14.1.2-preview-5b293ee50
@metamask-previews/messenger@2.0.0-preview-5b293ee50
@metamask-previews/messenger-cli@0.2.0-preview-5b293ee50
@metamask-previews/money-account-api-data-service@0.4.1-preview-5b293ee50
@metamask-previews/money-account-balance-service@2.4.3-preview-5b293ee50
@metamask-previews/money-account-controller@1.0.0-preview-5b293ee50
@metamask-previews/money-account-upgrade-controller@3.0.2-preview-5b293ee50
@metamask-previews/money-account-utils@1.1.0-preview-5b293ee50
@metamask-previews/multichain-account-service@13.0.2-preview-5b293ee50
@metamask-previews/multichain-api-middleware@4.0.3-preview-5b293ee50
@metamask-previews/multichain-network-controller@3.2.4-preview-5b293ee50
@metamask-previews/multichain-transactions-controller@7.1.2-preview-5b293ee50
@metamask-previews/name-controller@9.1.2-preview-5b293ee50
@metamask-previews/network-connection-banner-controller@0.2.1-preview-5b293ee50
@metamask-previews/network-controller@36.0.0-preview-5b293ee50
@metamask-previews/network-enablement-controller@6.0.5-preview-5b293ee50
@metamask-previews/notification-services-controller@26.0.1-preview-5b293ee50
@metamask-previews/passkey-controller@3.1.0-preview-5b293ee50
@metamask-previews/permission-controller@13.1.1-preview-5b293ee50
@metamask-previews/permission-log-controller@5.1.0-preview-5b293ee50
@metamask-previews/perps-controller@13.0.0-preview-5b293ee50
@metamask-previews/phishing-controller@17.4.0-preview-5b293ee50
@metamask-previews/platform-api-docs@0.0.0-preview-5b293ee50
@metamask-previews/polling-controller@16.0.9-preview-5b293ee50
@metamask-previews/preferences-controller@23.1.0-preview-5b293ee50
@metamask-previews/profile-metrics-controller@4.0.3-preview-5b293ee50
@metamask-previews/profile-sync-controller@29.0.0-preview-5b293ee50
@metamask-previews/ramps-controller@20.0.0-preview-5b293ee50
@metamask-previews/rate-limit-controller@7.0.1-preview-5b293ee50
@metamask-previews/react-data-query@1.0.0-preview-5b293ee50
@metamask-previews/remote-feature-flag-controller@6.0.0-preview-5b293ee50
@metamask-previews/sample-controllers@5.0.6-preview-5b293ee50
@metamask-previews/seedless-onboarding-controller@10.1.1-preview-5b293ee50
@metamask-previews/selected-network-controller@26.1.7-preview-5b293ee50
@metamask-previews/sentinel-api-service@1.0.1-preview-5b293ee50
@metamask-previews/shield-controller@6.0.1-preview-5b293ee50
@metamask-previews/signature-controller@39.2.10-preview-5b293ee50
@metamask-previews/smart-transactions-controller@25.1.1-preview-5b293ee50
@metamask-previews/snap-account-service@2.1.2-preview-5b293ee50
@metamask-previews/social-controllers@2.8.0-preview-5b293ee50
@metamask-previews/solana-test-validator-up@1.0.0-preview-5b293ee50
@metamask-previews/stellar-quickstart-up@0.0.0-preview-5b293ee50
@metamask-previews/storage-service@1.0.2-preview-5b293ee50
@metamask-previews/subscription-controller@8.0.1-preview-5b293ee50
@metamask-previews/transaction-controller@69.6.1-preview-5b293ee50
@metamask-previews/transaction-pay-controller@27.0.0-preview-5b293ee50
@metamask-previews/user-operation-controller@41.2.9-preview-5b293ee50
@metamask-previews/wallet@12.0.2-preview-5b293ee50
@metamask-previews/wallet-cli@0.0.0-preview-5b293ee50

Comment thread packages/phishing-controller/src/PhishingDataService.ts Outdated
Comment thread packages/phishing-controller/src/PhishingDataService.ts
Comment thread packages/phishing-controller/src/PhishingDataService.ts Outdated
Comment thread packages/phishing-controller/src/PhishingDataService.ts Outdated
Comment thread packages/phishing-controller/src/PhishingDataService.ts Outdated
Comment thread packages/phishing-controller/src/PhishingDataService.ts Outdated
Comment thread packages/phishing-controller/src/PhishingDataService.test.ts

@adonesky1 adonesky1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged main into this branch to clear the conflicts (CHANGELOG, package.json, yarn.lock, all union resolutions). Worth knowing that #10007 landed on main while this was open and removed fetchQuery from every KycService POST for three reasons that all apply here: deduped in-flight writes, responses published on the messenger as cacheUpdated payloads, and the service policy retrying non-idempotent POSTs. Those are the same root cause behind the bulk dedupe and retry fan-out comments I left earlier.

Could we reconcile with that precedent, keeping fetchQuery for the GET list endpoints and scanUrl but calling #postJson directly for scanAddress and getApprovals? The bulk endpoints genuinely need per-item caching so they are a real exception, but it would help to say so explicitly in a comment.

Also note #6388 (C2 blocklist Array to Set) is now in the base. It only touched PhishingDetector.ts, which this PR does not modify, so the merge is clean there.

Comment thread packages/phishing-controller/src/PhishingDataService.ts
Comment thread packages/phishing-controller/src/PhishingDataService.ts
Comment thread packages/phishing-controller/src/PhishingController.ts Outdated
Comment thread packages/phishing-controller/CHANGELOG.md Outdated
Bulk URL scanning:

- Key bulk queries by the scan URL parameter rather than the bare hostname.
  Path-sensitive hosts (ipfs.io, github.io, and the other entries in
  PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS) collapsed into one cache entry
  per host, so only the first path in a batch was scanned and the rest
  inherited its verdict.
- Return the results that did resolve when some lookups fail, reporting the
  failures per URL, instead of discarding the whole batch. A single failed
  lookup previously threw away fresh cached BLOCK verdicts for unrelated URLs.
  The call still rejects when nothing could be resolved at all.
- Stop caching a "no result" verdict for URLs the API reported an error for.
  Those URLs were silently skipped for the following minute.
- Report invalid URLs instead of collapsing them onto a shared empty key.

Request policy:

- Disable retries by default. The previous in-controller implementation made
  a single request per call and the controller's timeouts are sized for one
  attempt, so inheriting maxRetries: 3 meant a timeout could fire mid-retry.
  Retries also amplified badly through the batch loaders: a failed batch
  rejects every item query in it, and each retried on its own, turning one
  failed request into many single-item requests against a failing host.

Caching:

- Validate responses inside fetchQuery via responseStruct. Validating after
  the fact meant a malformed 200 was committed to the cache, and persisted,
  before it was rejected, so every caller for the next minute got the same
  error with no request made.
- Set gcTime explicitly on scan queries. TanStack Query defaults gcTime to
  Infinity when it detects a server environment, which includes the MV3
  service worker, so the cache would grow unbounded; the cache this replaces
  was explicitly size-bounded.
- Do not retain fetched lists in the query cache. The controller keeps its
  own copy, and retaining them meant the multi-megabyte stalelist was
  rewritten to disk on every scan-triggered persist.
- Do not route getApprovals through the query cache. It is never cached
  (staleTime and gcTime are both 0), so the cache only served to publish
  account-specific approval data on the messenger, matching #10007.

Also corrects two changelog claims: PhishingDataService is not yet one of
@metamask/wallet's default instances, so init is not automatic.
`responseStruct` requires a `Struct<Json>`, which these structs are not
(`HotlistDiffsResponseStruct` infers `unknown[]`), so the build failed.
Validating inside the query function achieves the same result: a malformed
response throws before TanStack Query commits it, so it is never cached or
persisted, and the existing error messages are preserved.
@adonesky1

Copy link
Copy Markdown
Contributor

@metamaskbot publish-previews

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@8.0.0-preview-1cf7cc7b3
@metamask-previews/accounts-controller@39.1.1-preview-1cf7cc7b3
@metamask-previews/address-book-controller@7.1.2-preview-1cf7cc7b3
@metamask-previews/ai-controllers@1.0.0-preview-1cf7cc7b3
@metamask-previews/analytics-controller@2.0.0-preview-1cf7cc7b3
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-1cf7cc7b3
@metamask-previews/announcement-controller@8.1.0-preview-1cf7cc7b3
@metamask-previews/app-metadata-controller@2.0.1-preview-1cf7cc7b3
@metamask-previews/approval-controller@9.0.2-preview-1cf7cc7b3
@metamask-previews/assets-controller@14.0.3-preview-1cf7cc7b3
@metamask-previews/assets-controllers@111.1.3-preview-1cf7cc7b3
@metamask-previews/authenticated-user-storage@3.0.2-preview-1cf7cc7b3
@metamask-previews/base-controller@9.1.0-preview-1cf7cc7b3
@metamask-previews/base-data-service@1.0.0-preview-1cf7cc7b3
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-1cf7cc7b3
@metamask-previews/bridge-controller@80.1.1-preview-1cf7cc7b3
@metamask-previews/bridge-status-controller@75.4.0-preview-1cf7cc7b3
@metamask-previews/build-utils@3.0.4-preview-1cf7cc7b3
@metamask-previews/chain-agnostic-permission@1.7.0-preview-1cf7cc7b3
@metamask-previews/chomp-api-service@4.0.1-preview-1cf7cc7b3
@metamask-previews/claims-controller@0.6.1-preview-1cf7cc7b3
@metamask-previews/client-controller@1.0.1-preview-1cf7cc7b3
@metamask-previews/client-utils@2.1.1-preview-1cf7cc7b3
@metamask-previews/compliance-controller@2.1.0-preview-1cf7cc7b3
@metamask-previews/composable-controller@12.0.1-preview-1cf7cc7b3
@metamask-previews/config-registry-controller@3.1.0-preview-1cf7cc7b3
@metamask-previews/connectivity-controller@0.3.0-preview-1cf7cc7b3
@metamask-previews/controller-utils@12.3.0-preview-1cf7cc7b3
@metamask-previews/core-backend@9.0.0-preview-1cf7cc7b3
@metamask-previews/delegation-controller@3.0.2-preview-1cf7cc7b3
@metamask-previews/earn-controller@12.2.6-preview-1cf7cc7b3
@metamask-previews/eip-5792-middleware@3.0.5-preview-1cf7cc7b3
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-1cf7cc7b3
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-1cf7cc7b3
@metamask-previews/eth-block-tracker@15.0.1-preview-1cf7cc7b3
@metamask-previews/eth-json-rpc-middleware@24.0.2-preview-1cf7cc7b3
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-1cf7cc7b3
@metamask-previews/foundryup@1.0.1-preview-1cf7cc7b3
@metamask-previews/gas-fee-controller@26.3.2-preview-1cf7cc7b3
@metamask-previews/gator-permissions-controller@5.0.2-preview-1cf7cc7b3
@metamask-previews/geolocation-controller@1.0.0-preview-1cf7cc7b3
@metamask-previews/java-tron-up@1.0.0-preview-1cf7cc7b3
@metamask-previews/json-rpc-engine@10.5.0-preview-1cf7cc7b3
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-1cf7cc7b3
@metamask-previews/keyring-controller@27.1.1-preview-1cf7cc7b3
@metamask-previews/kyc-controller@0.0.0-preview-1cf7cc7b3
@metamask-previews/local-node-utils@1.0.0-preview-1cf7cc7b3
@metamask-previews/logging-controller@9.0.0-preview-1cf7cc7b3
@metamask-previews/message-manager@14.1.2-preview-1cf7cc7b3
@metamask-previews/messenger@2.0.0-preview-1cf7cc7b3
@metamask-previews/messenger-cli@0.2.0-preview-1cf7cc7b3
@metamask-previews/money-account-api-data-service@0.4.1-preview-1cf7cc7b3
@metamask-previews/money-account-balance-service@2.4.3-preview-1cf7cc7b3
@metamask-previews/money-account-controller@1.0.0-preview-1cf7cc7b3
@metamask-previews/money-account-upgrade-controller@3.0.2-preview-1cf7cc7b3
@metamask-previews/money-account-utils@1.1.0-preview-1cf7cc7b3
@metamask-previews/multichain-account-service@13.0.2-preview-1cf7cc7b3
@metamask-previews/multichain-api-middleware@4.0.3-preview-1cf7cc7b3
@metamask-previews/multichain-network-controller@3.2.4-preview-1cf7cc7b3
@metamask-previews/multichain-transactions-controller@7.1.2-preview-1cf7cc7b3
@metamask-previews/name-controller@9.1.2-preview-1cf7cc7b3
@metamask-previews/network-connection-banner-controller@0.2.1-preview-1cf7cc7b3
@metamask-previews/network-controller@36.0.0-preview-1cf7cc7b3
@metamask-previews/network-enablement-controller@6.0.5-preview-1cf7cc7b3
@metamask-previews/notification-services-controller@26.0.1-preview-1cf7cc7b3
@metamask-previews/passkey-controller@3.1.0-preview-1cf7cc7b3
@metamask-previews/permission-controller@13.1.1-preview-1cf7cc7b3
@metamask-previews/permission-log-controller@5.1.0-preview-1cf7cc7b3
@metamask-previews/perps-controller@15.0.0-preview-1cf7cc7b3
@metamask-previews/phishing-controller@17.4.0-preview-1cf7cc7b3
@metamask-previews/platform-api-docs@0.1.0-preview-1cf7cc7b3
@metamask-previews/polling-controller@16.0.9-preview-1cf7cc7b3
@metamask-previews/preferences-controller@23.1.0-preview-1cf7cc7b3
@metamask-previews/profile-metrics-controller@4.0.3-preview-1cf7cc7b3
@metamask-previews/profile-sync-controller@29.0.0-preview-1cf7cc7b3
@metamask-previews/ramps-controller@20.2.0-preview-1cf7cc7b3
@metamask-previews/rate-limit-controller@7.0.1-preview-1cf7cc7b3
@metamask-previews/react-data-query@1.0.0-preview-1cf7cc7b3
@metamask-previews/remote-feature-flag-controller@6.1.0-preview-1cf7cc7b3
@metamask-previews/sample-controllers@5.0.6-preview-1cf7cc7b3
@metamask-previews/seedless-onboarding-controller@10.1.1-preview-1cf7cc7b3
@metamask-previews/selected-network-controller@26.1.7-preview-1cf7cc7b3
@metamask-previews/sentinel-api-service@1.0.1-preview-1cf7cc7b3
@metamask-previews/shield-controller@6.0.1-preview-1cf7cc7b3
@metamask-previews/signature-controller@39.2.10-preview-1cf7cc7b3
@metamask-previews/smart-transactions-controller@26.0.0-preview-1cf7cc7b3
@metamask-previews/snap-account-service@2.1.2-preview-1cf7cc7b3
@metamask-previews/social-controllers@2.8.0-preview-1cf7cc7b3
@metamask-previews/solana-test-validator-up@1.0.0-preview-1cf7cc7b3
@metamask-previews/stellar-quickstart-up@0.0.0-preview-1cf7cc7b3
@metamask-previews/storage-service@1.0.2-preview-1cf7cc7b3
@metamask-previews/subscription-controller@8.0.1-preview-1cf7cc7b3
@metamask-previews/transaction-controller@69.7.0-preview-1cf7cc7b3
@metamask-previews/transaction-pay-controller@27.1.1-preview-1cf7cc7b3
@metamask-previews/user-operation-controller@41.2.9-preview-1cf7cc7b3
@metamask-previews/wallet@12.0.2-preview-1cf7cc7b3
@metamask-previews/wallet-cli@0.0.0-preview-1cf7cc7b3

…ishing-data-service

# Conflicts:
#	packages/phishing-controller/CHANGELOG.md
#	packages/phishing-controller/package.json
#	yarn.lock
@adonesky1
adonesky1 marked this pull request as ready for review September 9, 2026 20:36
@adonesky1
adonesky1 requested review from a team as code owners September 9, 2026 20:36
@adonesky1
adonesky1 enabled auto-merge September 9, 2026 20:38

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/phishing-controller/src/PhishingDataService.ts
Comment thread packages/phishing-controller/src/PhishingDataService.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a905f5c. Configure here.

Comment thread packages/phishing-controller/src/PhishingDataService.ts

@mcmire mcmire left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just started looking at this PR. Here are some comments for now. I will do another pass later.

export const METAMASK_STALELIST_FILE = '/v1/stalelist';
export const METAMASK_HOTLIST_DIFF_FILE = '/v2/diffsSince';

export const CLIENT_SIDE_DETECION_BASE_URL =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

Suggested change
export const CLIENT_SIDE_DETECION_BASE_URL =
export const CLIENT_SIDE_DETECTION_BASE_URL =

gcTime: LIST_GC_TIME,
});

return jsonResponse as DataResultWrapper<PhishingStalelist>;

@mcmire mcmire Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the typecast? Ideally this.#validate should return the correct type (as it's the one ensuring that the data matches a known type).

(edit: I'll try to come back to this and make a better suggestion here)

- **BREAKING:** Remove the `urlScanCacheTTL`, `urlScanCacheMaxSize`, `tokenScanCacheTTL`, `tokenScanCacheMaxSize`, `addressScanCacheTTL`, and `addressScanCacheMaxSize` options from `PhishingControllerOptions`; scan result freshness is now controlled by `SCAN_RESULT_STALE_TIME` in `PhishingDataService` ([#9914](https://github.com/MetaMask/core/pull/9914))
- Tokens for which the bulk scanning API returns no result are now negatively cached for `SCAN_RESULT_STALE_TIME` instead of being re-requested on every call ([#9914](https://github.com/MetaMask/core/pull/9914))
- `scanUrl` now reports the underlying error message in `fetchError` for network errors instead of `'timeout of 8000ms exceeded'` ([#9914](https://github.com/MetaMask/core/pull/9914))
- Requests are no longer retried by default; the previous in-controller implementation made a single request per call, and the controller's timeouts are sized for one attempt. Pass `policyOptions.maxRetries` to opt back in ([#9914](https://github.com/MetaMask/core/pull/9914))

@mcmire mcmire Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass policyOptions.maxRetries to what? It would be helpful to more clear about which class is being changed here.

- Client state migrations should remove these properties from persisted `PhishingController` state
- **BREAKING:** Remove the `urlScanCacheTTL`, `urlScanCacheMaxSize`, `tokenScanCacheTTL`, `tokenScanCacheMaxSize`, `addressScanCacheTTL`, and `addressScanCacheMaxSize` options from `PhishingControllerOptions`; scan result freshness is now controlled by `SCAN_RESULT_STALE_TIME` in `PhishingDataService` ([#9914](https://github.com/MetaMask/core/pull/9914))
- Tokens for which the bulk scanning API returns no result are now negatively cached for `SCAN_RESULT_STALE_TIME` instead of being re-requested on every call ([#9914](https://github.com/MetaMask/core/pull/9914))
- `scanUrl` now reports the underlying error message in `fetchError` for network errors instead of `'timeout of 8000ms exceeded'` ([#9914](https://github.com/MetaMask/core/pull/9914))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we clarify that it's scanUrl in PhishingController that's being changed?

- `scanUrl` now reports the underlying error message in `fetchError` for network errors instead of `'timeout of 8000ms exceeded'` ([#9914](https://github.com/MetaMask/core/pull/9914))
- Requests are no longer retried by default; the previous in-controller implementation made a single request per call, and the controller's timeouts are sized for one attempt. Pass `policyOptions.maxRetries` to opt back in ([#9914](https://github.com/MetaMask/core/pull/9914))
- Malformed API responses (e.g. a stalelist without a numeric `lastUpdated`, or scan results without a `recommendedAction`/`result_type`) are now rejected and treated as request failures instead of being passed through, and are not cached ([#9914](https://github.com/MetaMask/core/pull/9914))
- `bulkScanUrls` now returns the results it was able to resolve even if some lookups fail, reporting the failures per URL in `errors`; it only rejects when no result could be resolved at all. Previously a single failed lookup discarded every result in the batch, including cached `BLOCK` verdicts for unrelated URLs ([#9914](https://github.com/MetaMask/core/pull/9914))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using multiple sentences for a changelog entry is a sign that it's too long, can more details be given in a sub-bullet?

- Requests are no longer retried by default; the previous in-controller implementation made a single request per call, and the controller's timeouts are sized for one attempt. Pass `policyOptions.maxRetries` to opt back in ([#9914](https://github.com/MetaMask/core/pull/9914))
- Malformed API responses (e.g. a stalelist without a numeric `lastUpdated`, or scan results without a `recommendedAction`/`result_type`) are now rejected and treated as request failures instead of being passed through, and are not cached ([#9914](https://github.com/MetaMask/core/pull/9914))
- `bulkScanUrls` now returns the results it was able to resolve even if some lookups fail, reporting the failures per URL in `errors`; it only rejects when no result could be resolved at all. Previously a single failed lookup discarded every result in the batch, including cached `BLOCK` verdicts for unrelated URLs ([#9914](https://github.com/MetaMask/core/pull/9914))
- `bulkScanUrls` no longer caches a "no result" verdict for URLs the API reported an error for, so those URLs are retried on the next call instead of being silently skipped for a minute ([#9914](https://github.com/MetaMask/core/pull/9914))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a bug being fixed? If so should this be in the "Fixed" category?

- Requests are no longer retried by default; the previous in-controller implementation made a single request per call, and the controller's timeouts are sized for one attempt. Pass `policyOptions.maxRetries` to opt back in ([#9914](https://github.com/MetaMask/core/pull/9914))
- Malformed API responses (e.g. a stalelist without a numeric `lastUpdated`, or scan results without a `recommendedAction`/`result_type`) are now rejected and treated as request failures instead of being passed through, and are not cached ([#9914](https://github.com/MetaMask/core/pull/9914))
- `bulkScanUrls` now returns the results it was able to resolve even if some lookups fail, reporting the failures per URL in `errors`; it only rejects when no result could be resolved at all. Previously a single failed lookup discarded every result in the batch, including cached `BLOCK` verdicts for unrelated URLs ([#9914](https://github.com/MetaMask/core/pull/9914))
- `bulkScanUrls` no longer caches a "no result" verdict for URLs the API reported an error for, so those URLs are retried on the next call instead of being silently skipped for a minute ([#9914](https://github.com/MetaMask/core/pull/9914))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Usually we note in the changelog when dependencies are added or updated. Can we do this for @metamask/base-data-service and @metamask/storage-service?

Suggested change
- `bulkScanUrls` no longer caches a "no result" verdict for URLs the API reported an error for, so those URLs are retried on the next call instead of being silently skipped for a minute ([#9914](https://github.com/MetaMask/core/pull/9914))
- `bulkScanUrls` no longer caches a "no result" verdict for URLs the API reported an error for, so those URLs are retried on the next call instead of being silently skipped for a minute ([#9914](https://github.com/MetaMask/core/pull/9914))
- Add `@metamask/base-data-service` (`^2.0.0`) as a dependency ([#9914](https://github.com/MetaMask/core/pull/9914))
- Add `@metamask/base-data-service` (`^2.0.0`) as a dependency ([#9914](https://github.com/MetaMask/core/pull/9914))

// Deliberately not routed through `fetchQuery`. Approvals reflect live,
// account-specific state that is never cached, so the query cache would
// provide no benefit while publishing the response on the messenger as a
// `cacheUpdated` payload. This matches the handling of non-cached POSTs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchQuery provides some other benefits, for instance, you can pass a responseStruct option to automatically validate the response. Could you pass staleTime: 0 to bypass the caching? I think this is usually the preferred approach in other packages.

adonesky1 and others added 7 commits September 10, 2026 11:44
Queries no longer wait indefinitely for StorageService:getItem after init();
the wait is bounded by a new hydrationTimeout (default 1s). Persisted caches
are shape-validated before hydration and a shouldHydrateQuery filter lets
services validate individual persisted queries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
applyDiffs previously indexed listSets with the diff's list type and threw a
TypeError for any type it did not know, and the stricter hotlist struct in
this branch turned the same case into a rejected hotlist response that left
phishingLists empty. Unknown list types are now skipped per diff. Also adds
normalizeScanAddress for lowercasing EVM addresses in scan keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A single unrecognized verdict or malformed nested field previously rejected
the whole bulk URL, bulk token, or approvals response, dropping every BLOCK
and Malicious verdict in the batch. Entries are now validated individually:
malformed URL results are reported per URL, malformed token results and
approvals are omitted. Hotlist diffs may target unknown list types and the
C2 blocklist no longer requires lastFetchedAt, which the controller never
reads. Bulk-seeded cache entries now carry a hostname so scanUrl always
returns one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bounded rehydration wait added an asynchronous hop before every query
even for services that never call init(). That hop delays request timers
relative to callers' own timeouts; in the phishing controller's timeout
tests the request outlived the test, was aborted during teardown, and
TanStack then scheduled a real five-minute gcTime timer on the destroyed
query, keeping the Jest process alive. Only await rehydration when an
initialization promise exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Address lowercasing lived only in PhishingController, while the service's
query keys became public API for UI consumers. A mixed-case token or address
passed directly to the service missed the API's lowercase response keys and
negatively cached a null verdict. The service now lowercases EVM addresses
for both the request and the cache key; non-EVM addresses are unchanged.
Also corrects the scanToken JSDoc, which claimed concurrent calls coalesce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed scan results

With policyOptions.maxRetries enabled, each item query backed by a failed
batch retried on its own and de-batched into single-item requests (10 URLs
against a failing host produced 31 POSTs). The batch POST now runs under the
service policy and item-level batch errors are excluded from retries, so a
failed batch is retried intact. Persisted scan results are validated against
their endpoint shapes before hydration; entries that fail, and any query
other than a scan result, are discarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@mcmire mcmire left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still working through this. Left some more comments.

},
},
},
// Circuit breaking is disabled by default: this service talks to four

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we considered creating four data services, one for each API? Would that it easier to finetune the behavior? Or is our goal here to mimic what the controller was doing before?

* @param operation - The asynchronous operation to execute.
* @returns The operation result.
*/
protected async executeWithPolicy<Result>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it makes more sense to expose the policy property rather than wrapping policy.execute()? I recall data services from other teams where it seems like it would have been nice to have access to the policy object.

init: RequestInit,
timeout?: number,
): Promise<Json> {
const controller = new AbortController();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have an AbortController kept under this.#abortController. Why are we creating another one?

async load(key: string): Promise<Json | null> {
return new Promise((resolve, reject) => {
pending.push({ key, resolve, reject });
// Items registered outside an explicit flush (e.g. by the retry

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this is necessary, can you shine some light here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants