Skip to content

Commit

Permalink
Initial implementation of new global APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
FrederikBolding committed Nov 9, 2022
1 parent 2407423 commit 6e4951b
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -459,15 +459,15 @@ describe('BaseSnapExecutor', () => {

it('reports when outbound requests are made', async () => {
const CODE = `
module.exports.onRpcRequest = () => wallet.request({ method: 'eth_blockNumber', params: [] });
module.exports.onRpcRequest = () => ethereum.request({ method: 'eth_blockNumber', params: [] });
`;
const executor = new TestSnapExecutor();

await executor.writeCommand({
jsonrpc: '2.0',
id: 1,
method: 'executeSnap',
params: [FAKE_SNAP_NAME, CODE, []],
params: [FAKE_SNAP_NAME, CODE, ['ethereum']],
});

expect(await executor.readCommand()).toStrictEqual({
Expand Down Expand Up @@ -1193,14 +1193,19 @@ describe('BaseSnapExecutor', () => {
jest.useRealTimers();
const consoleLogSpy = jest.spyOn(console, 'log');
const consoleWarnSpy = jest.spyOn(console, 'warn');
const TIMER_ENDOWMENTS = ['setTimeout', 'clearTimeout', 'console'];
const TIMER_ENDOWMENTS = [
'setTimeout',
'clearTimeout',
'console',
'ethereum',
];
const CODE = `
let promise;
module.exports.onRpcRequest = async ({request}) => {
switch (request.method) {
case 'first':
promise = wallet.request({ method: 'eth_blockNumber', params: [] })
promise = ethereum.request({ method: 'eth_blockNumber', params: [] })
.then(() => console.log('Jailbreak'));
return 'FIRST OK';
case 'second':
Expand Down Expand Up @@ -1320,14 +1325,19 @@ describe('BaseSnapExecutor', () => {
jest.useRealTimers();
const consoleLogSpy = jest.spyOn(console, 'log');
const consoleWarnSpy = jest.spyOn(console, 'warn');
const TIMER_ENDOWMENTS = ['setTimeout', 'clearTimeout', 'console'];
const TIMER_ENDOWMENTS = [
'setTimeout',
'clearTimeout',
'console',
'ethereum',
];
const CODE = `
let promise;
module.exports.onRpcRequest = async ({request}) => {
switch (request.method) {
case 'first':
promise = wallet.request({ method: 'eth_blockNumber', params: [] })
promise = ethereum.request({ method: 'eth_blockNumber', params: [] })
.catch(() => console.log('Jailbreak'));
return 'FIRST OK';
case 'second':
Expand Down Expand Up @@ -1450,15 +1460,15 @@ describe('BaseSnapExecutor', () => {
// This will ensure that the reject(reason); is called from inside the proxy method
// when the original promise throws an error (i.e. RPC request fails).
const CODE = `
module.exports.onRpcRequest = () => wallet.request({ method: 'eth_blockNumber', params: [] });
module.exports.onRpcRequest = () => ethereum.request({ method: 'eth_blockNumber', params: [] });
`;
const executor = new TestSnapExecutor();

await executor.writeCommand({
jsonrpc: '2.0',
id: 1,
method: 'executeSnap',
params: [FAKE_SNAP_NAME, CODE, []],
params: [FAKE_SNAP_NAME, CODE, ['ethereum']],
});

expect(await executor.readCommand()).toStrictEqual({
Expand Down
48 changes: 40 additions & 8 deletions packages/execution-environments/src/common/BaseSnapExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference, spaced-comment
/// <reference path="../../../../node_modules/ses/index.d.ts" />
import { Duplex } from 'stream';
import { MetaMaskInpageProvider } from '@metamask/providers';
import { SnapProvider, SnapExports } from '@metamask/snap-types';
import { StreamProvider } from '@metamask/providers';
import { SnapExports, SnapAPI } from '@metamask/snap-types';
import { errorCodes, ethErrors, serializeError } from 'eth-rpc-errors';
import {
isObject,
Expand All @@ -22,6 +22,7 @@ import {
SnapExportsParameters,
} from '@metamask/snap-utils';
import { validate } from 'superstruct';
import { RequestArguments } from '@metamask/providers/dist/BaseProvider';
import EEOpenRPCDocument from '../openrpc.json';
import { createEndowments } from './endowments';
import {
Expand Down Expand Up @@ -274,13 +275,21 @@ export class BaseSnapExecutor {
});
};

const wallet = this.createSnapProvider();
const provider = new StreamProvider(this.rpcStream, {
jsonRpcStreamName: 'metamask-provider',
});

await provider.initialize();

const snap = this.createSnapAPI(provider);
const ethereum = this.createEIP1193Provider(provider);
// We specifically use any type because the Snap can modify the object any way they want
const snapModule: any = { exports: {} };

try {
const { endowments, teardown: endowmentTeardown } = createEndowments(
wallet,
snap,
ethereum,
_endowments,
);

Expand Down Expand Up @@ -348,15 +357,38 @@ export class BaseSnapExecutor {
/**
* Instantiates a snap provider object (i.e. `globalThis.wallet`).
*
* @param provider - A StreamProvider connected to MetaMask.
* @returns The snap provider object.
*/
private createSnapProvider(): SnapProvider {
const provider = new MetaMaskInpageProvider(this.rpcStream, {
shouldSendMetadata: false,
});
private createSnapAPI(provider: StreamProvider): SnapAPI {
const originalRequest = provider.request;

const request = async (args: RequestArguments) => {
assert(
args.method.startsWith('wallet_') || args.method.startsWith('snap_'),
);
this.notify({ method: 'OutboundRequest' });
try {
return await withTeardown(originalRequest(args), this as any);
} finally {
this.notify({ method: 'OutboundResponse' });
}
};

return { request };
}

/**
* Instantiates an eip1193 provider object (i.e. `globalThis.ethereum`).
*
* @param provider - A StreamProvider connected to MetaMask.
* @returns The eip1193 provider object.
*/
private createEIP1193Provider(provider: StreamProvider): StreamProvider {
const originalRequest = provider.request;

provider.request = async (args) => {
assert(!args.method.startsWith('snap_'));
this.notify({ method: 'OutboundRequest' });
try {
return await withTeardown(originalRequest(args), this as any);
Expand Down
131 changes: 69 additions & 62 deletions packages/execution-environments/src/common/endowments/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import fetchMock from 'jest-fetch-mock';
import { createEndowments, isConstructor } from '.';

const mockSnapAPI = { foo: Symbol('bar') };
const mockEthereum = { foo: Symbol('bar') };

describe('Endowment utils', () => {
describe('createEndowments', () => {
beforeAll(() => {
Expand All @@ -14,27 +17,31 @@ describe('Endowment utils', () => {
});

it('handles no endowments', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments } = createEndowments(mockWallet as any);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
);

expect(createEndowments(mockWallet as any, [])).toStrictEqual({
expect(
createEndowments(mockSnapAPI as any, mockEthereum as any),
).toStrictEqual({
endowments: {
wallet: mockWallet,
snap: mockSnapAPI,
},
teardown: expect.any(Function),
});
expect(endowments.wallet).toBe(mockWallet);
expect(endowments.snap).toBe(mockSnapAPI);
});

it('handles unattenuated endowments', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments } = createEndowments(mockWallet as any, [
'Math',
'Date',
]);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['Math', 'Date'],
);

expect(endowments).toStrictEqual({
wallet: mockWallet,
snap: mockSnapAPI,
Math,
Date,
});
Expand All @@ -43,75 +50,79 @@ describe('Endowment utils', () => {
});

it('handles special cases where endowment is a function but not a constructor', () => {
const mockWallet = { foo: Symbol('bar') };
const mockEndowment = () => {
return {};
};
Object.assign(globalThis, { mockEndowment });
const { endowments } = createEndowments(mockWallet as any, [
'Math',
'Date',
'mockEndowment',
]);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['Math', 'Date', 'mockEndowment'],
);
expect(endowments.Math).toBe(Math);
expect(endowments.Date).toBe(Date);
expect(endowments.mockEndowment).toBeDefined();
});

it('handles factory endowments', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments } = createEndowments(mockWallet as any, [
'setTimeout',
]);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['setTimeout'],
);

expect(endowments).toStrictEqual({
wallet: mockWallet,
snap: mockSnapAPI,
setTimeout: expect.any(Function),
});
expect(endowments.setTimeout).not.toBe(setTimeout);
});

it('handles some endowments from the same factory', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments } = createEndowments(mockWallet as any, [
'setTimeout',
]);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['setTimeout'],
);

expect(endowments).toMatchObject({
wallet: mockWallet,
snap: mockSnapAPI,
setTimeout: expect.any(Function),
});
expect(endowments.setTimeout).not.toBe(setTimeout);
});

it('handles all endowments from the same factory', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments } = createEndowments(mockWallet as any, [
'setTimeout',
'clearTimeout',
]);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['setTimeout', 'clearTimeout'],
);

expect(endowments).toMatchObject({
wallet: mockWallet,
snap: mockSnapAPI,
setTimeout: expect.any(Function),
clearTimeout: expect.any(Function),
});
expect(endowments.clearTimeout).not.toBe(clearTimeout);
});

it('handles multiple endowments, factory and non-factory', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments } = createEndowments(mockWallet as any, [
'Buffer',
'console',
'Math',
'setTimeout',
'clearTimeout',
'WebAssembly',
]);
const { endowments } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
[
'Buffer',
'console',
'Math',
'setTimeout',
'clearTimeout',
'WebAssembly',
],
);

expect(endowments).toMatchObject({
wallet: mockWallet,
snap: mockSnapAPI,
Buffer,
console,
Math,
Expand All @@ -120,7 +131,7 @@ describe('Endowment utils', () => {
WebAssembly,
});

expect(endowments.wallet).toBe(mockWallet);
expect(endowments.snap).toBe(mockSnapAPI);
expect(endowments.Buffer).toBe(Buffer);
expect(endowments.console).toBe(console);
expect(endowments.Math).toBe(Math);
Expand All @@ -131,20 +142,17 @@ describe('Endowment utils', () => {
});

it('throws for unknown endowments', () => {
const mockWallet = { foo: Symbol('bar') };
expect(() => createEndowments(mockWallet as any, ['foo'])).toThrow(
'Unknown endowment: "foo"',
);
expect(() =>
createEndowments(mockSnapAPI as any, mockEthereum as any, ['foo']),
).toThrow('Unknown endowment: "foo"');
});

it('teardown calls all teardown functions', () => {
const mockWallet = { foo: Symbol('bar') };
const { endowments, teardown } = createEndowments(mockWallet as any, [
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
]);
const { endowments, teardown } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
);

const clearTimeoutSpy = jest.spyOn(globalThis, 'clearTimeout');
const clearIntervalSpy = jest.spyOn(globalThis, 'clearInterval');
Expand All @@ -168,7 +176,7 @@ describe('Endowment utils', () => {
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
expect(endowments).toMatchObject({
wallet: mockWallet,
snap: mockSnapAPI,
setTimeout: expect.any(Function),
clearTimeout: expect.any(Function),
setInterval: expect.any(Function),
Expand All @@ -177,12 +185,11 @@ describe('Endowment utils', () => {
});

it('teardown can be called multiple times', async () => {
const { endowments, teardown } = createEndowments({} as any, [
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
]);
const { endowments, teardown } = createEndowments(
mockSnapAPI as any,
mockEthereum as any,
['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
);

const { setInterval, setTimeout } = endowments as {
setInterval: typeof globalThis.setInterval;
Expand Down

0 comments on commit 6e4951b

Please sign in to comment.