From a53b48e4ef2576b98a6c914157af01bc22be2e61 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Wed, 1 Jul 2026 21:37:01 -0600 Subject: [PATCH 1/2] feat: aus leaderboard preferences --- .../authenticated-user-storage/CHANGELOG.md | 6 + ...icated-user-storage-method-action-types.ts | 30 ++- .../src/authenticated-user-storage.test.ts | 217 ++++++++++++++++++ .../src/authenticated-user-storage.ts | 87 +++++++ .../authenticated-user-storage/src/index.ts | 3 + .../authenticated-user-storage/src/types.ts | 9 + .../src/validators.ts | 32 +++ .../fixtures/authenticated-userstorage.ts | 30 +++ .../tests/mocks/authenticated-userstorage.ts | 12 + 9 files changed, 425 insertions(+), 1 deletion(-) diff --git a/packages/authenticated-user-storage/CHANGELOG.md b/packages/authenticated-user-storage/CHANGELOG.md index a261a40550..ff98a84ee8 100644 --- a/packages/authenticated-user-storage/CHANGELOG.md +++ b/packages/authenticated-user-storage/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `getLeaderboardPreferences` and `setLeaderboardPreferences` methods to `AuthenticatedUserStorageService` for reading and persisting the authenticated user's Top Traders leaderboard preferences, along with the corresponding messenger actions (`AuthenticatedUserStorageService:getLeaderboardPreferences`, `AuthenticatedUserStorageService:setLeaderboardPreferences`) and the `LeaderboardPreferences` type ([#XXXX](https://github.com/MetaMask/core/pull/XXXX)) + - `getLeaderboardPreferences` returns the leaderboard-preferences blob or `null` on 404, mirroring `getAssetsWatchlist`. + - The blob (`{ version: 1, optedOut: boolean }`) is stored in its own `/preferences/leaderboard` AUS namespace, independent of `NotificationPreferences`. + ## [3.0.0] ### Added diff --git a/packages/authenticated-user-storage/src/authenticated-user-storage-method-action-types.ts b/packages/authenticated-user-storage/src/authenticated-user-storage-method-action-types.ts index b400de367c..35f31b4a4b 100644 --- a/packages/authenticated-user-storage/src/authenticated-user-storage-method-action-types.ts +++ b/packages/authenticated-user-storage/src/authenticated-user-storage-method-action-types.ts @@ -85,6 +85,32 @@ export type AuthenticatedUserStorageServiceSetAssetsWatchlistAction = { handler: AuthenticatedUserStorageService['setAssetsWatchlist']; }; +/** + * Returns the Top Traders leaderboard preferences for the authenticated user. + * + * @returns The leaderboard preferences blob, or `null` if none has been set + * (404). + */ +export type AuthenticatedUserStorageServiceGetLeaderboardPreferencesAction = { + type: `AuthenticatedUserStorageService:getLeaderboardPreferences`; + handler: AuthenticatedUserStorageService['getLeaderboardPreferences']; +}; + +/** + * Creates or updates the Top Traders leaderboard preferences for the + * authenticated user. + * + * @param blob - The full leaderboard preferences blob. + * @param clientType - Optional client type header. + * @throws A `StructError` from `@metamask/superstruct` if `blob` is + * structurally invalid; an `HttpError` from `@metamask/controller-utils` if + * the API responds with a non-2xx status. + */ +export type AuthenticatedUserStorageServiceSetLeaderboardPreferencesAction = { + type: `AuthenticatedUserStorageService:setLeaderboardPreferences`; + handler: AuthenticatedUserStorageService['setLeaderboardPreferences']; +}; + /** * Union of all AuthenticatedUserStorageService action types. */ @@ -95,4 +121,6 @@ export type AuthenticatedUserStorageServiceMethodActions = | AuthenticatedUserStorageServiceGetNotificationPreferencesAction | AuthenticatedUserStorageServicePutNotificationPreferencesAction | AuthenticatedUserStorageServiceGetAssetsWatchlistAction - | AuthenticatedUserStorageServiceSetAssetsWatchlistAction; + | AuthenticatedUserStorageServiceSetAssetsWatchlistAction + | AuthenticatedUserStorageServiceGetLeaderboardPreferencesAction + | AuthenticatedUserStorageServiceSetLeaderboardPreferencesAction; diff --git a/packages/authenticated-user-storage/src/authenticated-user-storage.test.ts b/packages/authenticated-user-storage/src/authenticated-user-storage.test.ts index 6b8ed45cda..613264cc11 100644 --- a/packages/authenticated-user-storage/src/authenticated-user-storage.test.ts +++ b/packages/authenticated-user-storage/src/authenticated-user-storage.test.ts @@ -14,6 +14,8 @@ import { handleMockPutNotificationPreferences, handleMockGetAssetsWatchlist, handleMockSetAssetsWatchlist, + handleMockGetLeaderboardPreferences, + handleMockSetLeaderboardPreferences, } from '../tests/fixtures/authenticated-userstorage'; import { MOCK_DELEGATION_RESPONSE, @@ -22,6 +24,9 @@ import { MOCK_NOTIFICATION_PREFERENCES, MOCK_ASSETS_WATCHLIST_BLOB, MOCK_ASSETS_WATCHLIST_URL, + MOCK_LEADERBOARD_PREFERENCES, + MOCK_LEADERBOARD_PREFERENCES_URL, + MOCK_INVALID_LEADERBOARD_PREFERENCES, } from '../tests/mocks/authenticated-userstorage'; import type { AuthenticatedUserStorageMessenger } from './authenticated-user-storage'; import { @@ -463,6 +468,182 @@ describe('AuthenticatedUserStorageService', () => { }); }); + describe('AuthenticatedUserStorageService:getLeaderboardPreferences', () => { + it('returns the leaderboard preferences via the messenger', async () => { + handleMockGetLeaderboardPreferences(); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'AuthenticatedUserStorageService:getLeaderboardPreferences', + ); + + expect(result).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + }); + }); + + describe('AuthenticatedUserStorageService:setLeaderboardPreferences', () => { + it('sets the leaderboard preferences via the messenger', async () => { + const mock = handleMockSetLeaderboardPreferences(); + const { rootMessenger } = createService(); + + await rootMessenger.call( + 'AuthenticatedUserStorageService:setLeaderboardPreferences', + MOCK_LEADERBOARD_PREFERENCES, + ); + + expect(mock.isDone()).toBe(true); + }); + }); + + describe('getLeaderboardPreferences', () => { + it('returns the leaderboard preferences from the API', async () => { + const mock = handleMockGetLeaderboardPreferences(); + const { service } = createService(); + + const result = await service.getLeaderboardPreferences(); + + expect(mock.isDone()).toBe(true); + expect(result).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + }); + + it('sends the Authorization header', async () => { + const scope = nock(MOCK_LEADERBOARD_PREFERENCES_URL, { + reqheaders: { + authorization: 'Bearer mock-access-token', + }, + }) + .get('') + .reply(200, MOCK_LEADERBOARD_PREFERENCES); + + const { service } = createService(); + const result = await service.getLeaderboardPreferences(); + + expect(scope.isDone()).toBe(true); + expect(result).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + }); + + it('returns null when the leaderboard preferences are not found', async () => { + handleMockGetLeaderboardPreferences({ status: 404 }); + const { service } = createService(); + + const result = await service.getLeaderboardPreferences(); + + expect(result).toBeNull(); + }); + + it('throws when the API returns a non-200/404 status', async () => { + handleMockGetLeaderboardPreferences({ status: 500 }); + const { service } = createService(); + + await expect(service.getLeaderboardPreferences()).rejects.toThrow( + 'Failed to get leaderboard preferences: 500', + ); + }); + + it('throws when the response body is malformed', async () => { + handleMockGetLeaderboardPreferences({ + status: 200, + body: MOCK_INVALID_LEADERBOARD_PREFERENCES, + }); + const { service } = createService(); + + await expect(service.getLeaderboardPreferences()).rejects.toThrow( + /Expected.*but received/u, + ); + }); + + it('caches the result so a second call within staleTime does not re-fetch', async () => { + const scope = nock(MOCK_LEADERBOARD_PREFERENCES_URL) + .get('') + .once() + .reply(200, MOCK_LEADERBOARD_PREFERENCES); + const { service } = createService(); + + const first = await service.getLeaderboardPreferences(); + const second = await service.getLeaderboardPreferences(); + + expect(scope.isDone()).toBe(true); + expect(first).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + expect(second).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + }); + }); + + describe('setLeaderboardPreferences', () => { + it('submits the leaderboard preferences to the API', async () => { + const mock = handleMockSetLeaderboardPreferences(); + const { service } = createService(); + + await service.setLeaderboardPreferences(MOCK_LEADERBOARD_PREFERENCES); + + expect(mock.isDone()).toBe(true); + }); + + it('sends the correct request body', async () => { + handleMockSetLeaderboardPreferences(undefined, async (_, requestBody) => { + expect(requestBody).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + }); + const { service } = createService(); + + await service.setLeaderboardPreferences(MOCK_LEADERBOARD_PREFERENCES); + }); + + it('sends Content-Type and Authorization headers but no X-Client-Type when clientType is omitted', async () => { + const scope = nock(MOCK_LEADERBOARD_PREFERENCES_URL, { + reqheaders: { + 'content-type': 'application/json', + authorization: 'Bearer mock-access-token', + }, + badheaders: ['x-client-type'], + }) + .put('') + .reply(200); + const { service } = createService(); + + await service.setLeaderboardPreferences(MOCK_LEADERBOARD_PREFERENCES); + + expect(scope.isDone()).toBe(true); + }); + + it('includes X-Client-Type header when clientType is provided', async () => { + const scope = nock(MOCK_LEADERBOARD_PREFERENCES_URL, { + reqheaders: { + 'x-client-type': 'mobile', + }, + }) + .put('') + .reply(200); + const { service } = createService(); + + await service.setLeaderboardPreferences( + MOCK_LEADERBOARD_PREFERENCES, + 'mobile', + ); + + expect(scope.isDone()).toBe(true); + }); + + it('throws when the API returns a non-200 status', async () => { + handleMockSetLeaderboardPreferences({ status: 400 }); + const { service } = createService(); + + await expect( + service.setLeaderboardPreferences(MOCK_LEADERBOARD_PREFERENCES), + ).rejects.toThrow('Failed to put leaderboard preferences: 400'); + }); + + it('throws a structural error before sending the request when the blob is malformed', async () => { + const { service } = createService(); + const malformed = { + version: 2, + optedOut: true, + } as unknown as Parameters[0]; + + await expect( + service.setLeaderboardPreferences(malformed), + ).rejects.toThrow(/At path: version -- Expected the literal/u); + }); + }); + describe('cache invalidation', () => { it('invalidates listDelegations cache after createDelegation', async () => { handleMockCreateDelegation(); @@ -542,6 +723,42 @@ describe('AuthenticatedUserStorageService', () => { expect(first).toStrictEqual(MOCK_ASSETS_WATCHLIST_BLOB); expect(second).toStrictEqual(updatedBlob); }); + + it('invalidates getLeaderboardPreferences cache after setLeaderboardPreferences', async () => { + handleMockSetLeaderboardPreferences(); + handleMockGetLeaderboardPreferences(); + const { service } = createService(); + const invalidateSpy = jest.spyOn(service, 'invalidateQueries'); + + await service.setLeaderboardPreferences(MOCK_LEADERBOARD_PREFERENCES); + + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: ['AuthenticatedUserStorageService:getLeaderboardPreferences'], + }); + }); + + it('causes a subsequent getLeaderboardPreferences to refetch after setLeaderboardPreferences', async () => { + const updatedBlob = { + version: 1 as const, + optedOut: false, + }; + const getScope = nock(MOCK_LEADERBOARD_PREFERENCES_URL) + .get('') + .reply(200, MOCK_LEADERBOARD_PREFERENCES) + .put('') + .reply(200) + .get('') + .reply(200, updatedBlob); + + const { service } = createService(); + const first = await service.getLeaderboardPreferences(); + await service.setLeaderboardPreferences(updatedBlob); + const second = await service.getLeaderboardPreferences(); + + expect(getScope.isDone()).toBe(true); + expect(first).toStrictEqual(MOCK_LEADERBOARD_PREFERENCES); + expect(second).toStrictEqual(updatedBlob); + }); }); describe('authorization', () => { diff --git a/packages/authenticated-user-storage/src/authenticated-user-storage.ts b/packages/authenticated-user-storage/src/authenticated-user-storage.ts index 2dd3bb29ad..eb9d277c95 100644 --- a/packages/authenticated-user-storage/src/authenticated-user-storage.ts +++ b/packages/authenticated-user-storage/src/authenticated-user-storage.ts @@ -17,12 +17,14 @@ import type { ClientType, DelegationResponse, DelegationSubmission, + LeaderboardPreferences, NotificationPreferences, } from './types'; import { assertAssetsWatchlistBlob, assertAssetsWatchlistBlobForWrite, assertDelegationResponseArray, + assertLeaderboardPreferences, assertNotificationPreferences, } from './validators'; @@ -54,6 +56,8 @@ const MESSENGER_EXPOSED_METHODS = [ 'putNotificationPreferences', 'getAssetsWatchlist', 'setAssetsWatchlist', + 'getLeaderboardPreferences', + 'setLeaderboardPreferences', ] as const; /** @@ -432,6 +436,89 @@ export class AuthenticatedUserStorageService extends BaseDataService< }); } + /** + * Returns the Top Traders leaderboard preferences for the authenticated user. + * + * @returns The leaderboard preferences blob, or `null` if none has been set + * (404). + */ + async getLeaderboardPreferences(): Promise { + const url = `${getAuthenticatedStorageUrl(this.#environment)}/preferences/leaderboard`; + + const data = await this.fetchQuery({ + queryKey: [`${this.name}:getLeaderboardPreferences`], + queryFn: async () => { + const headers = await this.#getHeaders(); + const response = await fetch(url, { headers }); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new HttpError( + response.status, + `Failed to get leaderboard preferences: ${response.status}`, + ); + } + + return response.json(); + }, + }); + + if (data === null) { + return null; + } + + assertLeaderboardPreferences(data); + return data; + } + + /** + * Creates or updates the Top Traders leaderboard preferences for the + * authenticated user. + * + * @param blob - The full leaderboard preferences blob. + * @param clientType - Optional client type header. + * @throws A `StructError` from `@metamask/superstruct` if `blob` is + * structurally invalid; an `HttpError` from `@metamask/controller-utils` if + * the API responds with a non-2xx status. + */ + async setLeaderboardPreferences( + blob: LeaderboardPreferences, + clientType?: ClientType, + ): Promise { + assertLeaderboardPreferences(blob); + + const url = `${getAuthenticatedStorageUrl(this.#environment)}/preferences/leaderboard`; + + await this.fetchQuery({ + queryKey: [`${this.name}:setLeaderboardPreferences`, blob as unknown as Json], + staleTime: 0, + queryFn: async () => { + const headers = await this.#getHeaders(clientType); + const response = await fetch(url, { + method: 'PUT', + headers, + body: JSON.stringify(blob), + }); + + if (!response.ok) { + throw new HttpError( + response.status, + `Failed to put leaderboard preferences: ${response.status}`, + ); + } + + return null; + }, + }); + + await this.invalidateQueries({ + queryKey: [`${this.name}:getLeaderboardPreferences`], + }); + } + async #getHeaders(clientType?: ClientType): Promise> { const accessToken = await this.messenger.call( 'AuthenticationController:getBearerToken', diff --git a/packages/authenticated-user-storage/src/index.ts b/packages/authenticated-user-storage/src/index.ts index 96d3efec1b..c2ada34ba2 100644 --- a/packages/authenticated-user-storage/src/index.ts +++ b/packages/authenticated-user-storage/src/index.ts @@ -23,6 +23,8 @@ export type { AuthenticatedUserStorageServicePutNotificationPreferencesAction, AuthenticatedUserStorageServiceGetAssetsWatchlistAction, AuthenticatedUserStorageServiceSetAssetsWatchlistAction, + AuthenticatedUserStorageServiceGetLeaderboardPreferencesAction, + AuthenticatedUserStorageServiceSetLeaderboardPreferencesAction, } from './authenticated-user-storage-method-action-types'; export { getUserStorageApiUrl } from './env'; export type { Environment } from './env'; @@ -43,5 +45,6 @@ export type { PriceAlertPreference, NotificationPreferences, AssetsWatchlistBlob, + LeaderboardPreferences, ClientType, } from './types'; diff --git a/packages/authenticated-user-storage/src/types.ts b/packages/authenticated-user-storage/src/types.ts index f3a2323a19..3585ab8cb8 100644 --- a/packages/authenticated-user-storage/src/types.ts +++ b/packages/authenticated-user-storage/src/types.ts @@ -135,6 +135,15 @@ export type NotificationPreferences = { // one file keeps the two in lock-step. export type { AssetsWatchlistBlob } from './validators'; +// --------------------------------------------------------------------------- +// Leaderboard preferences +// --------------------------------------------------------------------------- + +// `LeaderboardPreferences` is inferred from `LeaderboardPreferencesSchema` in +// `./validators` and re-exported here so the public type surface remains in +// `./types`. +export type { LeaderboardPreferences } from './validators'; + // --------------------------------------------------------------------------- // Shared // --------------------------------------------------------------------------- diff --git a/packages/authenticated-user-storage/src/validators.ts b/packages/authenticated-user-storage/src/validators.ts index 8e5eaa0b9a..657c00a441 100644 --- a/packages/authenticated-user-storage/src/validators.ts +++ b/packages/authenticated-user-storage/src/validators.ts @@ -236,3 +236,35 @@ export function assertAssetsWatchlistBlobForWrite( ): asserts data is AssetsWatchlistBlob { assert(data, AssetsWatchlistBlobWriteSchema); } + +/** + * The authenticated user's Top Traders leaderboard preferences: a mutable + * per-user singleton blob. + * + * The `version` literal is carried inside the blob (not in the URL) so the + * schema can evolve in a backwards-compatible way. + */ +const LeaderboardPreferencesSchema = type({ + version: literal(1), + optedOut: boolean(), +}); + +/** + * The authenticated user's Top Traders leaderboard preferences. + * + * Inferred from {@link LeaderboardPreferencesSchema} so the runtime schema and + * the static type stay in lock-step. + */ +export type LeaderboardPreferences = Infer; + +/** + * Asserts that the given value is a valid `LeaderboardPreferences`. + * + * @param data - The unknown value to validate. + * @throws If the value does not match the expected schema. + */ +export function assertLeaderboardPreferences( + data: unknown, +): asserts data is LeaderboardPreferences { + assert(data, LeaderboardPreferencesSchema); +} diff --git a/packages/authenticated-user-storage/tests/fixtures/authenticated-userstorage.ts b/packages/authenticated-user-storage/tests/fixtures/authenticated-userstorage.ts index 564ab5f1a0..c28d49b939 100644 --- a/packages/authenticated-user-storage/tests/fixtures/authenticated-userstorage.ts +++ b/packages/authenticated-user-storage/tests/fixtures/authenticated-userstorage.ts @@ -5,6 +5,8 @@ import { MOCK_ASSETS_WATCHLIST_URL, MOCK_DELEGATIONS_URL, MOCK_DELEGATION_RESPONSE, + MOCK_LEADERBOARD_PREFERENCES, + MOCK_LEADERBOARD_PREFERENCES_URL, MOCK_NOTIFICATION_PREFERENCES, MOCK_NOTIFICATION_PREFERENCES_URL, } from '../mocks/authenticated-userstorage'; @@ -103,3 +105,31 @@ export function handleMockSetAssetsWatchlist( } return interceptor.reply(reply.status, reply.body); } + +export function handleMockGetLeaderboardPreferences( + mockReply?: MockReply, +): nock.Scope { + const reply = mockReply ?? { + status: 200, + body: MOCK_LEADERBOARD_PREFERENCES, + }; + return nock(MOCK_LEADERBOARD_PREFERENCES_URL) + .persist() + .get('') + .reply(reply.status, reply.body); +} + +export function handleMockSetLeaderboardPreferences( + mockReply?: MockReply, + callback?: (uri: string, requestBody: nock.Body) => Promise, +): nock.Scope { + const reply = mockReply ?? { status: 200 }; + const interceptor = nock(MOCK_LEADERBOARD_PREFERENCES_URL).persist().put(''); + + if (callback) { + return interceptor.reply(reply.status, async (uri, requestBody) => { + return callback(uri, requestBody); + }); + } + return interceptor.reply(reply.status, reply.body); +} diff --git a/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts b/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts index bb12a5f516..995e695c26 100644 --- a/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts +++ b/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts @@ -3,6 +3,7 @@ import type { AssetsWatchlistBlob, DelegationResponse, DelegationSubmission, + LeaderboardPreferences, NotificationPreferences, } from '../../src/types'; import { DEFAULT_PRICE_ALERT_PREFERENCES } from '../../src/validators'; @@ -10,6 +11,7 @@ import { DEFAULT_PRICE_ALERT_PREFERENCES } from '../../src/validators'; export const MOCK_DELEGATIONS_URL = `${getAuthenticatedStorageUrl('prod')}/delegations`; export const MOCK_NOTIFICATION_PREFERENCES_URL = `${getAuthenticatedStorageUrl('prod')}/preferences/notifications`; export const MOCK_ASSETS_WATCHLIST_URL = `${getAuthenticatedStorageUrl('prod')}/assets-watchlist`; +export const MOCK_LEADERBOARD_PREFERENCES_URL = `${getAuthenticatedStorageUrl('prod')}/preferences/leaderboard`; export const MOCK_DELEGATION_SUBMISSION: DelegationSubmission = { signedDelegation: { @@ -88,3 +90,13 @@ export const MOCK_INVALID_ASSETS_WATCHLIST_BLOB = { version: 2, assets: 'not-an-array', } as const; + +export const MOCK_LEADERBOARD_PREFERENCES: LeaderboardPreferences = { + version: 1, + optedOut: true, +}; + +export const MOCK_INVALID_LEADERBOARD_PREFERENCES = { + version: 2, + optedOut: 'nope', +} as const; From 4b67793f8e572b628adadac2a73d3647e78cbca9 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Thu, 2 Jul 2026 08:28:23 -0600 Subject: [PATCH 2/2] fix(authenticated-user-storage): wrap long queryKey line and link changelog to PR #9355 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/authenticated-user-storage/CHANGELOG.md | 2 +- .../src/authenticated-user-storage.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/authenticated-user-storage/CHANGELOG.md b/packages/authenticated-user-storage/CHANGELOG.md index ff98a84ee8..33b12f3bfa 100644 --- a/packages/authenticated-user-storage/CHANGELOG.md +++ b/packages/authenticated-user-storage/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `getLeaderboardPreferences` and `setLeaderboardPreferences` methods to `AuthenticatedUserStorageService` for reading and persisting the authenticated user's Top Traders leaderboard preferences, along with the corresponding messenger actions (`AuthenticatedUserStorageService:getLeaderboardPreferences`, `AuthenticatedUserStorageService:setLeaderboardPreferences`) and the `LeaderboardPreferences` type ([#XXXX](https://github.com/MetaMask/core/pull/XXXX)) +- Add `getLeaderboardPreferences` and `setLeaderboardPreferences` methods to `AuthenticatedUserStorageService` for reading and persisting the authenticated user's Top Traders leaderboard preferences, along with the corresponding messenger actions (`AuthenticatedUserStorageService:getLeaderboardPreferences`, `AuthenticatedUserStorageService:setLeaderboardPreferences`) and the `LeaderboardPreferences` type ([#9355](https://github.com/MetaMask/core/pull/9355)) - `getLeaderboardPreferences` returns the leaderboard-preferences blob or `null` on 404, mirroring `getAssetsWatchlist`. - The blob (`{ version: 1, optedOut: boolean }`) is stored in its own `/preferences/leaderboard` AUS namespace, independent of `NotificationPreferences`. diff --git a/packages/authenticated-user-storage/src/authenticated-user-storage.ts b/packages/authenticated-user-storage/src/authenticated-user-storage.ts index eb9d277c95..05e75811fc 100644 --- a/packages/authenticated-user-storage/src/authenticated-user-storage.ts +++ b/packages/authenticated-user-storage/src/authenticated-user-storage.ts @@ -493,7 +493,10 @@ export class AuthenticatedUserStorageService extends BaseDataService< const url = `${getAuthenticatedStorageUrl(this.#environment)}/preferences/leaderboard`; await this.fetchQuery({ - queryKey: [`${this.name}:setLeaderboardPreferences`, blob as unknown as Json], + queryKey: [ + `${this.name}:setLeaderboardPreferences`, + blob as unknown as Json, + ], staleTime: 0, queryFn: async () => { const headers = await this.#getHeaders(clientType);