Skip to content

Commit 5216c01

Browse files
authored
feat: init AUS watchlist integration (#31734)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Integrates the Perps watchlist with the **Authenticated User Storage (AUS) service**, so a user's watched markets are persisted to remote storage and synced across devices when they are authenticated. ### Changes **`@metamask/perps-controller` bump to `^9.0.0`** (`package.json`, `yarn.lock`) - The new controller version implements optimistic local updates and AUS sync inside `toggleWatchlistMarket`. On add/remove, it writes the updated watchlist into the `perps.watchlistMarkets` field of `NotificationPreferences` via `AuthenticatedUserStorageService:putNotificationPreferences`. It gracefully degrades to local-only when the user is unauthenticated or the preferences blob has not yet been initialised. **Messenger wiring** (`perps-controller-messenger/index.ts`) - Delegates `AuthenticatedUserStorageService:getNotificationPreferences` and `AuthenticatedUserStorageService:putNotificationPreferences` actions to the `PerpsControllerMessenger` so the controller can call them through the root messenger. **`usePerpsWatchlistActions` hook cleanup** (`usePerpsWatchlistActions.ts`) - Removed the `TODO` scaffolding comments for optimistic UI and User Storage API seams — those are now handled inside the controller. - `toggleWatchlistMarket` calls are now properly `await`-ed so errors surface and the error toast works correctly. **Integration tests** (`perps-controller/index.test.ts`, `perps-controller-messenger/index.test.ts`) - New integration test suite (`PerpsController watchlist ↔ AuthenticatedUserStorageService`) covering: add writes the symbol to AUS, remove deletes it, list merges correctly with existing remote symbols, rollback on failed write, and graceful no-op when the preferences blob is `null`. - Messenger test confirms the two new AUS actions are delegated. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Integrate AUS Perps Watchlist ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: [TAT-2663](https://consensyssoftware.atlassian.net/browse/TAT-2663) ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/f89061e1-4ce5-41d4-8f77-c9ba5ee4e722 ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches cross-device user preference persistence via Authenticated User Storage; risk is mitigated by controller-owned rollback, skipped writes when prefs are uninitialized, and new integration coverage. > > **Overview** > Wires Perps watchlist persistence to **Authenticated User Storage (AUS)** by adopting **`@metamask/perps-controller` ^9.0.0** (via `package.json` resolution). Watchlist add/remove now runs optimistic local updates and syncs `perps.watchlistMarkets` through `NotificationPreferences`, with local-only behavior when prefs are missing or the user is unauthenticated. > > **`usePerpsWatchlistActions`** drops the old optimistic/AUS TODO scaffolding and documents that **`PerpsController.toggleWatchlistMarket`** owns that behavior. Analytics for favorite/unfavorite only fire after confirming the symbol is actually on or off the list post-toggle (so failed adds or rollbacks do not emit misleading events). > > Adds **integration tests** on the real controller: AUS read/write on toggle, merge with remote symbols, rollback when `putNotificationPreferences` fails, and skipping remote writes when `getNotificationPreferences` is `null`. Messenger test copy is updated to describe AUS notification-preference delegation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0bcf1df. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> [TAT-2663]: https://consensyssoftware.atlassian.net/browse/TAT-2663?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent d9e9e17 commit 5216c01

4 files changed

Lines changed: 288 additions & 46 deletions

File tree

app/components/UI/Perps/hooks/usePerpsWatchlistActions.ts

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,22 @@ interface UsePerpsWatchlistActionsResult {
1616
/**
1717
* Adds a market to the watchlist.
1818
*
19-
* Currently wraps the asynchronous PerpsController.toggleWatchlistMarket call.
20-
*
21-
* TODO(TAT-2663 — optimistic UI): Before the await, apply an optimistic local
22-
* state update (e.g. pass an onOptimisticUpdate callback from the caller).
23-
* On failure, roll back that state and show the addError toast below.
24-
*
25-
* TODO(TAT-2663 — User Storage API): Replace / augment the controller call
26-
* with a POST to the User Storage API once it is available.
19+
* Delegates to PerpsController.toggleWatchlistMarket, which performs an
20+
* optimistic local update and syncs to AuthenticatedUserStorageService.
21+
* Falls back to local-only when unauthenticated.
2722
*/
2823
addToWatchlist: (symbol: string) => Promise<void>;
2924
/**
3025
* Removes a market from the watchlist.
3126
*
32-
* Same optimistic UI and User Storage seams as addToWatchlist above.
27+
* Same optimistic-update and AUS sync behaviour as addToWatchlist.
3328
*/
3429
removeFromWatchlist: (symbol: string) => Promise<void>;
3530
}
3631

3732
/**
38-
* Provides add/remove watchlist actions with analytics, error reporting,
39-
* and structural seams for optimistic UI and the future User Storage API.
33+
* Provides add/remove watchlist actions with analytics and error reporting.
34+
* The controller handles optimistic UI, AUS sync, and graceful degradation.
4035
*
4136
* @param source - Analytics source identifying where the action was triggered
4237
* (e.g. PERPS_EVENT_VALUE.SOURCE.PERPS_HOME_WATCHLIST).
@@ -57,27 +52,21 @@ export const usePerpsWatchlistActions = (
5752
return;
5853
}
5954

60-
// TODO(TAT-2663 — optimistic UI): Call onOptimisticUpdate?.() here
61-
// to instantly reflect the add in the UI before the async call resolves.
62-
6355
await controller.toggleWatchlistMarket(symbol);
6456

65-
// TODO(TAT-2663 — User Storage API): await userStorageApi.addWatchlistMarket(symbol)
66-
67-
const watchlistCount = controller.getWatchlistMarkets().length;
68-
track(MetaMetricsEvents.PERPS_UI_INTERACTION, {
69-
[PERPS_EVENT_PROPERTY.INTERACTION_TYPE]:
70-
PERPS_EVENT_VALUE.INTERACTION_TYPE.FAVORITE_TOGGLED,
71-
[PERPS_EVENT_PROPERTY.ACTION_TYPE]:
72-
PERPS_EVENT_VALUE.ACTION_TYPE.FAVORITE_MARKET,
73-
[PERPS_EVENT_PROPERTY.ASSET]: symbol,
74-
[PERPS_EVENT_PROPERTY.SOURCE]: source,
75-
[PERPS_EVENT_PROPERTY.FAVORITES_COUNT]: watchlistCount,
76-
});
57+
const watchlistAfter = controller.getWatchlistMarkets();
58+
if (watchlistAfter.includes(symbol)) {
59+
track(MetaMetricsEvents.PERPS_UI_INTERACTION, {
60+
[PERPS_EVENT_PROPERTY.INTERACTION_TYPE]:
61+
PERPS_EVENT_VALUE.INTERACTION_TYPE.FAVORITE_TOGGLED,
62+
[PERPS_EVENT_PROPERTY.ACTION_TYPE]:
63+
PERPS_EVENT_VALUE.ACTION_TYPE.FAVORITE_MARKET,
64+
[PERPS_EVENT_PROPERTY.ASSET]: symbol,
65+
[PERPS_EVENT_PROPERTY.SOURCE]: source,
66+
[PERPS_EVENT_PROPERTY.FAVORITES_COUNT]: watchlistAfter.length,
67+
});
68+
}
7769
} catch (error) {
78-
// TODO(TAT-2663 — optimistic UI): Roll back the optimistic state
79-
// update here (e.g. call onRollback?.()) before showing the error toast.
80-
8170
Logger.error(ensureError(error, 'usePerpsWatchlistActions.add'), {
8271
tags: {
8372
feature: PERPS_CONSTANTS.FeatureName,
@@ -99,26 +88,22 @@ export const usePerpsWatchlistActions = (
9988
const removeFromWatchlist = useCallback(
10089
async (symbol: string): Promise<void> => {
10190
try {
102-
// TODO(TAT-2663 — optimistic UI): Apply optimistic remove here.
103-
10491
const controller = Engine.context.PerpsController;
10592
await controller.toggleWatchlistMarket(symbol);
10693

107-
// TODO(TAT-2663 — User Storage API): await userStorageApi.removeWatchlistMarket(symbol)
108-
109-
const watchlistCount = controller.getWatchlistMarkets().length;
110-
track(MetaMetricsEvents.PERPS_UI_INTERACTION, {
111-
[PERPS_EVENT_PROPERTY.INTERACTION_TYPE]:
112-
PERPS_EVENT_VALUE.INTERACTION_TYPE.FAVORITE_TOGGLED,
113-
[PERPS_EVENT_PROPERTY.ACTION_TYPE]:
114-
PERPS_EVENT_VALUE.ACTION_TYPE.UNFAVORITE_MARKET,
115-
[PERPS_EVENT_PROPERTY.ASSET]: symbol,
116-
[PERPS_EVENT_PROPERTY.SOURCE]: source,
117-
[PERPS_EVENT_PROPERTY.FAVORITES_COUNT]: watchlistCount,
118-
});
94+
const watchlistAfter = controller.getWatchlistMarkets();
95+
if (!watchlistAfter.includes(symbol)) {
96+
track(MetaMetricsEvents.PERPS_UI_INTERACTION, {
97+
[PERPS_EVENT_PROPERTY.INTERACTION_TYPE]:
98+
PERPS_EVENT_VALUE.INTERACTION_TYPE.FAVORITE_TOGGLED,
99+
[PERPS_EVENT_PROPERTY.ACTION_TYPE]:
100+
PERPS_EVENT_VALUE.ACTION_TYPE.UNFAVORITE_MARKET,
101+
[PERPS_EVENT_PROPERTY.ASSET]: symbol,
102+
[PERPS_EVENT_PROPERTY.SOURCE]: source,
103+
[PERPS_EVENT_PROPERTY.FAVORITES_COUNT]: watchlistAfter.length,
104+
});
105+
}
119106
} catch (error) {
120-
// TODO(TAT-2663 — optimistic UI): Roll back the optimistic remove here.
121-
122107
Logger.error(ensureError(error, 'usePerpsWatchlistActions.remove'), {
123108
tags: {
124109
feature: PERPS_CONSTANTS.FeatureName,

app/core/Engine/controllers/perps-controller/index.test.ts

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import {
77
PerpsControllerState,
88
InitializationState,
99
MARKET_SORTING_CONFIG,
10+
PerpsPlatformDependencies,
1011
} from '@metamask/perps-controller';
1112
import { perpsControllerInit } from '.';
1213
import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger';
14+
import { getPerpsControllerMessenger } from '../../messengers/perps-controller-messenger';
15+
import type { NotificationPreferences } from '@metamask/authenticated-user-storage';
1316

1417
// Mock mobile-specific modules that ./index.ts imports to avoid pulling in
1518
// Engine and React Native dependencies in the test environment
@@ -162,3 +165,256 @@ describe('perps controller init', () => {
162165
expect(perpsControllerState).toStrictEqual(initialPerpsControllerState);
163166
});
164167
});
168+
169+
// ---------------------------------------------------------------------------
170+
// Integration: watchlist ↔ AuthenticatedUserStorageService
171+
//
172+
// The describe block above mocks PerpsController as jest.fn() (needed to test
173+
// the init wiring). These tests need the real controller, so they use
174+
// jest.requireActual to reach the preview build directly.
175+
// ---------------------------------------------------------------------------
176+
177+
function buildSeedPrefs(
178+
overrides: Partial<NotificationPreferences> = {},
179+
): NotificationPreferences {
180+
return {
181+
walletActivity: {
182+
inAppNotificationsEnabled: false,
183+
pushNotificationsEnabled: false,
184+
accounts: [],
185+
},
186+
marketing: {
187+
inAppNotificationsEnabled: false,
188+
pushNotificationsEnabled: false,
189+
},
190+
perps: {
191+
inAppNotificationsEnabled: false,
192+
pushNotificationsEnabled: false,
193+
watchlistMarkets: {
194+
hyperliquid: { testnet: [], mainnet: [] },
195+
myx: { testnet: [], mainnet: [] },
196+
},
197+
},
198+
socialAI: {
199+
inAppNotificationsEnabled: false,
200+
pushNotificationsEnabled: false,
201+
mutedTraderProfileIds: [],
202+
},
203+
...overrides,
204+
};
205+
}
206+
207+
function buildTestInfrastructure(): PerpsPlatformDependencies {
208+
return {
209+
logger: { error: jest.fn() },
210+
debugLogger: { log: jest.fn() },
211+
metrics: { isEnabled: jest.fn(() => false), trackPerpsEvent: jest.fn() },
212+
performance: { now: jest.fn(() => 0) },
213+
streamManager: {
214+
pauseChannel: jest.fn(),
215+
resumeChannel: jest.fn(),
216+
clearAllChannels: jest.fn(),
217+
},
218+
featureFlags: { validateVersionGated: jest.fn(() => false) },
219+
marketDataFormatters: {
220+
formatVolume: jest.fn(() => ''),
221+
formatPerpsFiat: jest.fn(() => ''),
222+
formatPercentage: jest.fn(() => ''),
223+
priceRangesUniversal: [],
224+
},
225+
cacheInvalidator: { invalidate: jest.fn(), invalidateAll: jest.fn() },
226+
diskCache: {
227+
// Called synchronously in the constructor (#hydrateCacheFromDiskSync).
228+
getItemSync: jest.fn(() => null),
229+
getItem: jest.fn(() => Promise.resolve(null)),
230+
setItem: jest.fn(() => Promise.resolve()),
231+
removeItem: jest.fn(() => Promise.resolve()),
232+
},
233+
tracer: {
234+
trace: jest.fn(),
235+
endTrace: jest.fn(),
236+
setMeasurement: jest.fn(),
237+
addBreadcrumb: jest.fn(),
238+
},
239+
rewards: { getPerpsDiscountForAccount: jest.fn(() => Promise.resolve(0)) },
240+
};
241+
}
242+
243+
interface GetNotificationPreferencesAction {
244+
type: 'AuthenticatedUserStorageService:getNotificationPreferences';
245+
handler: () => Promise<NotificationPreferences | null>;
246+
}
247+
248+
interface PutNotificationPreferencesAction {
249+
type: 'AuthenticatedUserStorageService:putNotificationPreferences';
250+
handler: (prefs: NotificationPreferences) => Promise<void>;
251+
}
252+
253+
function buildRealController(
254+
getNotificationPreferencesImpl: () => Promise<NotificationPreferences | null>,
255+
) {
256+
// Use the real PerpsController from the preview build — the top-level
257+
// jest.mock replaces it with jest.fn() for the init tests above, so we
258+
// reach through with requireActual here.
259+
const {
260+
PerpsController: RealPerpsController,
261+
getDefaultPerpsControllerState,
262+
} = jest.requireActual('@metamask/perps-controller/PerpsController') as {
263+
PerpsController: typeof PerpsController;
264+
getDefaultPerpsControllerState: () => PerpsControllerState;
265+
};
266+
267+
const baseMessenger = new ExtendedMessenger<
268+
MockAnyNamespace,
269+
GetNotificationPreferencesAction | PutNotificationPreferencesAction
270+
>({
271+
namespace: MOCK_ANY_NAMESPACE,
272+
});
273+
274+
const getSpy = jest.fn().mockImplementation(getNotificationPreferencesImpl);
275+
const putSpy = jest.fn().mockResolvedValue(undefined);
276+
277+
baseMessenger.registerActionHandler(
278+
'AuthenticatedUserStorageService:getNotificationPreferences',
279+
getSpy,
280+
);
281+
baseMessenger.registerActionHandler(
282+
'AuthenticatedUserStorageService:putNotificationPreferences',
283+
putSpy,
284+
);
285+
286+
const controllerMessenger = getPerpsControllerMessenger(baseMessenger);
287+
288+
const controller = new RealPerpsController({
289+
messenger: controllerMessenger,
290+
state: getDefaultPerpsControllerState(),
291+
infrastructure: buildTestInfrastructure(),
292+
clientConfig: {},
293+
});
294+
295+
return { controller, getSpy, putSpy };
296+
}
297+
298+
describe('PerpsController watchlist ↔ AuthenticatedUserStorageService', () => {
299+
beforeEach(() => {
300+
jest.clearAllMocks();
301+
});
302+
303+
describe('toggleWatchlistMarket — add', () => {
304+
it('calls putNotificationPreferences with the new symbol in hyperliquid.mainnet', async () => {
305+
const { controller, putSpy } = buildRealController(() =>
306+
Promise.resolve(buildSeedPrefs()),
307+
);
308+
309+
await controller.toggleWatchlistMarket('BTC');
310+
311+
expect(putSpy).toHaveBeenCalledTimes(1);
312+
expect(putSpy).toHaveBeenCalledWith(
313+
expect.objectContaining({
314+
perps: expect.objectContaining({
315+
watchlistMarkets: expect.objectContaining({
316+
hyperliquid: expect.objectContaining({
317+
mainnet: expect.arrayContaining(['BTC']),
318+
}),
319+
}),
320+
}),
321+
}),
322+
);
323+
});
324+
325+
it('keeps the local watchlist state after a successful AUS write', async () => {
326+
const { controller } = buildRealController(() =>
327+
Promise.resolve(buildSeedPrefs()),
328+
);
329+
330+
await controller.toggleWatchlistMarket('ETH');
331+
332+
expect(controller.getWatchlistMarkets()).toContain('ETH');
333+
});
334+
335+
it('merges with existing remote symbols — does not overwrite the whole list', async () => {
336+
const seedWithSol = buildSeedPrefs({
337+
perps: {
338+
inAppNotificationsEnabled: false,
339+
pushNotificationsEnabled: false,
340+
watchlistMarkets: {
341+
hyperliquid: { testnet: [], mainnet: ['SOL'] },
342+
myx: { testnet: [], mainnet: [] },
343+
},
344+
},
345+
});
346+
347+
const { controller, putSpy } = buildRealController(() =>
348+
Promise.resolve(seedWithSol),
349+
);
350+
// Seed local state with SOL first
351+
await controller.toggleWatchlistMarket('SOL');
352+
putSpy.mockClear();
353+
354+
await controller.toggleWatchlistMarket('BTC');
355+
356+
expect(putSpy).toHaveBeenCalledWith(
357+
expect.objectContaining({
358+
perps: expect.objectContaining({
359+
watchlistMarkets: expect.objectContaining({
360+
hyperliquid: expect.objectContaining({
361+
mainnet: expect.arrayContaining(['SOL', 'BTC']),
362+
}),
363+
}),
364+
}),
365+
}),
366+
);
367+
});
368+
});
369+
370+
describe('toggleWatchlistMarket — remove', () => {
371+
it('calls putNotificationPreferences without the removed symbol', async () => {
372+
const { controller, putSpy } = buildRealController(() =>
373+
Promise.resolve(buildSeedPrefs()),
374+
);
375+
await controller.toggleWatchlistMarket('BTC'); // add
376+
putSpy.mockClear();
377+
378+
await controller.toggleWatchlistMarket('BTC'); // remove
379+
380+
expect(putSpy).toHaveBeenCalledTimes(1);
381+
const [calledPrefs] = putSpy.mock.calls[0] as [NotificationPreferences];
382+
expect(
383+
calledPrefs.perps.watchlistMarkets?.hyperliquid.mainnet,
384+
).not.toContain('BTC');
385+
});
386+
387+
it('reverts local state when the AUS write fails', async () => {
388+
const { controller, putSpy } = buildRealController(() =>
389+
Promise.resolve(buildSeedPrefs()),
390+
);
391+
await controller.toggleWatchlistMarket('BTC');
392+
expect(controller.getWatchlistMarkets()).toContain('BTC');
393+
394+
putSpy.mockRejectedValueOnce(new Error('network error'));
395+
await controller.toggleWatchlistMarket('BTC'); // remove attempt fails
396+
397+
expect(controller.getWatchlistMarkets()).toContain('BTC');
398+
});
399+
});
400+
401+
describe('AUS write is skipped when preferences blob is not yet initialised', () => {
402+
it('does not call putNotificationPreferences when getNotificationPreferences returns null', async () => {
403+
const { controller, putSpy } = buildRealController(() =>
404+
Promise.resolve(null),
405+
);
406+
407+
await controller.toggleWatchlistMarket('BTC');
408+
409+
expect(putSpy).not.toHaveBeenCalled();
410+
});
411+
412+
it('keeps the optimistic local update even when the remote write is skipped', async () => {
413+
const { controller } = buildRealController(() => Promise.resolve(null));
414+
415+
await controller.toggleWatchlistMarket('BTC');
416+
417+
expect(controller.getWatchlistMarkets()).toContain('BTC');
418+
});
419+
});
420+
});

app/core/Engine/messengers/perps-controller-messenger/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('PerpsController Messenger', () => {
8686
);
8787
});
8888

89-
it('delegates the AuthenticatedUserStorageService watchlist actions', () => {
89+
it('delegates AUS notification-preference actions to the perps controller messenger', () => {
9090
const baseControllerMessenger = new ExtendedMessenger<MockAnyNamespace>({
9191
namespace: MOCK_ANY_NAMESPACE,
9292
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
]
169169
},
170170
"resolutions": {
171+
"@metamask/perps-controller": "^9.0.0",
171172
"@metamask/network-controller": "33.0.0",
172173
"@react-native-community/viewpager": "patch:@react-native-community/viewpager@npm%3A3.3.0#~/.yarn/patches/@react-native-community-viewpager-npm-3.3.0.patch",
173174
"@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.5",

0 commit comments

Comments
 (0)