diff --git a/packages/authenticated-user-storage/CHANGELOG.md b/packages/authenticated-user-storage/CHANGELOG.md index 322a697481b..d1401d8ae9a 100644 --- a/packages/authenticated-user-storage/CHANGELOG.md +++ b/packages/authenticated-user-storage/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Route `setAssetsWatchlist` through `BaseDataService.executeMutation` so + watchlist writes are not cached, deduplicated, or retried. - Bump `@metamask/utils` from `^11.11.0` to `^11.12.0` ([#10076](https://github.com/MetaMask/core/pull/10076)) ### Removed 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 39c88ae9c5b..bf5202ea8ec 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 @@ -75,7 +75,8 @@ export type AuthenticatedUserStorageServiceGetAssetsWatchlistAction = { * contain at most `ASSETS_WATCHLIST_MAX_ASSETS` CAIP-19 asset identifiers; * this is enforced by `assertAssetsWatchlistBlobForWrite` before the * request is sent. - * @param clientType - Optional client type header. + * @param clientTypeOrGlobalId - Optional client type header or mutation ID. + * @param globalId - Optional mutation ID when a client type is provided. * @throws A `StructError` from `@metamask/superstruct` if `blob` is * structurally invalid or `assets` exceeds the cap; an `HttpError` from * `@metamask/controller-utils` if the API responds with a non-2xx status. 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 f0c5c3ee8f8..88b9729c1ff 100644 --- a/packages/authenticated-user-storage/src/authenticated-user-storage.test.ts +++ b/packages/authenticated-user-storage/src/authenticated-user-storage.test.ts @@ -461,6 +461,59 @@ describe('AuthenticatedUserStorageService', () => { expect(mock.isDone()).toBe(true); }); + + it('does not retry a failed request', async () => { + nock(MOCK_ASSETS_WATCHLIST_URL) + .put('') + .reply(500, { error: 'server error' }) + .put('') + .reply(200); + const { service } = createService(); + + await expect( + service.setAssetsWatchlist(MOCK_ASSETS_WATCHLIST_BLOB), + ).rejects.toThrow('Failed to put assets watchlist: 500'); + }); + + it('does not deduplicate concurrent requests', async () => { + const scope = nock(MOCK_ASSETS_WATCHLIST_URL).put('').twice().reply(200); + const { service } = createService(); + + await Promise.all([ + service.setAssetsWatchlist(MOCK_ASSETS_WATCHLIST_BLOB), + service.setAssetsWatchlist(MOCK_ASSETS_WATCHLIST_BLOB), + ]); + + expect(scope.isDone()).toBe(true); + }); + + it('includes the global ID in mutation cache updates', async () => { + handleMockSetAssetsWatchlist(); + const { service, messenger } = createService(); + const publishSpy = jest.spyOn(messenger, 'publish'); + + await service.setAssetsWatchlist(MOCK_ASSETS_WATCHLIST_BLOB, 'global-id'); + + expect(publishSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'AuthenticatedUserStorageService:cacheUpdated:', + ), + expect.objectContaining({ + objectType: 'mutation', + state: expect.objectContaining({ + mutations: [ + expect.objectContaining({ + mutationKey: [ + 'AuthenticatedUserStorageService:setAssetsWatchlist', + MOCK_ASSETS_WATCHLIST_BLOB, + ], + meta: { globalId: 'global-id' }, + }), + ], + }), + }), + ); + }); }); describe('cache invalidation', () => { diff --git a/packages/authenticated-user-storage/src/authenticated-user-storage.ts b/packages/authenticated-user-storage/src/authenticated-user-storage.ts index 2b14522fe3c..989d909d238 100644 --- a/packages/authenticated-user-storage/src/authenticated-user-storage.ts +++ b/packages/authenticated-user-storage/src/authenticated-user-storage.ts @@ -34,6 +34,19 @@ import { */ export const serviceName = 'AuthenticatedUserStorageService'; +/** + * Checks whether a value is a supported client type. + * + * This distinguishes the existing client-type argument from the optional + * mutation global ID when the method is invoked through a UI query client. + * + * @param value - The value to check. + * @returns Whether the value is a client type. + */ +function isClientType(value: string | undefined): value is ClientType { + return value === 'extension' || value === 'mobile' || value === 'portfolio'; +} + /** * Builds the versioned API base URL for a given environment. * @@ -392,23 +405,31 @@ export class AuthenticatedUserStorageService extends BaseDataService< * contain at most `ASSETS_WATCHLIST_MAX_ASSETS` CAIP-19 asset identifiers; * this is enforced by `assertAssetsWatchlistBlobForWrite` before the * request is sent. - * @param clientType - Optional client type header. + * @param clientTypeOrGlobalId - Optional client type header or mutation ID. + * @param globalId - Optional mutation ID when a client type is provided. * @throws A `StructError` from `@metamask/superstruct` if `blob` is * structurally invalid or `assets` exceeds the cap; an `HttpError` from * `@metamask/controller-utils` if the API responds with a non-2xx status. */ async setAssetsWatchlist( blob: AssetsWatchlistBlob, - clientType?: ClientType, + clientTypeOrGlobalId?: ClientType | string, + globalId?: string, ): Promise { assertAssetsWatchlistBlobForWrite(blob); const url = `${getAuthenticatedStorageUrl(this.#environment)}/preferences/assets-watchlist`; - - await this.fetchQuery({ - queryKey: [`${this.name}:setAssetsWatchlist`, blob as unknown as Json], - staleTime: 0, - queryFn: async () => { + const clientType = isClientType(clientTypeOrGlobalId) + ? clientTypeOrGlobalId + : undefined; + const mutationGlobalId = clientType + ? globalId + : (globalId ?? clientTypeOrGlobalId); + + await this.executeMutation({ + mutationKey: [`${this.name}:setAssetsWatchlist`, blob as Json], + globalId: mutationGlobalId, + mutationFn: async () => { const headers = await this.#getHeaders(clientType); const response = await fetch(url, { method: 'PUT',