Skip to content

Commit a6572a0

Browse files
authored
fix(accounts): dynamically enable Money account keyrings and service (#29502)
## **Description** Keep the `MoneyKeyring` builder registered in the `KeyringController` so that if the feature flag gets enabled dynamically, the controller and keyring will get created dynamically too! - The money keyring state is never removed/cleared - When the flag goes from ON -> OFF and there was a Money account, it gets cleared - When the flag goes from OFF -> ON and there was no Money account, it gets created automatically ## **Changelog** CHANGELOG entry: N/A ## **Related issues** Fixes: TODO ## **Manual testing steps** Make sure to enable this in your `.js.env`: ```env DEBUG=metamask:money-account-controller ``` Here's a patch to get some logs: ```diff diff --git a/app/core/Engine/controllers/money-account-controller-init.ts b/app/core/Engine/controllers/money-account-controller-init.ts index c493253..b5d5f4b761 100644 --- a/app/core/Engine/controllers/money-account-controller-init.ts +++ b/app/core/Engine/controllers/money-account-controller-init.ts @@ -39,12 +39,14 @@ export const moneyAccountControllerInit: MessengerClientInitFunction< const { isUnlocked } = initMessenger.call( 'KeyringController:getState', ); + console.log('testing: Initializing money account due to FF on'); // Check for the `KeyringController` to be unlocked, otherwise we won't be able // to create the Money keyring if it doesn't exist yet! if (isUnlocked) { // This call is idempotent, so it is safe to call even if the // controller is already initialized. await controller.init(); + console.log('testing: Clearing money account state due to FF off'); } } else if (!isEnabled && hasMoneyAccount) { // Clear state if we had a previous Money account and FF is off. diff --git a/app/core/Engine/controllers/remote-feature-flag-controller-init.ts b/app/core/Engine/controllers/remote-feature-flag-controller-init.ts index 2ab54cc..2ac6fb26d9 100644 --- a/app/core/Engine/controllers/remote-feature-flag-controller-init.ts +++ b/app/core/Engine/controllers/remote-feature-flag-controller-init.ts @@ -45,9 +45,7 @@ export const remoteFeatureFlagControllerInit: MessengerClientInitFunction< distribution: getFeatureFlagAppDistribution(), }, }), - fetchInterval: __DEV__ - ? 1000 - : AppConstants.FEATURE_FLAGS_API.DEFAULT_FETCH_INTERVAL, + fetchInterval: 1000, }); if (disabled) { @@ -61,6 +59,15 @@ export const remoteFeatureFlagControllerInit: MessengerClientInitFunction< Logger.log('Feature flags updated'); }) .catch((error) => Logger.log('Feature flags update failed: ', error)); + setInterval(() => + controller + .updateRemoteFeatureFlags() + .then(() => { + Logger.log('Feature flags updated (interval)'); + }) + .catch((error) => Logger.log('Feature flags update failed: ', error)) + , 10 * 1000 + ); } return { diff --git a/app/lib/Money/feature-flags.ts b/app/lib/Money/feature-flags.ts index 736ed10..464497e8dc 100644 --- a/app/lib/Money/feature-flags.ts +++ b/app/lib/Money/feature-flags.ts @@ -16,6 +16,7 @@ export function isMoneyAccountEnabled( const localFlag = process.env.MM_MONEY_ENABLE_MONEY_ACCOUNT === 'true'; const remoteFlag = remoteFeatureFlags?.moneyEnableMoneyAccount as VersionGatedFeatureFlag; + console.log('testing: Remote Flag is:', remoteFlag, 'Local Flag is:', localFlag); return validatedVersionGatedFeatureFlag(remoteFlag) ?? localFlag; } ``` ```gherkin Feature: Money account feature flag handling (ON) Scenario: flag is ON Given the flag was OFF When the flag gets updated Then the Money account gets created automatically Feature: Money account feature flag handling (OFF) Scenario: flag is OFF Given the flag was ON When the flag gets updated Then the Money account gets cleared automatically ``` If you enabled those extra logs (with the patch above), toggling ON/OFF should give you something like this: ```log $ yarn watch |& grep -E "metamask:money-account-controller|testing:" (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) DEBUG metamask:money-account-controller Money keyring (entropy:01KQFS1XQNH0399NJM7EZEFWP7 - primary) account is: 0xad9d9f06da37139dd54fd48fda02cae244a590cb (b3961609-ef94-42ea-90fc-93149e185d56) +0ms (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": false, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Clearing money account state due to FF off (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Initializing money account due to FF on (NOBRIDGE) DEBUG metamask:money-account-controller Money keyring (entropy:01KQFS1XQNH0399NJM7EZEFWP7) account created: 0xad9d9f06da37139dd54fd48fda02cae244a590cb (b3961609-ef94-42ea-90fc-93149e185d56) +26s (NOBRIDGE) DEBUG metamask:money-account-controller Money keyring (entropy:01KQFS1XQNH0399NJM7EZEFWP7 - primary) account is: 0xad9d9f06da37139dd54fd48fda02cae244a590cb (b3961609-ef94-42ea-90fc-93149e185d56) +0ms (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": false, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Clearing money account state due to FF off (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": false, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": false, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Initializing money account due to FF on (NOBRIDGE) DEBUG metamask:money-account-controller Money keyring (entropy:01KQFS1XQNH0399NJM7EZEFWP7) account created: 0xad9d9f06da37139dd54fd48fda02cae244a590cb (b3961609-ef94-42ea-90fc-93149e185d56) +1m (NOBRIDGE) DEBUG metamask:money-account-controller Money keyring (entropy:01KQFS1XQNH0399NJM7EZEFWP7 - primary) account is: 0xad9d9f06da37139dd54fd48fda02cae244a590cb (b3961609-ef94-42ea-90fc-93149e185d56) +0ms (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false (NOBRIDGE) LOG testing: Remote Flag is: {"enabled": true, "minimumVersion": "0.0.0"} Local Flag is: false ``` - We only re-init if there was no Money account AND the flag is ON - We only clear if there was a Money account AND the flag is OFF - We only do those steps once (not clearing twice, not calling `init` twice) ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] 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 - [ ] 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** - [ ] 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 account/keyring initialization and wallet reset flows, so mistakes could create or wipe Money account state unexpectedly when feature flags change. Changes are scoped and covered by targeted unit tests, but still impact core account plumbing. > > **Overview** > **Money accounts are now managed dynamically based on remote feature-flag updates.** `MoneyAccountController` subscribes to `RemoteFeatureFlagController:stateChange` during init and will `init()` when the flag turns on (only if the keyring is unlocked and no money accounts exist), or `clearState()` when the flag turns off (only if money accounts exist), with error logging on failures. > > **Keyring handling is made resilient to flag timing.** The `MoneyKeyring` builder is now always registered in `keyringControllerInit` so vault deserialization can recognize the type even if the Money feature flag is disabled at that moment. > > **State clearing responsibilities are centralized.** `Engine.resetState` now clears `MoneyAccountController` state, while `AccountTreeInitService.clearState` no longer clears money accounts. Tests were updated/added to assert these behaviors and the new init messenger wiring (`getMoneyAccountControllerInitMessenger`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9d427ea. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent eedc67b commit a6572a0

10 files changed

Lines changed: 323 additions & 87 deletions

File tree

app/core/Engine/Engine.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,4 +1368,17 @@ describe('Engine', () => {
13681368
expect(sortedControllersInState).toEqual(sortedExpectedControllers);
13691369
});
13701370
});
1371+
1372+
describe('resetState', () => {
1373+
it('calls MoneyAccountController.clearState', async () => {
1374+
const engine = Engine.init(TEST_ANALYTICS_ID, backgroundState);
1375+
const clearStateSpy = jest
1376+
.spyOn(engine.context.MoneyAccountController, 'clearState')
1377+
.mockImplementation(() => undefined);
1378+
1379+
await engine.resetState();
1380+
1381+
expect(clearStateSpy).toHaveBeenCalled();
1382+
});
1383+
});
13711384
});

