Skip to content

Commit 0c7beed

Browse files
feat: Add native execution environment
1 parent a20241d commit 0c7beed

File tree

7 files changed

+114
-0
lines changed

7 files changed

+114
-0
lines changed

packages/snaps-controllers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"lodash": "^4.17.21",
139139
"prettier": "^3.3.3",
140140
"rimraf": "^4.1.2",
141+
"ses": "^1.14.0",
141142
"ts-node": "^10.9.1",
142143
"tsx": "^4.20.3",
143144
"typescript": "~5.3.3",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// eslint-disable-next-line import-x/no-unassigned-import
2+
import 'ses';
3+
import { HandlerType } from '@metamask/snaps-utils';
4+
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
5+
import { describe, expect, it } from 'vitest';
6+
7+
import { NativeExecutionService } from './NativeExecutionService';
8+
import { METAMASK_ORIGIN } from '../../snaps';
9+
import { createService } from '../../test-utils/service';
10+
11+
lockdown({
12+
domainTaming: 'unsafe',
13+
errorTaming: 'unsafe',
14+
stackFiltering: 'verbose',
15+
overrideTaming: 'severe',
16+
errorTrapping: 'none',
17+
});
18+
19+
describe('NativeExecutionService', () => {
20+
it('works', async () => {
21+
const { service } = createService(NativeExecutionService);
22+
23+
await service.executeSnap({
24+
snapId: MOCK_SNAP_ID,
25+
sourceCode: `module.exports.onRpcRequest = () => { console.log('foo'); return 'bar'; }`,
26+
endowments: ['console'],
27+
});
28+
29+
const result = await service.handleRpcRequest(MOCK_SNAP_ID, {
30+
origin: METAMASK_ORIGIN,
31+
request: {
32+
method: 'foo',
33+
},
34+
handler: HandlerType.OnRpcRequest,
35+
});
36+
37+
expect(result).toBe('bar');
38+
});
39+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { BasePostMessageStream } from '@metamask/post-message-stream';
2+
import { NativeSnapExecutor } from '@metamask/snaps-execution-environments';
3+
import type { Json } from '@metamask/utils';
4+
import { Duplex } from 'readable-stream';
5+
6+
import {
7+
AbstractExecutionService,
8+
type TerminateJobArgs,
9+
} from '../AbstractExecutionService';
10+
11+
export class NativeExecutionService extends AbstractExecutionService<NativeSnapExecutor> {
12+
protected terminateJob(_job: TerminateJobArgs<NativeSnapExecutor>): void {
13+
// no-op
14+
}
15+
16+
protected async initEnvStream(
17+
_snapId: string,
18+
): Promise<{ worker: NativeSnapExecutor; stream: BasePostMessageStream }> {
19+
// TODO: Sanity check this.
20+
const workerStream = new Duplex({
21+
objectMode: true,
22+
read() {
23+
return undefined;
24+
},
25+
write(chunk: Json, encoding: BufferEncoding, callback) {
26+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
27+
stream.push(chunk, encoding);
28+
callback();
29+
},
30+
});
31+
const stream = new Duplex({
32+
objectMode: true,
33+
read() {
34+
return undefined;
35+
},
36+
write(chunk: Json, encoding: BufferEncoding, callback) {
37+
workerStream.push(chunk, encoding);
38+
callback();
39+
},
40+
}) as unknown as BasePostMessageStream;
41+
42+
// NOTE: Initializes a Snap executor that runs in the same JS thread as the execution service.
43+
// Does not provide process isolation.
44+
const worker = NativeSnapExecutor.initialize(workerStream);
45+
46+
return Promise.resolve({ worker, stream });
47+
}
48+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './proxy';
2+
export * from './native';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ObjectMultiplex from '@metamask/object-multiplex';
2+
import { logError, SNAP_STREAM_NAMES } from '@metamask/snaps-utils';
3+
import type { Duplex } from 'readable-stream';
4+
import { pipeline } from 'readable-stream';
5+
6+
import { BaseSnapExecutor } from '../common/BaseSnapExecutor';
7+
8+
export class NativeSnapExecutor extends BaseSnapExecutor {
9+
static initialize(stream: Duplex) {
10+
const mux = new ObjectMultiplex();
11+
pipeline(stream, mux, stream, (error) => {
12+
if (error) {
13+
logError(`Parent stream failure, closing worker.`, error);
14+
}
15+
self.close();
16+
});
17+
18+
const commandStream = mux.createStream(SNAP_STREAM_NAMES.COMMAND);
19+
const rpcStream = mux.createStream(SNAP_STREAM_NAMES.JSON_RPC);
20+
21+
return new NativeSnapExecutor(commandStream, rpcStream);
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './NativeSnapExecutor';

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4311,6 +4311,7 @@ __metadata:
43114311
readable-web-to-node-stream: "npm:^3.0.2"
43124312
rimraf: "npm:^4.1.2"
43134313
semver: "npm:^7.5.4"
4314+
ses: "npm:^1.14.0"
43144315
tar-stream: "npm:^3.1.7"
43154316
ts-node: "npm:^10.9.1"
43164317
tsx: "npm:^4.20.3"

0 commit comments

Comments
 (0)