Skip to content

Commit

Permalink
feat!: use Messenger instead of SnapsController (#152)
Browse files Browse the repository at this point in the history
Introducing the concept of `Messenger` (messaging system that we use in
our `core` controllers) to break the runtime dependency we have with the
`SnapController`.

We will also use this new `Messenger` later when the Snap keyring will
re-forward some Snap account events to some other controllers.

This is **BREAKING** because of the removal of the
`KeyringSnapControllerClient` and because the `SnapKeyring`'s
constructor now requires a `Messenger` object instead of the
`SnapController`.
  • Loading branch information
ccharly authored Jan 21, 2025
1 parent e0353bd commit 2bdc87a
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 319 deletions.
14 changes: 5 additions & 9 deletions packages/keyring-internal-snap-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,18 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@metamask/base-controller": "^7.1.1",
"@metamask/keyring-api": "workspace:^",
"@metamask/keyring-snap-client": "workspace:^",
"@metamask/keyring-utils": "workspace:^",
"@metamask/snaps-controllers": "^9.10.0",
"@metamask/snaps-sdk": "^6.7.0",
"@metamask/snaps-utils": "^8.3.0",
"webextension-polyfill": "^0.12.0"
"@metamask/keyring-utils": "workspace:^"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^3.2.1",
"@lavamoat/preinstall-always-fail": "^2.1.0",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/providers": "^18.3.1",
"@metamask/snaps-controllers": "^9.10.0",
"@metamask/snaps-sdk": "^6.7.0",
"@metamask/snaps-utils": "^8.3.0",
"@metamask/utils": "^11.0.1",
"@ts-bridge/cli": "^0.6.1",
"@types/jest": "^29.5.12",
Expand All @@ -73,9 +72,6 @@
"typedoc": "^0.25.13",
"typescript": "~5.6.3"
},
"peerDependencies": {
"@metamask/providers": "^18.3.1"
},
"engines": {
"node": "^18.18 || >=20"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { KeyringAccount } from '@metamask/keyring-api';
import type { SnapId } from '@metamask/snaps-sdk';

import {
KeyringInternalSnapClient,
type KeyringInternalSnapClientMessenger,
} from './KeyringInternalSnapClient';

describe('KeyringInternalSnapClient', () => {
const snapId = 'local:localhost:3000' as SnapId;

const accountsList: KeyringAccount[] = [
{
id: '13f94041-6ae6-451f-a0fe-afdd2fda18a7',
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
options: {},
methods: [],
scopes: ['eip155'],
type: 'eip155:eoa',
},
];

const messenger = {
call: jest.fn(),
};

describe('listAccounts', () => {
const request = {
snapId,
origin: 'metamask',
handler: 'onKeyringRequest',
request: {
id: expect.any(String),
jsonrpc: '2.0',
method: 'keyring_listAccounts',
},
};

it('calls the listAccounts method and return the result', async () => {
const client = new KeyringInternalSnapClient({
messenger: messenger as unknown as KeyringInternalSnapClientMessenger,
snapId,
});

messenger.call.mockResolvedValue(accountsList);
const accounts = await client.listAccounts();
expect(messenger.call).toHaveBeenCalledWith(
'SnapController:handleRequest',
request,
);
expect(accounts).toStrictEqual(accountsList);
});

it('calls the listAccounts method and return the result (withSnapId)', async () => {
const client = new KeyringInternalSnapClient({
messenger: messenger as unknown as KeyringInternalSnapClientMessenger,
});

messenger.call.mockResolvedValue(accountsList);
const accounts = await client.withSnapId(snapId).listAccounts();
expect(messenger.call).toHaveBeenCalledWith(
'SnapController:handleRequest',
request,
);
expect(accounts).toStrictEqual(accountsList);
});

it('calls the default snapId value ("undefined")', async () => {
const client = new KeyringInternalSnapClient({
messenger: messenger as unknown as KeyringInternalSnapClientMessenger,
});

messenger.call.mockResolvedValue(accountsList);
await client.listAccounts();
expect(messenger.call).toHaveBeenCalledWith(
'SnapController:handleRequest',
{
...request,
snapId: 'undefined',
},
);
});
});
});
124 changes: 124 additions & 0 deletions packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { RestrictedControllerMessenger } from '@metamask/base-controller';
import { KeyringClient, type Sender } from '@metamask/keyring-snap-client';
import type { JsonRpcRequest } from '@metamask/keyring-utils';
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import type { HandlerType } from '@metamask/snaps-utils';
import type { Json } from '@metamask/utils';

// We only need to dispatch Snap request to the Snaps controller for now.
type AllowedActions = HandleSnapRequest;

/**
* A restricted-`Messenger` used by `KeyringInternalSnapClient` to dispatch
* internal Snap requests.
*/
export type KeyringInternalSnapClientMessenger = RestrictedControllerMessenger<
'KeyringInternalSnapClient',
AllowedActions,
never,
AllowedActions['type'],
never
>;

/**
* Implementation of the `Sender` interface that can be used to send requests
* to a Snap through a `Messenger`.
*/
class SnapControllerMessengerSender implements Sender {
readonly #snapId: SnapId;

readonly #origin: string;

readonly #messenger: KeyringInternalSnapClientMessenger;

readonly #handler: HandlerType;

/**
* Create a new instance of `SnapControllerSender`.
*
* @param messenger - The `Messenger` instance used when dispatching controllers actions.
* @param snapId - The ID of the Snap to use.
* @param origin - The sender's origin.
* @param handler - The handler type.
*/
constructor(
messenger: KeyringInternalSnapClientMessenger,
snapId: SnapId,
origin: string,
handler: HandlerType,
) {
this.#messenger = messenger;
this.#snapId = snapId;
this.#origin = origin;
this.#handler = handler;
}

/**
* Send a request to the Snap and return the response.
*
* @param request - JSON-RPC request to send to the Snap.
* @returns A promise that resolves to the response of the request.
*/
async send(request: JsonRpcRequest): Promise<Json> {
return this.#messenger.call('SnapController:handleRequest', {
snapId: this.#snapId,
origin: this.#origin,
handler: this.#handler,
request,
}) as Promise<Json>;
}
}

