Skip to content

Commit 2adeecd

Browse files
chore(deps): Bump @metamask/composable-controller from ^3.0.0 to ^10.0.0 (#10441)
## **Description** This commit updates `@metamask/composable-controller` to `^10.0.0`. This involves fixing bugs outlined in #10073, and applying the major changes to the `ComposableController` API that has accumulated between the intervening versions. ## Blocked by - MetaMask/core#4968 - `composable-controller` v10 has been written to ensure that the effect of this optimization is maximized. - #12348 - #12407 - ~#11162 ## Changelog ### Changed - **BREAKING:** Bump `@metamask/composable-controller` from `^3.0.0` to `^10.0.0`. - **BREAKING:** Instantiate `ComposableController` class constructor option `messenger` with a `RestrictedControllerMessenger` instance derived from the `controllerMessenger` class field instance of `Engine`, instead of passing in the unrestricted `controllerMessenger` instance directly. - **BREAKING:** Narrow external actions allowlist for `messenger` instance passed into `ComposableController` constructor from `GlobalActions['type']` to an empty union. - **BREAKING:** Narrow external events allowlist for `messenger` instance passed into `ComposableController` constructor from `GlobalEvents['type']` to a union of the `stateChange` events of all controllers included in the `EngineState` type. - Convert the `EngineState` interface to use type alias syntax to ensure compatibility with types used in MetaMask controllers. ### Fixed - **BREAKING:** Narrow `Engine` class `datamodel` field from `any` to `ComposableController<EngineState, StatefulControllers>`. - **BREAKING:** The `CurrencyRatesController` constructor now normalizes `null` into 0 for the `conversionRate` values of all native currencies keyed in the `currencyRates` object of `CurrencyRatesControllerState`. - Restore previously suppressed type-level validation for `ComposableController` constructor option `controllers`. ## **Related issues** - Closes #10073 - Applies MetaMask/core#4467 - Blocked by `@metamask/[email protected]` release. - Supersedes #10011 ## **Manual testing steps** ## **Screenshots/Recordings** ## **Pre-merge author checklist** - [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. ## **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. --------- Co-authored-by: Salah-Eddine Saakoun <[email protected]>
1 parent bf126d0 commit 2adeecd

File tree

11 files changed

+166
-82
lines changed

11 files changed

+166
-82
lines changed

app/core/BackgroundBridge/BackgroundBridge.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ export class BackgroundBridge extends EventEmitter {
497497
*/
498498
getState() {
499499
const vault = Engine.context.KeyringController.state.vault;
500-
const { selectedAddress } = Engine.datamodel.flatState;
500+
const { PreferencesController: { selectedAddress } } = Engine.datamodel.state;
501501
return {
502502
isInitialized: !!vault,
503503
isUnlocked: true,

app/core/BackgroundBridge/WalletConnectPort.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ class WalletConnectPort extends EventEmitter {
2323
if (msg?.data?.method === NOTIFICATION_NAMES.chainChanged) {
2424
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2525
// @ts-ignore
26-
const { selectedAddress } = Engine.datamodel.flatState;
26+
const {
27+
PreferencesController: { selectedAddress },
28+
} = Engine.datamodel.state;
2729
this._wcRequestActions?.updateSession?.({
2830
chainId: parseInt(msg.data.params.chainId, 16),
2931
accounts: [selectedAddress],

app/core/Engine/Engine.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,7 @@ describe('Engine', () => {
7070
it('matches initial state fixture', () => {
7171
const engine = Engine.init({});
7272
const initialBackgroundState = engine.datamodel.state;
73-
74-
// AssetsContractController is stateless in v37 resulting in an undefined state
75-
const newBackgroundState = {
76-
...backgroundState,
77-
AssetsContractController: undefined,
78-
};
79-
80-
expect(initialBackgroundState).toStrictEqual(newBackgroundState);
73+
expect(initialBackgroundState).toStrictEqual(backgroundState);
8174
});
8275

8376
it('setSelectedAccount throws an error if no account exists for the given address', () => {
@@ -89,6 +82,30 @@ describe('Engine', () => {
8982
);
9083
});
9184

85+
it('normalizes CurrencyController state property conversionRate from null to 0', () => {
86+
const ticker = 'ETH';
87+
const state = {
88+
CurrencyRateController: {
89+
currentCurrency: 'usd' as const,
90+
currencyRates: {
91+
[ticker]: {
92+
conversionRate: null,
93+
conversionDate: 0,
94+
usdConversionRate: null,
95+
},
96+
},
97+
},
98+
};
99+
const engine = Engine.init(state);
100+
expect(
101+
engine.datamodel.state.CurrencyRateController.currencyRates[ticker],
102+
).toStrictEqual({
103+
conversionRate: 0,
104+
conversionDate: 0,
105+
usdConversionRate: null,
106+
});
107+
});
108+
92109
describe('getTotalFiatAccountBalance', () => {
93110
let engine: EngineClass;
94111
afterEach(() => engine?.destroyEngineInstance());

app/core/Engine/Engine.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,15 @@ import { JsonMap } from '../Analytics/MetaMetrics.types';
200200
import { isPooledStakingFeatureEnabled } from '../../components/UI/Stake/constants';
201201
import {
202202
ControllerMessenger,
203-
Controllers,
204203
EngineState,
205204
EngineContext,
206205
TransactionEventPayload,
206+
StatefulControllers,
207207
} from './types';
208+
import {
209+
BACKGROUND_STATE_CHANGE_EVENT_NAMES,
210+
STATELESS_NON_CONTROLLER_NAMES,
211+
} from './constants';
208212
import {
209213
getGlobalChainId,
210214
getGlobalNetworkClientId,
@@ -239,9 +243,7 @@ export class Engine {
239243
/**
240244
* ComposableController reference containing all child controllers
241245
*/
242-
// TODO: Replace "any" with type
243-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
244-
datamodel: any;
246+
datamodel: ComposableController<EngineState, StatefulControllers>;
245247

246248
/**
247249
* Object containing the info for the latest incoming tx block
@@ -449,7 +451,25 @@ export class Engine {
449451
allowedActions: [`${networkController.name}:getNetworkClientById`],
450452
allowedEvents: [],
451453
}),
452-
state: initialState.CurrencyRateController,
454+
// normalize `null` currencyRate to `0`
455+
// TODO: handle `null` currencyRate by hiding fiat values instead
456+
state: {
457+
...initialState.CurrencyRateController,
458+
currencyRates: Object.fromEntries(
459+
Object.entries(
460+
initialState.CurrencyRateController?.currencyRates ?? {
461+
ETH: {
462+
conversionRate: 0,
463+
conversionDate: 0,
464+
usdConversionRate: null,
465+
},
466+
},
467+
).map(([k, v]) => [
468+
k,
469+
{ ...v, conversionRate: v.conversionRate ?? 0 },
470+
]),
471+
),
472+
},
453473
});
454474

455475
const gasFeeController = new GasFeeController({
@@ -1457,24 +1477,21 @@ export class Engine {
14571477
}),
14581478
};
14591479

1460-
// Avoiding `Object.values` and `getKnownPropertyNames` for performance benefits: https://www.measurethat.net/Benchmarks/Show/7173/0/objectvalues-vs-reduce
1461-
const controllers = (
1462-
Object.keys(this.context) as (keyof Controllers)[]
1463-
).reduce<Controllers[keyof Controllers][]>(
1464-
(controllers, controllerName) => {
1465-
const controller = this.context[controllerName];
1466-
if (controller) {
1467-
controllers.push(controller);
1468-
}
1469-
return controllers;
1480+
const childControllers = Object.assign({}, this.context);
1481+
STATELESS_NON_CONTROLLER_NAMES.forEach((name) => {
1482+
if (name in childControllers && childControllers[name]) {
1483+
delete childControllers[name];
1484+
}
1485+
});
1486+
this.datamodel = new ComposableController<EngineState, StatefulControllers>(
1487+
{
1488+
controllers: childControllers as StatefulControllers,
1489+
messenger: this.controllerMessenger.getRestricted({
1490+
name: 'ComposableController',
1491+
allowedActions: [],
1492+
allowedEvents: Array.from(BACKGROUND_STATE_CHANGE_EVENT_NAMES),
1493+
}),
14701494
},
1471-
[],
1472-
);
1473-
1474-
this.datamodel = new ComposableController(
1475-
// @ts-expect-error TODO: Filter out non-controller instances
1476-
controllers,
1477-
this.controllerMessenger,
14781495
);
14791496

14801497
const { NftController: nfts } = this.context;
@@ -2074,22 +2091,12 @@ export default {
20742091
AccountsController,
20752092
} = instance.datamodel.state;
20762093

2077-
// normalize `null` currencyRate to `0`
2078-
// TODO: handle `null` currencyRate by hiding fiat values instead
2079-
const modifiedCurrencyRateControllerState = {
2080-
...CurrencyRateController,
2081-
conversionRate:
2082-
CurrencyRateController.conversionRate === null
2083-
? 0
2084-
: CurrencyRateController.conversionRate,
2085-
};
2086-
20872094
return {
20882095
AccountTrackerController,
20892096
AddressBookController,
20902097
NftController,
20912098
TokenListController,
2092-
CurrencyRateController: modifiedCurrencyRateControllerState,
2099+
CurrencyRateController,
20932100
KeyringController,
20942101
NetworkController,
20952102
PhishingController,

app/core/Engine/constants.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Messageable modules that are part of the Engine's context, but are not defined with state.
3+
* TODO: Replace with type guard once consistent inheritance for non-controllers is implemented. See: https://github.com/MetaMask/decisions/pull/41
4+
*/
5+
export const STATELESS_NON_CONTROLLER_NAMES = [
6+
'AssetsContractController',
7+
'NftDetectionController',
8+
'TokenDetectionController',
9+
] as const;
10+
11+
export const BACKGROUND_STATE_CHANGE_EVENT_NAMES = [
12+
'AccountsController:stateChange',
13+
'AccountTrackerController:stateChange',
14+
'AddressBookController:stateChange',
15+
'ApprovalController:stateChange',
16+
'CurrencyRateController:stateChange',
17+
'GasFeeController:stateChange',
18+
'KeyringController:stateChange',
19+
'LoggingController:stateChange',
20+
'NetworkController:stateChange',
21+
'NftController:stateChange',
22+
'PermissionController:stateChange',
23+
'PhishingController:stateChange',
24+
'PPOMController:stateChange',
25+
'PreferencesController:stateChange',
26+
'RemoteFeatureFlagController:stateChange',
27+
'SelectedNetworkController:stateChange',
28+
'SignatureController:stateChange',
29+
'SmartTransactionsController:stateChange',
30+
'SwapsController:stateChange',
31+
'TokenBalancesController:stateChange',
32+
'TokenListController:stateChange',
33+
'TokenRatesController:stateChange',
34+
'TokensController:stateChange',
35+
'TransactionController:stateChange',
36+
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
37+
'SnapController:stateChange',
38+
'SnapsRegistry:stateChange',
39+
'SubjectMetadataController:stateChange',
40+
'AuthenticationController:stateChange',
41+
'UserStorageController:stateChange',
42+
'NotificationServicesController:stateChange',
43+
'NotificationServicesPushController:stateChange',
44+
///: END:ONLY_INCLUDE_IF
45+
] as const;

app/core/Engine/types.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,16 @@ import {
159159
AccountsControllerState,
160160
} from '@metamask/accounts-controller';
161161
import { getPermissionSpecifications } from '../Permissions/specifications.js';
162+
import { ComposableControllerEvents } from '@metamask/composable-controller';
163+
import { STATELESS_NON_CONTROLLER_NAMES } from './constants';
162164
import {
163165
RemoteFeatureFlagController,
164-
RemoteFeatureFlagControllerState
166+
RemoteFeatureFlagControllerState,
165167
} from '@metamask/remote-feature-flag-controller';
168+
import {
169+
RemoteFeatureFlagControllerActions,
170+
RemoteFeatureFlagControllerEvents,
171+
} from '@metamask/remote-feature-flag-controller/dist/remote-feature-flag-controller.cjs';
166172

167173
/**
168174
* Controllers that area always instantiated
@@ -174,6 +180,14 @@ type RequiredControllers = Omit<Controllers, 'PPOMController'>;
174180
*/
175181
type OptionalControllers = Pick<Controllers, 'PPOMController'>;
176182

183+
/**
184+
* Controllers that are defined with state.
185+
*/
186+
export type StatefulControllers = Omit<
187+
Controllers,
188+
(typeof STATELESS_NON_CONTROLLER_NAMES)[number]
189+
>;
190+
177191
type PermissionsByRpcMethod = ReturnType<typeof getPermissionSpecifications>;
178192
type Permissions = PermissionsByRpcMethod[keyof PermissionsByRpcMethod];
179193

@@ -223,9 +237,11 @@ type GlobalActions =
223237
| TransactionControllerActions
224238
| SelectedNetworkControllerActions
225239
| SmartTransactionsControllerActions
226-
| AssetsContractControllerActions;
240+
| AssetsContractControllerActions
241+
| RemoteFeatureFlagControllerActions;
227242

228243
type GlobalEvents =
244+
| ComposableControllerEvents<EngineState>
229245
| AccountTrackerControllerEvents
230246
| NftControllerEvents
231247
| SwapsControllerEvents
@@ -255,7 +271,8 @@ type GlobalEvents =
255271
| TransactionControllerEvents
256272
| SelectedNetworkControllerEvents
257273
| SmartTransactionsControllerEvents
258-
| AssetsContractControllerEvents;
274+
| AssetsContractControllerEvents
275+
| RemoteFeatureFlagControllerEvents;
259276

260277
// TODO: Abstract this into controller utils for TransactionController
261278
export interface TransactionEventPayload {
@@ -276,7 +293,10 @@ export type ControllerMessenger = ExtendedControllerMessenger<
276293
/**
277294
* All mobile controllers, keyed by name
278295
*/
279-
export interface Controllers {
296+
// Interfaces are incompatible with our controllers and state types by default.
297+
// Adding an index signature fixes this, but at the cost of widening the type unnecessarily.
298+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
299+
export type Controllers = {
280300
AccountsController: AccountsController;
281301
AccountTrackerController: AccountTrackerController;
282302
AddressBookController: AddressBookController;
@@ -316,7 +336,7 @@ export interface Controllers {
316336
NotificationServicesPushController: NotificationServicesPushController.Controller;
317337
///: END:ONLY_INCLUDE_IF
318338
SwapsController: SwapsController;
319-
}
339+
};
320340

321341
/**
322342
* Combines required and optional controllers for the Engine context type.
@@ -326,7 +346,10 @@ export type EngineContext = RequiredControllers & Partial<OptionalControllers>;
326346
/**
327347
* All engine state, keyed by controller name
328348
*/
329-
export interface EngineState {
349+
// Interfaces are incompatible with our controllers and state types by default.
350+
// Adding an index signature fixes this, but at the cost of widening the type unnecessarily.
351+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
352+
export type EngineState = {
330353
AccountTrackerController: AccountTrackerControllerState;
331354
AddressBookController: AddressBookControllerState;
332355
NftController: NftControllerState;
@@ -359,4 +382,4 @@ export interface EngineState {
359382
PPOMController: PPOMState;
360383
AccountsController: AccountsControllerState;
361384
SelectedNetworkController: SelectedNetworkControllerState;
362-
}
385+
};

app/core/EngineService/EngineService.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ jest.mock('../Engine', () => {
3535
destroyEngine: jest.fn(),
3636
init: jest.fn((_, keyringState) => {
3737
instance = {
38-
controllerMessenger: { subscribe: jest.fn() },
38+
controllerMessenger: {
39+
subscribe: jest.fn(),
40+
subscribeOnceIf: jest.fn(),
41+
},
3942
context: {
4043
AddressBookController: { subscribe: jest.fn() },
4144
KeyringController: {

app/core/EngineService/EngineService.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,29 +173,27 @@ class EngineService {
173173
},
174174
];
175175

176-
engine?.datamodel?.subscribe?.(() => {
177-
if (!engine.context.KeyringController.metadata.vault) {
178-
Logger.log('keyringController vault missing for INIT_BG_STATE_KEY');
179-
}
180-
if (!this.engineInitialized) {
176+
engine.controllerMessenger.subscribeOnceIf(
177+
'ComposableController:stateChange',
178+
() => {
179+
if (!engine.context.KeyringController.metadata.vault) {
180+
Logger.log('keyringController vault missing for INIT_BG_STATE_KEY');
181+
}
181182
store.dispatch({ type: INIT_BG_STATE_KEY });
182183
this.engineInitialized = true;
183-
}
184-
});
184+
},
185+
() => !this.engineInitialized,
186+
);
185187

186188
controllers.forEach((controller) => {
187-
const { name, key = undefined } = controller;
189+
const { name, key } = controller;
188190
const update_bg_state_cb = () => {
189191
if (!engine.context.KeyringController.metadata.vault) {
190192
Logger.log('keyringController vault missing for UPDATE_BG_STATE_KEY');
191193
}
192194
store.dispatch({ type: UPDATE_BG_STATE_KEY, payload: { key: name } });
193195
};
194-
if (key) {
195-
engine.controllerMessenger.subscribe(key, update_bg_state_cb);
196-
} else {
197-
engine.context[name].subscribe(update_bg_state_cb);
198-
}
196+
engine.controllerMessenger.subscribe(key, update_bg_state_cb);
199197
});
200198
};
201199

app/util/test/initial-background-state.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"AddressBookController": {
2222
"addressBook": {}
2323
},
24-
"AssetsContractController": {},
2524
"NftController": {
2625
"allNftContracts": {},
2726
"allNfts": {},
@@ -40,8 +39,6 @@
4039
"tokensChainsCache": {},
4140
"preventPollingOnNetworkRestart": false
4241
},
43-
"TokenDetectionController": {},
44-
"NftDetectionController": {},
4542
"PPOMController": {
4643
"storageMetadata": [],
4744
"versionInfo": []

0 commit comments

Comments
 (0)