feat(watchlist): add local-storage backed watchlist storage utils [ASSETS-3115]#30337
Conversation
Adds initial storage integration for the WatchList feature (ASSETS-3115),
backed by the existing MMKV-based StorageWrapper. The AUS SDK is not yet
available, so this local-device implementation is intended to be swapped
out for the SDK once it ships, while preserving the same async API and
schema validation contract.
- Define WatchlistBlobSchema with @metamask/superstruct
(array of asset strings + version literal 1, both defaulted).
- Export WatchlistBlob type inferred from the schema.
- Export WATCHLIST_STORAGE_PATH ('watchlistV1.tokens') and EMPTY_BLOB.
- readFromTokenWatchList: returns EMPTY_BLOB when storage is empty,
otherwise parses JSON and validates through the schema before
returning to the caller (applying defaults for missing fields).
- writeToTokenWatchList: validates the input blob through the schema
before serializing and writing, so malformed data never reaches storage.
Tests cover empty storage, schema defaults, valid round-trips, and
invalid-input rejection for both read and write paths.
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
| const raw = await StorageWrapper.getItem(WATCHLIST_STORAGE_PATH); | ||
| if (!raw) return EMPTY_BLOB; | ||
| return create(JSON.parse(raw), WatchlistBlobSchema); |
There was a problem hiding this comment.
Satisfactory, as this will be replaced by SDK changes:
MetaMask/core#8836
| const WatchlistBlobSchema = object({ | ||
| assets: defaulted(array(string()), () => []), | ||
| version: defaulted(literal(1), () => 1 as const), | ||
| }); |
There was a problem hiding this comment.
SDK will offer the schema that we can import:
MetaMask/core#8836
For now to keep unblocked, we can continue to use this.
Address review feedback on ASSETS-3115: convert the three readFromTokenWatchList 'throws when ...' tests into a single it.each table parameterized on the stored value. Test descriptions are preserved verbatim so test output is unchanged.
Address review feedback on ASSETS-3115: convert the two readFromTokenWatchList 'applies ... defaults ...' tests into a single it.each table parameterized on the stored value and expected result. Test descriptions are preserved verbatim so test output is unchanged.
Address review feedback on ASSETS-3115: convert the three writeToTokenWatchList 'rejects and does not write ...' tests into a single it.each table parameterized on the invalid input. Test descriptions are preserved verbatim so test output is unchanged.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 8212ace. Configure here.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection:
Key findings from investigation:
Since no existing functionality is modified and no existing E2E tests cover this new isolated utility, there are no E2E test tags that need to run. The unit tests in the PR itself provide adequate coverage for this new module. Performance Test Selection: |
|




Description
Adds the initial Storage Integration layer for the WatchList feature, as scoped by ASSETS-3115 and the WatchList Tech Spec.
The AUS SDK changes are not yet available, so this PR introduces a temporary local-device implementation backed by the existing MMKV-based
StorageWrapper. The async API and schema-validation contract are designed to remain stable, so the body of the two exported functions can later be swapped to delegate to the AUS SDK without changes for callers.New module:
app/components/UI/assets/watchlist/storage.tsWatchlistBlobSchema—@metamask/superstructschema describing the persisted blob:assets: defaultedarray(string())version: defaultedliteral(1)WatchlistBlob— type inferred from the schema.WATCHLIST_STORAGE_PATH = 'watchlistV1.tokens'andEMPTY_BLOB = { assets: [], version: 1 }constants (matching the tech-spec snippet).readFromTokenWatchList(): Promise<WatchlistBlob>— reads fromStorageWrapper, returnsEMPTY_BLOBwhen no value is stored, otherwiseJSON.parses and validates throughWatchlistBlobSchema(applying schema defaults) before returning. Per ticket AC: "must parse through the defined schema before client can use it".writeToTokenWatchList(blob): Promise<void>— validates the input throughWatchlistBlobSchemafirst, thenJSON.stringifys and writes viaStorageWrapper. Per ticket AC: "must validate input before stringify and store".Note on file path: the ticket asks for
app/components/UI/assets/watchlist/storage.ts(lowercaseassets/). There is also an existingapp/components/UI/Assets/(capitalA). On case-insensitive filesystems (macOS APFS default, Windows NTFS default) these two folders will collide. Flagging for awareness — happy to rename toAssets/watchlist/(or another path) if preferred.Changelog
CHANGELOG entry: null
Related issues
Fixes: ASSETS-3115
Parent story: ASSETS-2912 — [Mobile][WatchList] Allow users to add, remove, and view their watchlisted tokens
Tech Spec: WatchList Tech Spec — Technical Breakdown Tasks
Related SDK PR (future swap-in): MetaMask/core#8836
Manual testing steps
This PR introduces internal utility functions with no UI surface yet, so there is nothing to test manually in-app. Verified via unit tests:
Screenshots/Recordings
Before
N/A — new utility module, no UI.
After
N/A — new utility module, no UI.
Pre-merge author checklist
storage.test.ts, 17 cases, all passing)Performance checks (if applicable)
Not applicable — internal storage helper, no perf-critical UI path or new in-app surface.
Pre-merge reviewer checklist