/**
* A `KeyringClient` that allows the communication with a Snap through a
* `Messenger`.
*/
export class KeyringInternalSnapClient extends KeyringClient {
readonly #messenger: KeyringInternalSnapClientMessenger;

/**
* Create a new instance of `KeyringInternalSnapClient`.
*
* The `handlerType` argument has a hard-coded default `string` value instead
* of a `HandlerType` value to prevent the `@metamask/snaps-utils` module
* from being required at runtime.
*
* @param args - Constructor arguments.
* @param args.messenger - The `KeyringInternalSnapClientMessenger` instance to use.
* @param args.snapId - The ID of the Snap to use (default: `'undefined'`).
* @param args.origin - The sender's origin (default: `'metamask'`).
* @param args.handler - The handler type (default: `'onKeyringRequest'`).
*/
constructor({
messenger,
snapId = 'undefined' as SnapId,
origin = 'metamask',
handler = 'onKeyringRequest' as HandlerType,
}: {
messenger: KeyringInternalSnapClientMessenger;
snapId?: SnapId;
origin?: string;
handler?: HandlerType;
}) {
super(
new SnapControllerMessengerSender(messenger, snapId, origin, handler),
);
this.#messenger = messenger;
}

/**
* Create a new instance of `KeyringInternalSnapClient` with the specified
* `snapId`.
*
* @param snapId - The ID of the Snap to use in the new instance.
* @returns A new instance of `KeyringInternalSnapClient` with the
* specified Snap ID.
*/
withSnapId(snapId: SnapId): KeyringInternalSnapClient {
return new KeyringInternalSnapClient({
messenger: this.#messenger,
snapId,
});
}
}

This file was deleted.

Loading

0 comments on commit 2bdc87a

Please sign in to comment.