Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Refactor/12873 modular controller init #13065

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 30 additions & 22 deletions app/core/Engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,8 @@ import {
AccountsControllerSelectedAccountChangeEvent,
AccountsControllerAccountAddedEvent,
AccountsControllerAccountRenamedEvent,
} from './controllers/AccountsController/constants';
import { AccountsControllerMessenger } from '@metamask/accounts-controller';
import { createAccountsController } from './controllers/AccountsController/utils';
import { createRemoteFeatureFlagController } from './controllers/RemoteFeatureFlagController';
} from './controllers/accounts-controller/constants';
import { createRemoteFeatureFlagController } from './controllers/remote-feature-flag-controller';
import { captureException } from '@sentry/react-native';
import { lowerCase } from 'lodash';
import {
Expand Down Expand Up @@ -215,6 +213,9 @@ import {
getGlobalNetworkClientId,
} from '../../util/networks/global-network';
import { logEngineCreation } from './utils/logger';
import { initControllers } from './utils';
import { AccountsControllerInit } from './controllers/accounts-controller/utils';
import { Controller, ControllerInitFunction } from './modular-controller.types';

const NON_EMPTY = 'NON_EMPTY';

Expand Down Expand Up @@ -356,26 +357,15 @@ export class Engine {
chainId: getGlobalChainId(networkController),
});

// Create AccountsController
const accountsControllerMessenger: AccountsControllerMessenger =
this.controllerMessenger.getRestricted({
name: 'AccountsController',
allowedEvents: [
'SnapController:stateChange',
'KeyringController:accountRemoved',
'KeyringController:stateChange',
],
allowedActions: [
'KeyringController:getAccounts',
'KeyringController:getKeyringsByType',
'KeyringController:getKeyringForAccount',
],
});
const accountsController = createAccountsController({
messenger: accountsControllerMessenger,
initialState: initialState.AccountsController,
// Modular controller initialization
const controllerInitFunctions = [AccountsControllerInit];
const { controllersByName } = this.#initControllers({
initFunctions: controllerInitFunctions,
initState: initialState as EngineState,
});

const accountsController = controllersByName.AccountsController;

const nftController = new NftController({
chainId: getGlobalChainId(networkController),
useIpfsSubdomains: false,
Expand Down Expand Up @@ -1566,6 +1556,24 @@ export class Engine {
Engine.instance = this;
}

#initControllers({
initFunctions,
initState,
}: {
initFunctions: ControllerInitFunction<Controller>[];
initState: EngineState;
}) {
const initRequest = {
baseControllerMessenger: this.controllerMessenger,
persistedState: initState,
};

return initControllers({
initFunctions,
initRequest,
});
}

// Logs the "Transaction Finalized" event after a transaction was either confirmed, dropped or failed.
_handleTransactionFinalizedEvent = async (
transactionEventPayload: TransactionEventPayload,
Expand Down
45 changes: 0 additions & 45 deletions app/core/Engine/controllers/AccountsController/utils.ts

This file was deleted.

File renamed without changes.
39 changes: 39 additions & 0 deletions app/core/Engine/controllers/accounts-controller/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
AccountsController,
AccountsControllerState,
} from '@metamask/accounts-controller';
import { logAccountsControllerCreation } from './logger';

// Default AccountsControllerState
export const defaultAccountsControllerState: AccountsControllerState = {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
};

import { ControllerInitFunction } from '../../modular-controller.types';
import { getAccountsControllerMessenger } from '../../messengers/accounts-controller-messenger';

export const AccountsControllerInit: ControllerInitFunction<
AccountsController
> = (request) => {
const {
baseControllerMessenger,
persistedState,
// getController,
} = request;

const controllerMessenger = getAccountsControllerMessenger(
baseControllerMessenger,
);

logAccountsControllerCreation(persistedState.AccountsController);

const controller = new AccountsController({
messenger: controllerMessenger,
state: persistedState.AccountsController ?? defaultAccountsControllerState,
});

return { controller };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './utils';
export * from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
KeyringControllerAccountRemovedEvent,
KeyringControllerGetAccountsAction,
KeyringControllerGetKeyringForAccountAction,
KeyringControllerGetKeyringsByTypeAction,
KeyringControllerStateChangeEvent,
} from '@metamask/keyring-controller';
import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers';

export type MessengerActions =
| KeyringControllerGetAccountsAction
| KeyringControllerGetKeyringsByTypeAction
| KeyringControllerGetKeyringForAccountAction;

export type MessengerEvents =
| SnapControllerStateChangeEvent
| KeyringControllerAccountRemovedEvent
| KeyringControllerStateChangeEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AccountsControllerMessenger } from '@metamask/accounts-controller';
import { ControllerMessenger } from '../../types';