app/core/Engine/Engine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,7 @@ export class Engine {
11591159
SnapController,
11601160
///: END:ONLY_INCLUDE_IF
11611161
LoggingController,
1162+
MoneyAccountController,
11621163
} = this.context;
11631164

11641165
// Remove all permissions.
@@ -1189,6 +1190,9 @@ export class Engine {
11891190
}));
11901191

11911192
LoggingController.clear();
1193+
1194+
// Accounts:
1195+
MoneyAccountController.clearState();
11921196
};
11931197

11941198
removeAllListeners() {

app/core/Engine/controllers/keyring-controller/keyring-controller-init.test.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
11
import { buildMessengerClientInitRequestMock } from '../../utils/test-utils';
2-
3-
jest.mock('../../../../lib/Money/feature-flags', () => ({
4-
isMoneyAccountEnabled: jest.fn(),
5-
}));
6-
7-
const mockIsMoneyAccountEnabled = jest.requireMock(
8-
'../../../../lib/Money/feature-flags',
9-
).isMoneyAccountEnabled as jest.Mock;
102
import { ExtendedMessenger } from '../../../ExtendedMessenger';
113
import { getKeyringControllerMessenger } from '../../messengers/keyring-controller-messenger';
124
import { MessengerClientInitRequest } from '../../types';
@@ -72,7 +64,6 @@ function getInitRequestMock(): jest.Mocked<
7264
describe('keyringControllerInit', () => {
7365
beforeEach(() => {
7466
jest.clearAllMocks();
75-
mockIsMoneyAccountEnabled.mockReturnValue(true);
7667
});
7768

7869
it('initializes the controller', () => {
@@ -108,26 +99,12 @@ describe('keyringControllerInit', () => {
10899
return builder;
109100
}
110101

111-
it('includes a MoneyKeyring builder when the flag is enabled', () => {
112-
mockIsMoneyAccountEnabled.mockReturnValue(true);
113-
102+
it('always includes a MoneyKeyring builder', () => {
114103
const builder = getMoneyKeyringBuilder();
115104

116105
expect(builder).toBeDefined();
117106
});
118107

119-
it('does not include a MoneyKeyring builder when the flag is disabled', () => {
120-
mockIsMoneyAccountEnabled.mockReturnValue(false);
121-
122-
keyringControllerInit(getInitRequestMock());
123-
124-
const { keyringBuilders } = jest.mocked(KeyringController).mock
125-
.calls[0][0] as { keyringBuilders: KeyringBuilder[] };
126-
127-
const builder = keyringBuilders.find((b) => b.type === MoneyKeyring.type);
128-
expect(builder).toBeUndefined();
129-
});
130-
131108
it('creates a MoneyKeyring instance when invoked', () => {
132109
const builder = getMoneyKeyringBuilder();
133110

app/core/Engine/controllers/keyring-controller/keyring-controller-init.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { MessengerClientInitFunction } from '../../types';
2-
import { isMoneyAccountEnabled } from '../../../../lib/Money/feature-flags';
32
import { CryptographicFunctions } from '@metamask/key-tree';
43
import { encodeMnemonic } from '@metamask/keyring-sdk';
54
import {
@@ -44,10 +43,6 @@ export const keyringControllerInit: MessengerClientInitFunction<
4443
qrKeyringScanner,
4544
getMessengerClient,
4645
}) => {
47-
const { remoteFeatureFlags } = getMessengerClient(
48-
'RemoteFeatureFlagController',
49-
).state;
50-
5146
// Required by the HD keyring and money keyring to use native crypto functions.
5247
const cryptographicFunctions: CryptographicFunctions = {
5348
pbkdf2Sha512: pbkdf2,
@@ -81,35 +76,34 @@ export const keyringControllerInit: MessengerClientInitFunction<
8176
hdKeyringBuilder.type = HdKeyring.type;
8277
additionalKeyrings.push(hdKeyringBuilder);
8378

84-
// We only need this keyring if Money accounts are enabled.
85-
if (isMoneyAccountEnabled(remoteFeatureFlags)) {
86-
const moneyKeyringBuilder = () =>
87-
new MoneyKeyring({
88-
cryptographicFunctions,
89-
getMnemonic: async (entropySource: string) =>
90-
// This builder needs the controller itself, so we re-use `getMessengerClient` to access
91-
// the controller instance as it will be available when this method gets called.
92-
// NOTE: This is required since we cannot self-use our own actions with the init messenger.
93-
getMessengerClient('KeyringController').withKeyringUnsafe(
94-
{
95-
filter: (keyring, metadata): keyring is HdKeyring =>
96-
keyring.type === KeyringTypes.hd &&
97-
metadata.id === entropySource,
98-
},
99-
async ({ keyring }) => {
100-
if (!keyring?.mnemonic) {
101-
throw new Error(
102-
`Unable to get mnemonic to initialize MoneyKeyring`,
103-
);
104-
}
79+
// The builder is always registered so the KeyringController can recognise the
80+
// MoneyKeyring type during vault deserialization (even if the feature flag is
81+
// disabled at that time).
82+
const moneyKeyringBuilder = () =>
83+
new MoneyKeyring({
84+
cryptographicFunctions,
85+
getMnemonic: async (entropySource: string) =>
86+
// This builder needs the controller itself, so we re-use `getMessengerClient` to access
87+
// the controller instance as it will be available when this method gets called.
88+
// NOTE: This is required since we cannot self-use our own actions with the init messenger.
89+
getMessengerClient('KeyringController').withKeyringUnsafe(
90+
{
91+
filter: (keyring, metadata): keyring is HdKeyring =>
92+
keyring.type === KeyringTypes.hd && metadata.id === entropySource,
93+
},
94+
async ({ keyring }) => {
95+
if (!keyring?.mnemonic) {
96+
throw new Error(
97+
`Unable to get mnemonic to initialize MoneyKeyring`,
98+
);
99+
}
105100

106-
return encodeMnemonic(keyring.mnemonic);
107-
},
108-
),
109-
});
110-
moneyKeyringBuilder.type = MoneyKeyring.type;
111-
additionalKeyrings.push(moneyKeyringBuilder);
112-
}
101+
return encodeMnemonic(keyring.mnemonic);
102+
},
103+
),
104+
});
105+
moneyKeyringBuilder.type = MoneyKeyring.type;
106+
additionalKeyrings.push(moneyKeyringBuilder);
113107

114108
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
115109
const snapKeyringBuilder = getMessengerClient('SnapKeyringBuilder');
Lines changed: 182 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,214 @@
11
import { buildMessengerClientInitRequestMock } from '../utils/test-utils';
22
import { ExtendedMessenger } from '../../ExtendedMessenger';
3-
import { getMoneyAccountControllerMessenger } from '../messengers/money-account-controller-messenger';
3+
import {
4+
getMoneyAccountControllerInitMessenger,
5+
getMoneyAccountControllerMessenger,
6+
MoneyAccountControllerInitMessenger,
7+
} from '../messengers/money-account-controller-messenger';
48
import { MessengerClientInitRequest } from '../types';
59
import { moneyAccountControllerInit } from './money-account-controller-init';
610
import {
711
MoneyAccountController,
812
MoneyAccountControllerMessenger,
913
} from '@metamask/money-account-controller';
1014
import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger';
15+
import { RemoteFeatureFlagControllerStateChangeEvent } from '@metamask/remote-feature-flag-controller';
16+
import { isMoneyAccountEnabled } from '../../../lib/Money/feature-flags';
17+
import Logger from '../../../util/Logger';
1118

1219
jest.mock('@metamask/money-account-controller');
20+
jest.mock('../../../lib/Money/feature-flags');
21+
jest.mock('../../../util/Logger');
22+
23+
const EMPTY_MONEY_ACCOUNTS = { moneyAccounts: {} };
24+
const NON_EMPTY_MONEY_ACCOUNTS = { moneyAccounts: { 'mock-account-id': {} } };
1325

14-
function getInitRequestMock(): jest.Mocked<
15-
MessengerClientInitRequest<MoneyAccountControllerMessenger>
16-
> {
17-
const baseMessenger = new ExtendedMessenger<MockAnyNamespace, never, never>({
26+
function buildInitRequestMock<
27+
Events extends RemoteFeatureFlagControllerStateChangeEvent = never,
28+
>(
29+
baseMessenger = new ExtendedMessenger<MockAnyNamespace, never, Events>({
1830
namespace: MOCK_ANY_NAMESPACE,
19-
});
31+
}),
32+
): {
33+
requestMock: jest.Mocked<
34+
MessengerClientInitRequest<
35+
MoneyAccountControllerMessenger,
36+
MoneyAccountControllerInitMessenger
37+
>
38+
>;
39+
baseMessenger: ExtendedMessenger<MockAnyNamespace, never, Events>;
40+
} {
41+
baseMessenger.registerActionHandler(
42+
// @ts-expect-error: Action not allowed on root messenger.
43+
'RemoteFeatureFlagController:getState',
44+
jest.fn().mockReturnValue({ remoteFeatureFlags: {} }),
45+
);
2046

21-
return {
47+
baseMessenger.registerActionHandler(
48+
// @ts-expect-error: Action not allowed on root messenger.
49+
'KeyringController:getState',
50+
jest.fn().mockReturnValue({ isUnlocked: true }),
51+
);
52+
53+
const requestMock = {
2254
...buildMessengerClientInitRequestMock(baseMessenger),
2355
controllerMessenger: getMoneyAccountControllerMessenger(baseMessenger),
24-
initMessenger: undefined,
25-
};
56+
initMessenger: getMoneyAccountControllerInitMessenger(baseMessenger),
57+
} as jest.Mocked<
58+
MessengerClientInitRequest<
59+
MoneyAccountControllerMessenger,
60+
MoneyAccountControllerInitMessenger
61+
>
62+
>;
63+
64+
return { requestMock, baseMessenger };
65+
}
66+
67+
function publishStateChange(
68+
baseMessenger: ExtendedMessenger<
69+
MockAnyNamespace,
70+
never,
71+
RemoteFeatureFlagControllerStateChangeEvent
72+
>,
73+
) {
74+
baseMessenger.publish(
75+
'RemoteFeatureFlagController:stateChange',
76+
{ remoteFeatureFlags: {}, cacheTimestamp: 0 },
77+
[],
78+
);
2679
}
2780

2881
describe('moneyAccountControllerInit', () => {
82+
beforeEach(() => {
83+
jest.clearAllMocks();
84+
});
85+
2986
it('initializes the controller', () => {
30-
const { controller } = moneyAccountControllerInit(getInitRequestMock());
87+
const { requestMock } = buildInitRequestMock();
88+
const { controller } = moneyAccountControllerInit(requestMock);
3189
expect(controller).toBeInstanceOf(MoneyAccountController);
3290
});
3391

3492
it('passes the proper arguments to the controller', () => {
35-
moneyAccountControllerInit(getInitRequestMock());
93+
const { requestMock } = buildInitRequestMock();
94+
moneyAccountControllerInit(requestMock);
3695

3796
const controllerMock = jest.mocked(MoneyAccountController);
3897
expect(controllerMock).toHaveBeenCalledWith({
3998
messenger: expect.any(Object),
4099
state: undefined,
41100
});
42101
});
102+
103+
describe('RemoteFeatureFlagController:stateChange subscription', () => {
104+
function buildStateChangeSetup() {
105+
const baseMessenger = new ExtendedMessenger<
106+
MockAnyNamespace,
107+
never,
108+
RemoteFeatureFlagControllerStateChangeEvent
109+
>({ namespace: MOCK_ANY_NAMESPACE });
110+
111+
const { requestMock } = buildInitRequestMock(baseMessenger);
112+
return { requestMock, baseMessenger };
113+
}
114+
115+
it('calls controller.init() when flag is enabled and keyring is unlocked', async () => {
116+
jest.mocked(isMoneyAccountEnabled).mockReturnValue(true);
117+
118+
const { requestMock, baseMessenger } = buildStateChangeSetup();
119+
const { controller } = moneyAccountControllerInit(requestMock);
120+
(controller as unknown as { state: unknown }).state =
121+
EMPTY_MONEY_ACCOUNTS;
122+
123+
publishStateChange(baseMessenger);
124+
await Promise.resolve();
125+
126+
expect(jest.mocked(controller.init)).toHaveBeenCalledTimes(1);
127+
});
128+
129+
it('does not call controller.init() when flag is enabled but keyring is locked', async () => {
130+
jest.mocked(isMoneyAccountEnabled).mockReturnValue(true);
131+
132+
const { requestMock, baseMessenger } = buildStateChangeSetup();
133+
134+
baseMessenger.unregisterActionHandler(
135+
// @ts-expect-error: Action not allowed on root messenger.
136+
'KeyringController:getState',
137+
);
138+
baseMessenger.registerActionHandler(
139+
// @ts-expect-error: Action not allowed on root messenger.
140+
'KeyringController:getState',
141+
jest.fn().mockReturnValue({ isUnlocked: false }),
142+
);
143+
144+
const { controller } = moneyAccountControllerInit(requestMock);
145+
(controller as unknown as { state: unknown }).state =
146+
EMPTY_MONEY_ACCOUNTS;
147+
148+
publishStateChange(baseMessenger);
149+
await Promise.resolve();
150+
151+
expect(jest.mocked(controller.init)).not.toHaveBeenCalled();
152+
});
153+
154+
it('does not call controller.init() when flag is enabled but money account already exists', async () => {
155+
jest.mocked(isMoneyAccountEnabled).mockReturnValue(true);
156+
157+
const { requestMock, baseMessenger } = buildStateChangeSetup();
158+
const { controller } = moneyAccountControllerInit(requestMock);
159+
(controller as unknown as { state: unknown }).state =
160+
NON_EMPTY_MONEY_ACCOUNTS;
161+
162+
publishStateChange(baseMessenger);
163+
await Promise.resolve();
164+
165+
expect(jest.mocked(controller.init)).not.toHaveBeenCalled();
166+
});
167+
168+
it('calls controller.clearState() when flag is disabled and money accounts exist', async () => {
169+
jest.mocked(isMoneyAccountEnabled).mockReturnValue(false);
170+
171+
const { requestMock, baseMessenger } = buildStateChangeSetup();
172+
const { controller } = moneyAccountControllerInit(requestMock);
173+
(controller as unknown as { state: unknown }).state =
174+
NON_EMPTY_MONEY_ACCOUNTS;
175+
176+
publishStateChange(baseMessenger);
177+
await Promise.resolve();
178+
179+
expect(jest.mocked(controller.clearState)).toHaveBeenCalledTimes(1);
180+
});
181+
182+
it('does not call controller.clearState() when flag is disabled and no money accounts exist', async () => {
183+
jest.mocked(isMoneyAccountEnabled).mockReturnValue(false);
184+
185+
const { requestMock, baseMessenger } = buildStateChangeSetup();
186+
const { controller } = moneyAccountControllerInit(requestMock);
187+
(controller as unknown as { state: unknown }).state =
188+
EMPTY_MONEY_ACCOUNTS;
189+
190+
publishStateChange(baseMessenger);
191+
await Promise.resolve();
192+
193+
expect(jest.mocked(controller.clearState)).not.toHaveBeenCalled();
194+
});
195+
196+
it('logs an error when the stateChange callback throws', async () => {
197+
const error = new Error('mock error');
198+
jest.mocked(isMoneyAccountEnabled).mockImplementation(() => {
199+
throw error;
200+
});
201+
202+
const { requestMock, baseMessenger } = buildStateChangeSetup();
203+
moneyAccountControllerInit(requestMock);
204+
205+
publishStateChange(baseMessenger);
206+
await Promise.resolve();
207+
208+
expect(jest.mocked(Logger.error)).toHaveBeenCalledWith(
209+
error,
210+
'MoneyAccountController: error handling RemoteFeatureFlagController state change',
211+
);
212+
});
213+
});
43214
});

0 commit comments

Comments
 (0)