Skip to content

Commit

Permalink
Move execution service to separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Feb 4, 2025
1 parent 11d0475 commit 9169d9f
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 39 deletions.
4 changes: 3 additions & 1 deletion app/scripts/controller-init/controller-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TransactionUpdateController } from '@metamask-institutional/transaction
import { AccountsController } from '@metamask/accounts-controller';
import {
CronjobController,
ExecutionService,
JsonSnapsRegistry,
SnapController,
SnapInsightsController,
Expand All @@ -31,6 +32,7 @@ import SwapsController from '../controllers/swaps';
*/
export type Controller =
| CronjobController
| ExecutionService
| GasFeeController
| JsonSnapsRegistry
| KeyringController
Expand All @@ -44,7 +46,6 @@ export type Controller =
| PreferencesController
| RateLimitController<RateLimitedApiMap>
| SmartTransactionsController
// TODO: Update `name` to `SnapController` instead of `string`.
| SnapController
| SnapInterfaceController
| SnapInsightsController
Expand All @@ -60,6 +61,7 @@ export type Controller =
*/
export type ControllerFlatState = AccountsController['state'] &
CronjobController['state'] &
ExecutionService['state'] &
GasFeeController['state'] &
JsonSnapsRegistry['state'] &
KeyringController['state'] &
Expand Down
4 changes: 4 additions & 0 deletions app/scripts/controller-init/messengers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
getCronjobControllerMessenger,
getExecutionServiceMessenger,
getRateLimitControllerInitMessenger,
getRateLimitControllerMessenger,
getSnapControllerInitMessenger,
Expand All @@ -21,6 +22,9 @@ export const CONTROLLER_MESSENGERS = {
CronjobController: {
getMessenger: getCronjobControllerMessenger,
},
ExecutionService: {
getMessenger: getExecutionServiceMessenger,
},
RateLimitController: {
getMessenger: getRateLimitControllerMessenger,
getInitMessenger: getRateLimitControllerInitMessenger,
Expand Down
65 changes: 65 additions & 0 deletions app/scripts/controller-init/snaps/execution-service-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
IframeExecutionService,
OffscreenExecutionService,
} from '@metamask/snaps-controllers';
import { Messenger } from '@metamask/base-controller';
import { ControllerInitRequest } from '../types';
import { buildControllerInitRequestMock } from '../test/utils';
import {
ExecutionServiceMessenger,
getExecutionServiceMessenger,
} from './execution-service-messenger';
import { ExecutionServiceInit } from './execution-service-init';

jest.mock('@metamask/snaps-controllers');
jest.mock('../../../../shared/modules/mv3.utils', () => ({
isManifestV3: true,
}));

function getInitRequestMock(): jest.Mocked<
ControllerInitRequest<ExecutionServiceMessenger>
> {
const baseMessenger = new Messenger<never, never>();

const requestMock = {
...buildControllerInitRequestMock(),
controllerMessenger: getExecutionServiceMessenger(baseMessenger),
};

return requestMock;
}

describe('ExecutionServiceInit', () => {
it('initializes the iframe execution service if `chrome.offscreen` is not available', () => {
const { controller } = ExecutionServiceInit(getInitRequestMock());
expect(controller).toBeInstanceOf(IframeExecutionService);
});

it('initializes the offscreen execution service if `chrome.offscreen` is available', () => {
Object.defineProperty(global, 'chrome', {
value: {
offscreen: {},
},
});

const { controller } = ExecutionServiceInit(getInitRequestMock());
expect(controller).toBeInstanceOf(OffscreenExecutionService);
});

it('passes the proper arguments to the service', () => {
Object.defineProperty(global, 'chrome', {
value: {
offscreen: {},
},
});

ExecutionServiceInit(getInitRequestMock());

const controllerMock = jest.mocked(OffscreenExecutionService);
expect(controllerMock).toHaveBeenCalledWith({
messenger: expect.any(Object),
offscreenPromise: expect.any(Promise),
setupSnapProvider: expect.any(Function),
});
});
});
75 changes: 75 additions & 0 deletions app/scripts/controller-init/snaps/execution-service-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
AbstractExecutionService,
IframeExecutionService,
OffscreenExecutionService,
ExecutionServiceArgs,
} from '@metamask/snaps-controllers';
import { assert } from '@metamask/utils';
import { SubjectType } from '@metamask/permission-controller';
import { Duplex } from 'readable-stream';
import { ControllerInitFunction } from '../types';
import { isManifestV3 } from '../../../../shared/modules/mv3.utils';
import { ExecutionServiceMessenger } from './execution-service-messenger';

/**
* Initialize the Snaps execution service.
*
* @param request - The request object.
* @param request.controllerMessenger - The messenger to use for the service.
* @param request.offscreenPromise - The promise that resolves when the
* offscreen document is ready.
* @param request.setupUntrustedCommunicationEip1193 - The setup function for
* EIP-1193 communication.
* @returns The initialized controller.
*/
export const ExecutionServiceInit: ControllerInitFunction<
AbstractExecutionService<unknown>,
ExecutionServiceMessenger
> = ({
controllerMessenger,
offscreenPromise,
setupUntrustedCommunicationEip1193,
}) => {
const useOffscreenDocument =
isManifestV3 &&
typeof chrome !== 'undefined' &&
typeof chrome.offscreen !== 'undefined';

/**
* Set up the EIP-1193 provider for the given Snap.
*
* @param snapId - The ID of the Snap.
* @param connectionStream - The stream to connect to the Snap.
*/
function setupSnapProvider(snapId: string, connectionStream: Duplex) {
return setupUntrustedCommunicationEip1193({
connectionStream,
sender: { snapId },
subjectType: SubjectType.Snap,
});
}

const args: ExecutionServiceArgs = {
messenger: controllerMessenger,
setupSnapProvider,
};

if (useOffscreenDocument) {
return {
controller: new OffscreenExecutionService({
...args,
offscreenPromise,
}),
};
}

const iframeUrl = process.env.IFRAME_EXECUTION_ENVIRONMENT_URL;
assert(iframeUrl, 'Missing iframe URL.');

return {
controller: new IframeExecutionService({
...args,
iframeUrl: new URL(iframeUrl),
}),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Messenger, RestrictedMessenger } from '@metamask/base-controller';
import { getExecutionServiceMessenger } from './execution-service-messenger.ts';

Check failure on line 2 in app/scripts/controller-init/snaps/execution-service-messenger.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Unexpected use of file extension "ts" for "./execution-service-messenger.ts"

describe('getExecutionServiceMessenger', () => {
it('returns a restricted messenger', () => {
const messenger = new Messenger<never, never>();
const executionServiceMessenger =

Check failure on line 7 in app/scripts/controller-init/snaps/execution-service-messenger.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Delete `⏎·····`
getExecutionServiceMessenger(messenger);

expect(executionServiceMessenger).toBeInstanceOf(RestrictedMessenger);
});
});
22 changes: 22 additions & 0 deletions app/scripts/controller-init/snaps/execution-service-messenger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Messenger } from '@metamask/base-controller';

export type ExecutionServiceMessenger = ReturnType<
typeof getExecutionServiceMessenger
>;

/**
* Get a restricted messenger for the execution service. This is scoped to the
* actions and events that the execution service is allowed to handle.
*
* @param messenger - The messenger to restrict.
* @returns The restricted messenger.
*/
export function getExecutionServiceMessenger(
messenger: Messenger<never, never>,
) {
return messenger.getRestricted({
name: 'ExecutionService',
allowedEvents: [],
allowedActions: [],
});
}
2 changes: 2 additions & 0 deletions app/scripts/controller-init/snaps/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { CronjobControllerInit } from './cronjob-controller-init';
export { getCronjobControllerMessenger } from './cronjob-controller-messenger';
export { ExecutionServiceInit } from './execution-service-init';
export { getExecutionServiceMessenger } from './execution-service-messenger';
export { RateLimitControllerInit } from './rate-limit-controller-init';
export {
getRateLimitControllerMessenger,
Expand Down
3 changes: 3 additions & 0 deletions app/scripts/controller-init/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export function buildControllerInitRequestMock(): jest.Mocked<
getPermittedAccounts: jest.fn(),
getProvider: jest.fn(),
getTransactionMetricsRequest: jest.fn(),
offscreenPromise: Promise.resolve(),
persistedState: {},
removeAllConnections: jest.fn(),
setupUntrustedCommunicationEip1193: jest.fn(),
showNotification: jest.fn(),
};
}
30 changes: 30 additions & 0 deletions app/scripts/controller-init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
RestrictedControllerMessenger,
} from '@metamask/base-controller';
import { Hex } from '@metamask/utils';
import { Duplex } from 'readable-stream';
import { SubjectType } from '@metamask/permission-controller';
import { TransactionMetricsRequest } from '../lib/transaction/metrics';
import { MessageSender } from '../../../types/global';
import { Controller, ControllerFlatState } from './controller-list';

