feat(authenticated-user-storage): add user custom tokens feature (imported/hidden assets) - #10233
Open
Prithpal-Sooriya wants to merge 5 commits into
Open
Prithpal-Sooriya wants to merge 5 commits into
Prithpal-Sooriya wants to merge 5 commits into
Conversation
…orted/hidden assets) Add a user-assets domain to the Authenticated User Storage SDK, backed by the new GET/PUT /preferences/user-assets endpoints: - getUserAssets / setUserAssets: low-level blob access, mirroring the assets-watchlist pattern (404 -> null on read, strict write validation) - importTokens / hideTokens: high-level API that handles deduplication and mutual exclusivity internally, returning the resolved blob - Fail-open conflict resolution: an identifier present in both importedAssets and hiddenAssets stays imported and is removed from hiddenAssets; writes are never rejected due to a conflict - Strict write-side schema validation: every entry must be a CAIP-19 asset identifier (CaipAssetTypeStruct); reads stay lenient Jira: ASSETS-3937
Per review feedback on #10233: keep JSDoc to concise summaries with the necessary param/returns/throws tags instead of multi-paragraph essays. The generated action-types file is regenerated to match. No functional change.
…s to it.each tables Per review feedback on #10233: the mirrored importTokens/hideTokens tests, the error-status and malformed-blob cases, and the cache invalidation trio are now table-driven via it.each, split per scenario. Adds a hiddenAssets-side malformed-blob row; all existing assertions preserved.
…assets writes Per review feedback on #10233: add a UserAssetsBlobNormalizedWriteSchema refinement asserting no identifier appears in both importedAssets and hiddenAssets, applied to the normalized blob right before the write. Structural validation of the raw input still fails fast, but the exclusivity check runs after fail-open normalization, so a write can never be rejected because of a conflict — it only guards against internal logic bugs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Jira: ASSETS-3937
Pattern references: #8836 (assets-watchlist domain), #9441 (
/preferences/*endpoint path)Design doc: ADR
0002-authenticated-user-storage-for-user-imported-tokens(aus-implementation notes)Summary
Adds a user custom tokens domain to the Authenticated User Storage SDK: a per-user singleton blob
recording which tokens the user chose to import and which they chose to hide, keyed by CAIP-19
asset identifiers. The PR contains both the endpoint layer (HTTP contract + strict schema validation)
and the high-level SDK API (
importTokens/hideTokens) that hides the state-management complexity.API
getUserAssets(): Promise<UserAssetsBlob | null>GET /preferences/user-assets; returnsnullon 404 (first read), mirroringgetAssetsWatchlistsetUserAssets(blob, clientType?): Promise<void>PUT /preferences/user-assets; validates (strict) → normalizes → PUTs the normalized blob → invalidates the get cacheimportTokens(ids, clientType?): Promise<UserAssetsBlob>idsintoimportedAssets(dedup, order-preserving), removes them fromhiddenAssets(import wins), persists, returns the resolved blobhideTokens(ids, clientType?): Promise<UserAssetsBlob>idsintohiddenAssets(dedup), removes them fromimportedAssets(mutual exclusivity), persists, returns the resolved blobAll four are exposed as messenger actions (
AuthenticatedUserStorageService:getUserAssets,:setUserAssets,:importTokens,:hideTokens) viaMESSENGER_EXPOSED_METHODS; the action-typesfile is regenerated with
yarn messenger-action-types:generate.Semantics & guarantees
importedAssetsandhiddenAssets, the user's intent to import wins — it stays inimportedAssetsand is removed fromhiddenAssets. The write is never rejected because of aconflict. Implemented as a pure function (
normalizeUserAssetsBlob) applied on every write, soeven direct
setUserAssetscalls and pre-existing server-side conflicts are resolved.first occurrence wins) before every write, both in the high-level merge and in
setUserAssets.importTokensremoves ids fromhiddenAssets;hideTokensremoves idsfrom
importedAssets. Each identifier lives in at most one list.identifier (
CaipAssetTypeStructfrom@metamask/utils);versionis aliteral(1). Malformedblobs throw a superstruct
StructErrorbefore the request is sent. Read-side stays lenient(plain strings) so existing server data is never rejected — same philosophy as the watchlist blob.
importTokens/hideTokensvalidateidsbefore any network I/O.Backend contract (for the AUS server team)
The server implementation of
PUT /preferences/user-assetsshould mirror the SDK as defense-in-depth:version === 1,importedAssets/hiddenAssetsarrays of CAIP-19 identifiers.keep it in
importedAssetsand remove it fromhiddenAssets.GETreturns it verbatim (404 until first write).The SDK already sends normalized blobs, so the server rule is a safety net, not a dependency.
Known limitations
optimistic concurrency in the current API. Concurrent writers can lose updates; same trade-off as
the existing endpoints, out of scope here.
ASSETS_WATCHLIST_MAX_ASSETS) — none is specified by the ticket.Test plan
HttpError; malformedresponse body →
StructError; caching + cache invalidation (incl. refetch after write, and afterimportTokens/hideTokens); request-body assertions proving dedup and fail-open normalizationhappen before the request; mutual-exclusivity assertions both directions; CAIP-19 strictness on
write; fail-fast invalid ids (no network call made);
X-Client-Typeheader handling.yarn build(full monorepotsc --build): clean.yarn lint:eslint,yarn lint:misc:check(oxfmt),
yarn constraints,yarn changelog:validate,yarn readme-content:check,yarn messenger-action-types:check: all clean.Changelog
Entry added under
## [Unreleased]→### Added, linking this PR: #10233.Note
Medium Risk
Introduces new authenticated persistence for user token preferences with read-modify-write semantics (same concurrency trade-offs as other preference blobs), but follows established watchlist patterns and validates writes strictly.
Overview
Adds a user custom tokens domain to
@metamask/authenticated-user-storage, backed byGET/PUT /preferences/user-assets, so clients can persist which CAIP-19 tokens a user imported vs hidden.AuthenticatedUserStorageServicegainsgetUserAssets(404 →null, like the watchlist),setUserAssets, and high-levelimportTokens/hideTokensthat read-modify-write the blob. All four are exposed on the messenger and exported asUserAssetsBlob. Writes validate CAIP-19 ids strictly, dedupe lists, and fail-open on import/hide conflicts (import wins);importTokens/hideTokensalso keep the two lists mutually exclusive.getUserAssetscache is invalidated after writes.Docs, changelog, validators, and broad unit coverage (HTTP, caching, normalization, fail-fast validation) accompany the change.
Reviewed by Cursor Bugbot for commit 78fdd35. Bugbot is set up for automated code reviews on this repo. Configure here.