Skip to content
Merged
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
10 changes: 5 additions & 5 deletions packages/examples/packages/lifecycle-hooks/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `@metamask/lifecylce-hooks-example-snap`

This snap demonstrates how to use the `onInstall` and `onUpdate` lifecycle
hooks.
This Snap demonstrates how to use the `onStart`, `onInstall`, and `onUpdate`
lifecycle hooks.

## Snap manifest

Expand All @@ -22,9 +22,9 @@ Along with other permissions, the manifest of this snap includes the

## Snap usage

This snap exposes the `onInstall` and `onUpdate` lifecycle hooks. These hooks
are called when the snap is installed or updated, respectively, and cannot be
called manually.
This Snap exposes the `onStart`, `onInstall`, and `onUpdate` lifecycle hooks.
These hooks are called when the client is started, when the Snap is installed,
or the Snap is updated, respectively, and cannot be called manually.

For more information, you can refer to
[the end-to-end tests](./src/index.test.ts).
2 changes: 1 addition & 1 deletion packages/examples/packages/lifecycle-hooks/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@metamask/lifecycle-hooks-example-snap",
"version": "2.1.3",
"description": "MetaMask example snap demonstrating the use of the `onInstall` and `onUpdate` lifecycle hooks",
"description": "MetaMask example snap demonstrating the use of the `onStart`, `onInstall`, and `onUpdate` lifecycle hooks",
"keywords": [
"MetaMask",
"Snaps",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"version": "2.1.3",
"description": "MetaMask example snap demonstrating the use of the `onInstall` and `onUpdate` lifecycle hooks.",
"description": "MetaMask example snap demonstrating the use of the `onStart`, `onInstall`, and `onUpdate` lifecycle hooks.",
"proposedName": "Lifecycle Hooks Example Snap",
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "q+mPr3Gq/2PZDIrqKu+wR7sh1hwKk+F5OoEo1ye+30Q=",
"shasum": "c6HavArVNGdBQlFYdVeKXpahvU0DHpB6D6UupoexRR0=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
24 changes: 24 additions & 0 deletions packages/examples/packages/lifecycle-hooks/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ import { describe, expect, it } from '@jest/globals';
import { assertIsAlertDialog, installSnap } from '@metamask/snaps-jest';
import { Box, Text } from '@metamask/snaps-sdk/jsx';

describe('onStart', () => {
it('shows dialog when the client is started', async () => {
const { onStart } = await installSnap();

const response = onStart();

const screen = await response.getInterface();
assertIsAlertDialog(screen);

expect(screen).toRender(
<Box>
<Text>
The client was started successfully, and the "onStart" handler was
called.
</Text>
</Box>,
);

await screen.ok();

expect(await response).toRespondWith(null);
});
});

describe('onInstall', () => {
it('shows dialog when the snap is installed', async () => {
const { onInstall } = await installSnap();
Expand Down
37 changes: 34 additions & 3 deletions packages/examples/packages/lifecycle-hooks/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import type { OnInstallHandler, OnUpdateHandler } from '@metamask/snaps-sdk';
import type {
OnInstallHandler,
OnStartHandler,
OnUpdateHandler,
} from '@metamask/snaps-sdk';
import { Box, Text } from '@metamask/snaps-sdk/jsx';

/**
* Handle starting of the client. This handler is called when the client is
* started, and can be used to perform any initialization that is required.
*
* This handler is optional. If it is not provided, the Snap will be started
* as usual.
*
* @see https://docs.metamask.io/snaps/reference/entry-points/#onstart
* @returns The JSON-RPC response.
*/
export const onStart: OnStartHandler = async () => {
return await snap.request({
method: 'snap_dialog',
params: {
type: 'alert',
content: (
<Box>
<Text>
The client was started successfully, and the "onStart" handler was
called.
</Text>
</Box>
),
},
});
};

/**
* Handle installation of the snap. This handler is called when the snap is
* installed, and can be used to perform any initialization that is required.'
*
* This handler is optional. If it is not provided, the snap will be installed
* as usual.
*
* @see https://docs.metamask.io/snaps/reference/exports/#oninstall
* @see https://docs.metamask.io/snaps/reference/entry-points/#oninstall
* @returns The JSON-RPC response.
*/
export const onInstall: OnInstallHandler = async () => {
Expand All @@ -35,7 +66,7 @@ export const onInstall: OnInstallHandler = async () => {
* This handler is optional. If it is not provided, the snap will be updated
* as usual.
*
* @see https://docs.metamask.io/snaps/reference/exports/#onupdate
* @see https://docs.metamask.io/snaps/reference/entry-points/#onupdate
* @returns The JSON-RPC response.
*/
export const onUpdate: OnUpdateHandler = async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/snaps-controllers/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 95.21,
"functions": 98.71,
"branches": 95.22,
"functions": 98.72,
"lines": 98.87,
"statements": 98.7
"statements": 98.71
}
121 changes: 121 additions & 0 deletions packages/snaps-controllers/src/snaps/SnapController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9889,6 +9889,127 @@ describe('SnapController', () => {
});

describe('SnapController actions', () => {
describe('SnapController:init', () => {
it('calls `onStart` for all Snaps with the `endowment:lifecycle-hooks` permission', async () => {
const rootMessenger = getControllerMessenger();
const messenger = getSnapControllerMessenger(rootMessenger);

rootMessenger.registerActionHandler(
'PermissionController:getPermissions',
(origin) => {
if (origin === MOCK_SNAP_ID) {
return {
[SnapEndowments.LifecycleHooks]:
MOCK_LIFECYCLE_HOOKS_PERMISSION,
};
}

return {};
},
);

rootMessenger.registerActionHandler(
'PermissionController:hasPermission',
(origin) => {
return origin === MOCK_SNAP_ID;
},
);

const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: getPersistedSnapsState(
getPersistedSnapObject({
id: MOCK_SNAP_ID,
}),
getPersistedSnapObject({
id: MOCK_LOCAL_SNAP_ID,
}),
),
},
}),
);

const call = jest.spyOn(messenger, 'call');
messenger.call('SnapController:init');
await sleep(10);

expect(call).toHaveBeenNthCalledWith(
2,
'PermissionController:hasPermission',
MOCK_SNAP_ID,
'endowment:lifecycle-hooks',
);

expect(call).toHaveBeenNthCalledWith(
6,
'ExecutionService:executeSnap',
expect.any(Object),
);

expect(messenger.call).toHaveBeenNthCalledWith(
7,
'ExecutionService:handleRpcRequest',
MOCK_SNAP_ID,
{
handler: HandlerType.OnStart,
origin: METAMASK_ORIGIN,
request: {
jsonrpc: '2.0',
id: expect.any(String),
method: HandlerType.OnStart,
},
},
);

snapController.destroy();
});

it('logs an error if the lifecycle hook throws', async () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation();

const rootMessenger = getControllerMessenger();
const messenger = getSnapControllerMessenger(rootMessenger);

rootMessenger.registerActionHandler(
'PermissionController:getPermissions',
() => {
return {
[SnapEndowments.LifecycleHooks]: MOCK_LIFECYCLE_HOOKS_PERMISSION,
};
},
);

rootMessenger.registerActionHandler(
'ExecutionService:handleRpcRequest',
() => {
throw new Error('Test error in lifecycle hook.');
},
);

const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: getPersistedSnapsState(),
},
}),
);

messenger.call('SnapController:init');
await sleep(10);

expect(consoleErrorSpy).toHaveBeenCalledWith(
`Error when calling \`onStart\` lifecycle hook for Snap "npm:@metamask/example-snap": Test error in lifecycle hook.`,
);

snapController.destroy();
});
});

