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
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": 93.31,
"functions": 96.8,
"lines": 98.15,
"statements": 97.88
"functions": 97.05,
"lines": 98.2,
"statements": 97.93
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,19 @@ import type { WebViewInterface } from './WebViewMessageStream';
* Create a response message for the given request. This function assumes that
* the response is for the parent, and uses the command stream.
*
* @param message - The request message.
* @param request - The request to respond to.
* @param response - The response to send.
* @returns The response message.
*/
function getResponse(
message: Record<string, unknown>,
request: JsonRpcRequest,
response: Json,
) {
function getResponse(request: JsonRpcRequest, response: Json) {
return {
target: 'parent',
data: {
jobId: message.jobId,
frameUrl: message.frameUrl,
name: 'command',
data: {
name: 'command',
data: {
jsonrpc: '2.0',
id: request.id,
result: response,
},
jsonrpc: '2.0',
id: request.id,
result: response,
},
},
};
Expand All @@ -50,11 +41,13 @@ describe('WebViewExecutionService', () => {
};

const { service } = createService(WebViewExecutionService, {
getWebView: async () =>
Promise.resolve(mockedWebView as unknown as WebViewInterface),
createWebView: async (_id: string) =>
mockedWebView as unknown as WebViewInterface,
removeWebView: jest.fn(),
});

expect(service).toBeDefined();
await service.terminateAllSnaps();
});

it('can execute snaps', async () => {
Expand Down Expand Up @@ -89,30 +82,29 @@ describe('WebViewExecutionService', () => {
// Handle incoming requests.
if (
isPlainObject(data) &&
isPlainObject(data.data) &&
data.data.name === 'command' &&
isJsonRpcRequest(data.data.data)
data.name === 'command' &&
isJsonRpcRequest(data.data)
) {
const request = data.data.data;
const request = data.data;

// Respond "OK" to the `ping`, `executeSnap`, and `terminate` request.
if (
request.method === 'ping' ||
request.method === 'executeSnap' ||
request.method === 'terminate'
) {
sendMessage(getResponse(data, request, 'OK'));
sendMessage(getResponse(request, 'OK'));
}
}
});

const { service } = createService(WebViewExecutionService, {
getWebView: async () =>
Promise.resolve({
registerMessageListener,
unregisterMessageListener: jest.fn(),
injectJavaScript,
}),
createWebView: async (_id: string) => ({
registerMessageListener,
unregisterMessageListener: jest.fn(),
injectJavaScript,
}),
removeWebView: jest.fn(),
});

expect(
Expand All @@ -122,5 +114,7 @@ describe('WebViewExecutionService', () => {
endowments: [],
}),
).toBe('OK');

await service.terminateAllSnaps();
});
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
import type { ExecutionServiceArgs } from '../AbstractExecutionService';
import { ProxyExecutionService } from '../proxy/ProxyExecutionService';
import type { TerminateJobArgs } from '../AbstractExecutionService';
import {
AbstractExecutionService,
type ExecutionServiceArgs,
} from '../AbstractExecutionService';
import type { WebViewInterface } from './WebViewMessageStream';
import { WebViewMessageStream } from './WebViewMessageStream';

export type WebViewExecutionServiceArgs = ExecutionServiceArgs & {
getWebView: () => Promise<WebViewInterface>;
createWebView: (jobId: string) => Promise<WebViewInterface>;
removeWebView: (jobId: string) => void;
};

export class WebViewExecutionService extends ProxyExecutionService {
#getWebView;
export class WebViewExecutionService extends AbstractExecutionService<WebViewInterface> {
readonly #createWebView;

readonly #removeWebView;

constructor({
messenger,
setupSnapProvider,
getWebView,
createWebView,
removeWebView,
...args
}: WebViewExecutionServiceArgs) {
super({
...args,
messenger,
setupSnapProvider,
stream: new WebViewMessageStream({
name: 'parent',
target: 'child',
getWebView,
}),
});
this.#getWebView = getWebView;
this.#createWebView = createWebView;
this.#removeWebView = removeWebView;
}

/**
Expand All @@ -37,16 +40,18 @@ export class WebViewExecutionService extends ProxyExecutionService {
* @returns An object with the worker ID and stream.
*/
protected async initEnvStream(jobId: string) {
// Ensure that the WebView has been loaded before we proceed.
await this.#ensureWebViewLoaded();
const webView = await this.#createWebView(jobId);

const stream = new WebViewMessageStream({
name: 'parent',
target: 'child',
webView,
});

return super.initEnvStream(jobId);
return { worker: webView, stream };
}

/**
* Ensure that the WebView has been loaded by awaiting the getWebView promise.
*/
async #ensureWebViewLoaded() {
await this.#getWebView();
protected terminateJob(jobWrapper: TerminateJobArgs<WebViewInterface>): void {
this.#removeWebView(jobWrapper.id);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PostMessageEvent } from '@metamask/post-message-stream';
import { BasePostMessageStream } from '@metamask/post-message-stream';
import { isValidStreamMessage } from '@metamask/post-message-stream/dist/utils';
import { logError } from '@metamask/snaps-utils';
import { assert, stringToBytes } from '@metamask/utils';

export type WebViewInterface = {
Expand All @@ -13,7 +12,7 @@ export type WebViewInterface = {
export type WebViewStreamArgs = {
name: string;
target: string;
getWebView: () => Promise<WebViewInterface>;
webView: WebViewInterface;
};

/**
Expand All @@ -33,29 +32,21 @@ export class WebViewMessageStream extends BasePostMessageStream {
* @param args.name - The name of the stream. Used to differentiate between
* multiple streams sharing the same window object.
* @param args.target - The name of the stream to exchange messages with.
* @param args.getWebView - A asynchronous getter for the webview.
* @param args.webView - A reference to the WebView.
*/
constructor({ name, target, getWebView }: WebViewStreamArgs) {
constructor({ name, target, webView }: WebViewStreamArgs) {
super();

this.#name = name;
this.#target = target;

this._onMessage = this._onMessage.bind(this);

// This is a bit atypical from other post-message streams.
// We have to wait for the WebView to fully load before we can continue using the stream.
getWebView()
.then((webView) => {
this.#webView = webView;
// This method is already bound.
// eslint-disable-next-line @typescript-eslint/unbound-method
webView.registerMessageListener(this._onMessage);
this._handshake();
})
.catch((error) => {
logError(error);
});
this.#webView = webView;
// This method is already bound.
// eslint-disable-next-line @typescript-eslint/unbound-method
this.#webView.registerMessageListener(this._onMessage);
this._handshake();
}

protected _postMessage(data: unknown): void {
Expand Down
11 changes: 3 additions & 8 deletions packages/snaps-controllers/src/test-utils/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function parseInjectedJS(js: string) {
/**
* Takes no param and return mocks necessary for testing WebViewMessageStream.
*
* @returns The mockWebView, mockGetWebView, and mockStream.
* @returns The mockWebView, and mockStream.
*/
export function createWebViewObjects() {
const registerMessageListenerA = jest.fn();
Expand Down Expand Up @@ -45,26 +45,21 @@ export function createWebViewObjects() {
}),
};

const mockGetWebViewA = jest.fn().mockResolvedValue(mockWebViewA);
const mockGetWebViewB = jest.fn().mockResolvedValue(mockWebViewB);

const streamA = new WebViewMessageStream({
name: 'a',
target: 'b',
getWebView: mockGetWebViewA,
webView: mockWebViewA,
});

const streamB = new WebViewMessageStream({
name: 'b',
target: 'a',
getWebView: mockGetWebViewB,
webView: mockWebViewB,
});

return {
mockWebViewA,
mockWebViewB,
mockGetWebViewA,
mockGetWebViewB,
streamA,
streamB,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
{
"resources": {
"@metamask/json-rpc-engine": {
"packages": {
"@metamask/providers>@metamask/safe-event-emitter": true,
"@metamask/rpc-errors": true,
"@metamask/utils": true
}
},
"@metamask/object-multiplex": {
"globals": {
"console.warn": true
},
"packages": {
"@metamask/object-multiplex>once": true,
"readable-stream": true
}
},
"@metamask/object-multiplex>once": {
"packages": {
"@metamask/object-multiplex>once>wrappy": true
}
},
"@metamask/post-message-stream": {
"globals": {
"MessageEvent.prototype": true,
Expand All @@ -16,6 +37,39 @@
"readable-stream": true
}
},
"@metamask/providers": {
"globals": {
"console": true
},
"packages": {
"@metamask/json-rpc-engine": true,
"@metamask/providers>@metamask/json-rpc-middleware-stream": true,
"@metamask/providers>@metamask/safe-event-emitter": true,
"@metamask/providers>is-stream": true,
"@metamask/rpc-errors": true,
"eslint>fast-deep-equal": true,
"readable-stream": true
}
},
"@metamask/providers>@metamask/json-rpc-middleware-stream": {
"globals": {
"console.warn": true,
"setTimeout": true
},
"packages": {
"@metamask/providers>@metamask/safe-event-emitter": true,
"@metamask/utils": true,
"readable-stream": true
}
},
"@metamask/providers>@metamask/safe-event-emitter": {
"globals": {
"setTimeout": true
},
"packages": {
"browserify>events": true
}
},
"@metamask/rpc-errors": {
"packages": {
"@metamask/rpc-errors>fast-safe-stringify": true,
Expand Down
4 changes: 2 additions & 2 deletions packages/snaps-execution-environments/src/webview/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { executeLockdownEvents } from '../common/lockdown/lockdown-events';
import { executeLockdownMore } from '../common/lockdown/lockdown-more';
import { ProxySnapExecutor } from '../proxy/ProxySnapExecutor';
import { IFrameSnapExecutor } from '../iframe/IFrameSnapExecutor';
import { WebViewExecutorStream } from './WebViewExecutorStream';

// Lockdown is already applied in LavaMoat
Expand All @@ -13,4 +13,4 @@
targetWindow: window.ReactNativeWebView,
});

ProxySnapExecutor.initialize(parentStream);
IFrameSnapExecutor.initialize(parentStream);

Check warning on line 16 in packages/snaps-execution-environments/src/webview/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/snaps-execution-environments/src/webview/index.ts#L16

Added line #L16 was not covered by tests
Loading