/** The supported controller names. */
Expand Down Expand Up @@ -40,6 +43,12 @@ export type BaseRestrictedControllerMessenger = RestrictedControllerMessenger<
string
>;

type SnapSender = {
snapId: string;
};

type Sender = MessageSender | SnapSender;

/**
* Request to initialize and return a controller instance.
* Includes standard data and methods not coupled to any specific controller.
Expand Down Expand Up @@ -104,6 +113,11 @@ export type ControllerInitRequest<
*/
getTransactionMetricsRequest(): TransactionMetricsRequest;

/**
* A promise that resolves when the offscreen document is ready.
*/
offscreenPromise: Promise<void>;

/**
* The full persisted state for all controllers.
* Includes controller name properties.
Expand All @@ -119,6 +133,22 @@ export type ControllerInitRequest<
*/
removeAllConnections(origin: string): void;

/**
* Create a multiplexed stream for connecting to an untrusted context like a
* like a website, Snap, or other extension.
*
* @param options - The options for creating the stream.
* @param options.connectionStream - The stream to connect to the untrusted
* context.
* @param options.sender - The sender of the stream.
* @param options.subjectType - The type of the subject of the stream.
*/
setupUntrustedCommunicationEip1193(options: {
connectionStream: Duplex;
sender: Sender;
subjectType: SubjectType;
}): void;