describe('SnapController:get', () => {
it('gets a snap', () => {
const messenger = getSnapControllerMessenger();
Expand Down
46 changes: 46 additions & 0 deletions packages/snaps-controllers/src/snaps/SnapController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ type PendingApproval = {

// Controller Messenger Actions

/**
* Initialise the SnapController. This should be called after all controllers
* are created.
*/
export type SnapControllerInitAction = {
type: `${typeof controllerName}:init`;
handler: SnapController['init'];
};

/**
* Gets the specified Snap from state.
*/
Expand Down Expand Up @@ -470,6 +479,7 @@ export type SnapControllerGetStateAction = ControllerGetStateAction<
>;

export type SnapControllerActions =
| SnapControllerInitAction
| ClearSnapState
| GetSnap
| GetSnapState
Expand Down Expand Up @@ -1160,6 +1170,11 @@ export class SnapController extends BaseController<
* actions.
*/
#registerMessageHandlers(): void {
this.messagingSystem.registerActionHandler(
`${controllerName}:init`,
(...args) => this.init(...args),
);

this.messagingSystem.registerActionHandler(
`${controllerName}:clearSnapState`,
(...args) => this.clearSnapState(...args),
Expand Down Expand Up @@ -1266,6 +1281,37 @@ export class SnapController extends BaseController<
);
}

/**
* Initialise the SnapController.
*
* Currently this method calls the `onStart` lifecycle hook for all
* installed Snaps.
*/
init() {
const snaps = this.getRunnableSnaps();
for (const { id } of snaps) {
const hasLifecycleHooksEndowment = this.messagingSystem.call(
'PermissionController:hasPermission',
id,
SnapEndowments.LifecycleHooks,
);

if (!hasLifecycleHooksEndowment) {
continue;
}

this.#callLifecycleHook(METAMASK_ORIGIN, id, HandlerType.OnStart).catch(
(error) => {
logError(
`Error when calling \`onStart\` lifecycle hook for Snap "${id}": ${getErrorMessage(
error,
)}`,
);
},
);
}
}

#handlePreinstalledSnaps(preinstalledSnaps: PreinstalledSnap[]) {
for (const {
snapId,
Expand Down
4 changes: 2 additions & 2 deletions packages/snaps-execution-environments/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 90,
"branches": 90.04,
"functions": 94.69,
"lines": 90.41,
"lines": 90.42,
"statements": 89.62
}
Original file line number Diff line number Diff line change
Expand Up @@ -1896,7 +1896,11 @@ describe('BaseSnapExecutor', () => {
});

describe('lifecycle hooks', () => {
const LIFECYCLE_HOOKS = [HandlerType.OnInstall, HandlerType.OnUpdate];
const LIFECYCLE_HOOKS = [
HandlerType.OnInstall,
HandlerType.OnUpdate,
HandlerType.OnStart,
];

for (const handler of LIFECYCLE_HOOKS) {
it(`supports \`${handler}\` export`, async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function getHandlerArguments(

case HandlerType.OnInstall:
case HandlerType.OnUpdate:
case HandlerType.OnStart:
return { origin };

case HandlerType.OnHomePage:
Expand Down
2 changes: 2 additions & 0 deletions packages/snaps-jest/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export async function installSnap<
onKeyringRequest,
onInstall,
onUpdate,
onStart,
onNameLookup,
onProtocolRequest,
onClientRequest,
Expand All @@ -208,6 +209,7 @@ export async function installSnap<
onKeyringRequest,
onInstall,
onUpdate,
onStart,
onNameLookup,
onProtocolRequest,
onClientRequest,
Expand Down
Loading
Loading