Skip to content

Commit 2bc47f8

Browse files
fix: NotificationServicesController - check keyring state when using stateChange event (#5731)
## Explanation Uncovered on extension. I haven't pieced all the moving parts together but notifications essentially perform: - Listens to `KeyringController:stateChange` events - When receiving an event we perform a `KeyringController:withKeyring` action. Somewhere above (or potentially another controller?) will then fire another `KeyringController:stateChange`, and we have a infinite loop preventing other redux or state updates. Now we will check the size of the keyring to better assume if accounts have been added or removed. ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Changelog <!-- THIS SECTION IS NO LONGER NEEDED. The process for updating changelogs has changed. Please consult the "Updating changelogs" section of the Contributing doc for more. --> ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 8edcd7d commit 2bc47f8

File tree

3 files changed

+68
-34
lines changed

3 files changed

+68
-34
lines changed

packages/notification-services-controller/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Bump `@metamask/base-controller` from ^8.0.0 to ^8.0.1 ([#5722](https://github.com/MetaMask/core/pull/5722))
1313

14+
### Fixed
15+
16+
- add a check inside the `KeyringController:stateChange` subscription inside `NotificationServicesController` to prevent infinite updates ([#5731](https://github.com/MetaMask/core/pull/5731))
17+
- As we invoke a `KeyringController:withKeyring` inside the `KeyringController:stateChange` event subscription,
18+
we are causing many infinite updates which block other controllers from performing state updates.
19+
- We now check the size of keyrings from the `KeyringController:stateChange` to better assume when keyrings have been added
20+
1421
## [6.0.0]
1522

1623
### Changed

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts

+48-32
Original file line numberDiff line numberDiff line change
@@ -103,50 +103,30 @@ describe('metamask-notifications - init()', () => {
103103
const actPublishKeyringStateChange = async (
104104
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105105
messenger: any,
106+
accounts: string[] = ['0x111', '0x222'],
106107
) => {
107108
messenger.publish(
108109
'KeyringController:stateChange',
109-
{} as KeyringControllerState,
110+
{
111+
keyrings: [{ accounts }],
112+
} as KeyringControllerState,
110113
[],
111114
);
112115
};
113116

114-
it('keyring Change Event but feature not enabled will not add or remove triggers', async () => {
115-
const { messenger, globalMessenger, mockWithKeyring } = arrangeMocks();
116-
117-
// initialize controller with 1 address
118-
mockWithKeyring.mockResolvedValueOnce([ADDRESS_1]);
119-
const controller = new NotificationServicesController({
120-
messenger,
121-
env: { featureAnnouncements: featureAnnouncementsEnv },
122-
});
123-
controller.init();
124-
125-
const mockUpdate = jest
126-
.spyOn(controller, 'updateOnChainTriggersByAccount')
127-
.mockResolvedValue({} as UserStorage);
128-
const mockDelete = jest
129-
.spyOn(controller, 'deleteOnChainTriggersByAccount')
130-
.mockResolvedValue({} as UserStorage);
131-
132-
// listAccounts has a new address
133-
mockWithKeyring.mockResolvedValueOnce([ADDRESS_1, ADDRESS_2]);
134-
await actPublishKeyringStateChange(globalMessenger);
135-
136-
expect(mockUpdate).not.toHaveBeenCalled();
137-
expect(mockDelete).not.toHaveBeenCalled();
138-
});
139-
140-
it('keyring Change Event with new triggers will update triggers correctly', async () => {
141-
const { messenger, globalMessenger, mockWithKeyring } = arrangeMocks();
142-
117+
const arrangeActAssertKeyringTest = async (
118+
controllerState?: Partial<NotificationServicesControllerState>,
119+
) => {
120+
const mocks = arrangeMocks();
121+
const { messenger, globalMessenger, mockWithKeyring } = mocks;
143122
// initialize controller with 1 address
144123
const controller = new NotificationServicesController({
145124
messenger,
146125
env: { featureAnnouncements: featureAnnouncementsEnv },
147126
state: {
148127
isNotificationServicesEnabled: true,
149-
subscriptionAccountsSeen: [ADDRESS_1],
128+
subscriptionAccountsSeen: [],
129+
...controllerState,
150130
},
151131
});
152132
controller.init();
@@ -160,7 +140,7 @@ describe('metamask-notifications - init()', () => {
160140

161141
const act = async (addresses: string[], assertion: () => void) => {
162142
mockWithKeyring.mockResolvedValueOnce(addresses);
163-
await actPublishKeyringStateChange(globalMessenger);
143+
await actPublishKeyringStateChange(globalMessenger, addresses);
164144
await waitFor(() => {
165145
assertion();
166146
});
@@ -170,6 +150,26 @@ describe('metamask-notifications - init()', () => {
170150
mockDelete.mockClear();
171151
};
172152

153+
return { act, mockUpdate, mockDelete };
154+
};
155+
156+
it('event KeyringController:stateChange will not add or remove triggers when feature is disabled', async () => {
157+
const { act, mockUpdate, mockDelete } = await arrangeActAssertKeyringTest({
158+
isNotificationServicesEnabled: false,
159+
});
160+
161+
// listAccounts has a new address
162+
await act([ADDRESS_1, ADDRESS_2], () => {
163+
expect(mockUpdate).not.toHaveBeenCalled();
164+
expect(mockDelete).not.toHaveBeenCalled();
165+
});
166+
});
167+
168+
it('event KeyringController:stateChange will update notification triggers when keyring accounts change', async () => {
169+
const { act, mockUpdate, mockDelete } = await arrangeActAssertKeyringTest({
170+
subscriptionAccountsSeen: [ADDRESS_1],
171+
});
172+
173173
// Act - if list accounts has been seen, then will not update
174174
await act([ADDRESS_1], () => {
175175
expect(mockUpdate).not.toHaveBeenCalled();
@@ -195,6 +195,22 @@ describe('metamask-notifications - init()', () => {
195195
});
196196
});
197197

198+
it('event KeyringController:stateChange will update only once when if the number of keyring accounts do not change', async () => {
199+
const { act, mockUpdate, mockDelete } = await arrangeActAssertKeyringTest();
200+
201+
// Act - First list of items, so will update
202+
await act([ADDRESS_1, ADDRESS_2], () => {
203+
expect(mockUpdate).toHaveBeenCalled();
204+
expect(mockDelete).not.toHaveBeenCalled();
205+
});
206+
207+
// Act - Since number of addresses in keyring has not changed, will not update
208+
await act([ADDRESS_1, ADDRESS_2], () => {
209+
expect(mockUpdate).not.toHaveBeenCalled();
210+
expect(mockDelete).not.toHaveBeenCalled();
211+
});
212+
});
213+
198214
const arrangeActInitialisePushNotifications = (
199215
modifications?: (mocks: ReturnType<typeof arrangeMocks>) => void,
200216
) => {

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
type KeyringControllerUnlockEvent,
1717
type KeyringControllerWithKeyringAction,
1818
KeyringTypes,
19+
type KeyringControllerState,
1920
} from '@metamask/keyring-controller';
2021
import type {
2122
AuthenticationController,
@@ -501,8 +502,12 @@ export default class NotificationServicesController extends BaseController<
501502
subscribe: () => {
502503
this.messagingSystem.subscribe(
503504
'KeyringController:stateChange',
504-
async () => {
505-
if (!this.state.isNotificationServicesEnabled) {
505+
async (totalAccounts, prevTotalAccounts) => {
506+
const hasTotalAccountsChanged = totalAccounts !== prevTotalAccounts;
507+
if (
508+
!this.state.isNotificationServicesEnabled ||
509+
!hasTotalAccountsChanged
510+
) {
506511
return;
507512
}
508513

@@ -518,6 +523,12 @@ export default class NotificationServicesController extends BaseController<
518523
}
519524
await Promise.all(promises);
520525
},
526+
(state: KeyringControllerState) => {
527+
return (
528+
state?.keyrings?.flatMap?.((keyring) => keyring.accounts)?.length ??
529+
0
530+
);
531+
},
521532
);
522533
},
523534
};

0 commit comments

Comments
 (0)