export function getAccountsControllerMessenger(
controllerMessenger: ControllerMessenger,
): AccountsControllerMessenger {
return controllerMessenger.getRestricted({
name: 'AccountsController',
allowedEvents: [
'SnapController:stateChange',
'KeyringController:accountRemoved',
'KeyringController:stateChange',
],
allowedActions: [
'KeyringController:getAccounts',
'KeyringController:getKeyringsByType',
'KeyringController:getKeyringForAccount',
],
});
}
88 changes: 88 additions & 0 deletions app/core/Engine/modular-controller.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
ActionConstraint,
EventConstraint,
RestrictedControllerMessenger,
} from '@metamask/base-controller';
import { ControllerMessenger, StatefulControllers } from './types';

export type Controller = StatefulControllers[keyof StatefulControllers];

/** The supported controller names. */
export type ControllerName = keyof StatefulControllers;

/** All controller types by name. */
export type ControllerByName = {
[name in ControllerName]: StatefulControllers[name];
};

/**
* Persisted state for all controllers.
* e.g. `{ TransactionController: { transactions: [] } }`.
*/
export type ControllerPersistedState = {
[name in ControllerName]: ControllerByName[name]['state'];
};

/** Generic controller messenger using base template types. */
export type BaseControllerMessenger = ControllerMessenger;

/** Generic restricted controller messenger using base template types. */
export type BaseRestrictedControllerMessenger = RestrictedControllerMessenger<
string,
ActionConstraint,
EventConstraint,
string,
string
>;

/**
* Request to initialize and return a controller instance.
* Includes standard data and methods not coupled to any specific controller.
*/
export type ControllerInitRequest = {
/**
* Base controller messenger for the client.
* Used to generate controller and init messengers for each controller.
*/
baseControllerMessenger: ControllerMessenger;

/**
* Retrieve a controller instance by name.
* Throws an error if the controller is not yet initialized.
*
* @param name - The name of the controller to retrieve.
*/
getController<Name extends ControllerName>(
name: Name,
): ControllerByName[Name];

/**
* The full persisted state for all controllers.
* Includes controller name properties.
* e.g. `{ TransactionController: { transactions: [] } }`.
*/
persistedState: ControllerPersistedState;
};

/**
* A single background API method available to the UI.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ControllerApi = (...args: any[]) => unknown;

/**
* Result of initializing a controller instance.
*/
export type ControllerInitResult<ControllerType extends Controller> = {
/**
* The initialized controller instance.
*/
controller: ControllerType;
};

/**
* Function to initialize a controller instance and return associated data.
*/
export type ControllerInitFunction<ControllerType extends Controller> = (
request: ControllerInitRequest,
) => ControllerInitResult<ControllerType>;
1 change: 1 addition & 0 deletions app/core/Engine/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './utils';
84 changes: 84 additions & 0 deletions app/core/Engine/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// import { createProjectLogger } from '@metamask/utils';
import {
ControllerByName,
ControllerInitFunction,
ControllerInitRequest,
ControllerName,
} from '../modular-controller.types';
import { Controller } from '../modular-controller.types';

// const log = createProjectLogger('controller-init');

/** Result of initializing controllers. */
export type InitControllersResult = {
/** All initialized controllers keyed by name. */
controllersByName: ControllerByName;
};

/**
* Initialize the controllers according to the provided init objects.
* Each init object can be a function that returns a controller, or a `ControllerInit` instance.
*
* @param options - Options bag.
* @param options.initFunctions - Array of init functions.
* @param options.initRequest - Base request used to initialize the controllers.
* Excluding the properties that are generated by this function.
* @returns The initialized controllers and associated data.
*/
export function initControllers({
initFunctions,
initRequest,
}: {
initFunctions: ControllerInitFunction<Controller>[];
initRequest: Omit<ControllerInitRequest, 'getController'>;
}): InitControllersResult {
// log('Initializing controllers', initFunctions.length);

// Will be populated with all controllers once initialized.
let controllersByName = {} as ControllerByName;

const getController = <Name extends ControllerName>(
name: Name,
): ControllerByName[Name] => getControllerOrThrow(controllersByName, name);

for (const initFunction of initFunctions) {
const finalInitRequest: ControllerInitRequest = {
...initRequest,
getController,
};

const result = initFunction(finalInitRequest);

const { controller } = result;

const { name } = controller;

controllersByName = {
...controllersByName,
[name]: controller,
};

// log('Initialized controller', name, {
// api: Object.keys(api),
// persistedStateKey,
// memStateKey,
// });
}

return {
controllersByName,
};
}

function getControllerOrThrow<Name extends ControllerName>(
controllersByName: ControllerByName,
name: Name,
): ControllerByName[Name] {
const controller = controllersByName[name];

if (!controller) {
throw new Error(`Controller requested before it was initialized: ${name}`);
}

return controller;
}
Loading