/**
* Show a native notification.
*
Expand Down
43 changes: 5 additions & 38 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ import { PPOMControllerInit } from './controller-init/confirmations/ppom-control
import { initControllers } from './controller-init/utils';
import {
CronjobControllerInit,
ExecutionServiceInit,
RateLimitControllerInit,
SnapControllerInit,
SnapInsightsControllerInit,
Expand Down Expand Up @@ -1316,30 +1317,6 @@ export default class MetamaskController extends EventEmitter {
subjectCacheLimit: 100,
});

const shouldUseOffscreenExecutionService =
isManifestV3 &&
typeof chrome !== 'undefined' &&
// eslint-disable-next-line no-undef
typeof chrome.offscreen !== 'undefined';

const snapExecutionServiceArgs = {
messenger: this.controllerMessenger.getRestricted({
name: 'ExecutionService',
}),
setupSnapProvider: this.setupSnapProvider.bind(this),
};

this.snapExecutionService =
shouldUseOffscreenExecutionService === false
? new IframeExecutionService({
...snapExecutionServiceArgs,
iframeUrl: new URL(process.env.IFRAME_EXECUTION_ENVIRONMENT_URL),
})
: new OffscreenExecutionService({
...snapExecutionServiceArgs,
offscreenPromise: this.offscreenPromise,
});

// Notification Controllers
this.authenticationController = new AuthenticationController.Controller({
state: initState.AuthenticationController,
Expand Down Expand Up @@ -2138,6 +2115,7 @@ export default class MetamaskController extends EventEmitter {
];

const controllerInitFunctions = {
ExecutionService: ExecutionServiceInit,
RateLimitController: RateLimitControllerInit,
SnapsRegistry: SnapsRegistryInit,
SnapController: SnapControllerInit,
Expand Down Expand Up @@ -5933,20 +5911,6 @@ export default class MetamaskController extends EventEmitter {
}
}

/**
* For snaps running in workers.
*
* @param snapId
* @param connectionStream
*/
setupSnapProvider(snapId, connectionStream) {
this.setupUntrustedCommunicationEip1193({
connectionStream,
sender: { snapId },
subjectType: SubjectType.Snap,
});
}

/**
* A method for creating an ethereum provider that is safely restricted for the requesting subject.
*
Expand Down Expand Up @@ -7609,8 +7573,11 @@ export default class MetamaskController extends EventEmitter {
getStateUI: this._getMetaMaskState.bind(this),
getTransactionMetricsRequest:
this.getTransactionMetricsRequest.bind(this),
offscreenPromise: this.offscreenPromise,
persistedState: initState,
removeAllConnections: this.removeAllConnections.bind(this),
setupUntrustedCommunicationEip1193:
this.setupUntrustedCommunicationEip1193.bind(this),
showNotification: this.platform._showNotification,
};

Expand Down

0 comments on commit 9169d9f

Please sign in to comment.