Skip to content

Commit c09e25a

Browse files
committed
controllers, rpc-methods: Add various RPC methods and hooks
1 parent 3ea15d4 commit c09e25a

12 files changed

Lines changed: 362 additions & 30 deletions

File tree

packages/controllers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"build:prep": "mkdir -p dist && rm -rf dist/*",
1919
"lint": "eslint . --ext ts,js,json",
2020
"lint:fix": "yarn lint --fix",
21-
"prepare": "yarn build"
21+
"prepare": "yarn lint:fix && yarn build"
2222
},
2323
"devDependencies": {
2424
"@metamask/eslint-config": "^5.0.0",

packages/controllers/src/plugins/PluginController.ts

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { ObservableStore } from '@metamask/obs-store';
22
import EventEmitter from '@metamask/safe-event-emitter';
3-
import { serializeError } from 'eth-rpc-errors';
3+
import { ethErrors, serializeError } from 'eth-rpc-errors';
44
import { IOcapLdCapability } from 'rpc-cap/dist/src/@types/ocap-ld';
55
import { IRequestedPermissions } from 'rpc-cap/dist/src/@types';
6-
76
import { WorkerController, SetupWorkerConnection } from '../workers/WorkerController';
87
import { CommandResponse } from '../workers/CommandEngine';
98
import { INLINE_PLUGINS } from './inlinePlugins';
109

1110
export const PLUGIN_PREFIX = 'wallet_plugin_';
11+
export const PLUGIN_PREFIX_REGEX = new RegExp(`^${PLUGIN_PREFIX}`, 'u');
1212

1313
const SERIALIZABLE_PLUGIN_PROPERTIES = new Set([
1414
'initialPermissions',
@@ -30,10 +30,17 @@ export interface Plugin extends SerializablePlugin {
3030
// The plugin is the callee
3131
export type PluginRpcHook = (origin: string, request: Record<string, unknown>) => Promise<CommandResponse>;
3232

33+
export type ProcessPluginReturnType = SerializablePlugin | { error: ReturnType<typeof serializeError> };
34+
export interface InstalledPlugins {
35+
[pluginName: string]: ProcessPluginReturnType;
36+
}
37+
3338
// Types that probably should be defined elsewhere in prod
3439
type RemoveAllPermissionsFunction = (pluginIds: string[]) => void;
3540
type CloseAllConnectionsFunction = (domain: string) => void;
3641
type RequestPermissionsFunction = (domain: string, requestedPermissions: IRequestedPermissions) => IOcapLdCapability[];
42+
type HasPermissionFunction = (domain: string, permissionName: string) => boolean;
43+
type GetPermissionsFunction = (domain: string) => IOcapLdCapability[];
3744

3845
interface StoredPlugins {
3946
[pluginId: string]: Plugin;
@@ -56,6 +63,8 @@ interface PluginControllerArgs {
5663
setupWorkerPluginProvider: SetupWorkerConnection;
5764
closeAllConnections: CloseAllConnectionsFunction;
5865
requestPermissions: RequestPermissionsFunction;
66+
getPermissions: GetPermissionsFunction;
67+
hasPermission: HasPermissionFunction;
5968
workerUrl: URL;
6069
}
6170

@@ -84,6 +93,10 @@ export class PluginController extends EventEmitter {
8493

8594
private _requestPermissions: RequestPermissionsFunction;
8695

96+
private _getPermissions: GetPermissionsFunction;
97+
98+
private _hasPermission: HasPermissionFunction;
99+
87100
private _pluginsBeingAdded: Map<string, Promise<Plugin>>;
88101

89102
constructor({
@@ -92,19 +105,22 @@ export class PluginController extends EventEmitter {
92105
removeAllPermissionsFor,
93106
closeAllConnections,
94107
requestPermissions,
108+
getPermissions,
109+
hasPermission,
95110
workerUrl,
96111
}: PluginControllerArgs) {
97-
98112
super();
99113
const _initState: PluginControllerState = {
100114
plugins: {},
101115
pluginStates: {},
102116
...initState,
103117
};
118+
104119
this.store = new ObservableStore({
105120
plugins: {},
106121
pluginStates: {},
107122
});
123+
108124
this.memStore = new ObservableStore({
109125
inlinePluginIsRunning: false,
110126
plugins: {},
@@ -120,6 +136,8 @@ export class PluginController extends EventEmitter {
120136
this._removeAllPermissionsFor = removeAllPermissionsFor;
121137
this._closeAllConnections = closeAllConnections;
122138
this._requestPermissions = requestPermissions;
139+
this._getPermissions = getPermissions;
140+
this._hasPermission = hasPermission;
123141

124142
this._pluginRpcHooks = new Map();
125143
this._pluginsBeingAdded = new Map();
@@ -195,16 +213,16 @@ export class PluginController extends EventEmitter {
195213
* @param pluginName - The name of the plugin to get.
196214
*/
197215
getSerializable(pluginName: string): SerializablePlugin | null {
198-
199216
const plugin = this.get(pluginName);
200217

201218
return plugin
202-
? Object.keys(plugin).reduce((acc, key) => {
219+
// The cast to "any" of the accumulator object is due to a TypeScript bug
220+
? Object.keys(plugin).reduce((serialized, key) => {
203221
if (SERIALIZABLE_PLUGIN_PROPERTIES.has(key as keyof Plugin)) {
204-
acc[key] = plugin[key as keyof SerializablePlugin];
222+
serialized[key] = plugin[key as keyof SerializablePlugin];
205223
}
206224

207-
return acc;
225+
return serialized;
208226
}, {} as any) as SerializablePlugin
209227
: null;
210228
}
@@ -221,7 +239,6 @@ export class PluginController extends EventEmitter {
221239
newPluginState: unknown,
222240
): Promise<void> {
223241
const state = this.store.getState();
224-
225242
const newPluginStates = { ...state.pluginStates, [pluginName]: newPluginState };
226243

227244
this.updateState({
@@ -274,7 +291,6 @@ export class PluginController extends EventEmitter {
274291
* @param {Array<string>} pluginName - The name of the plugins.
275292
*/
276293
removePlugins(pluginNames: string[]): void {
277-
278294
if (!Array.isArray(pluginNames)) {
279295
throw new Error('Expected Array of plugin names.');
280296
}
@@ -301,17 +317,57 @@ export class PluginController extends EventEmitter {
301317
});
302318
}
303319

320+
getPermittedPlugins(origin: string): InstalledPlugins {
321+
return this._getPermissions(origin).reduce(
322+
(permittedPlugins, perm) => {
323+
if (perm.parentCapability.startsWith(PLUGIN_PREFIX)) {
324+
325+
const pluginName = perm.parentCapability.replace(PLUGIN_PREFIX_REGEX, '');
326+
const plugin = this.getSerializable(pluginName);
327+
328+
permittedPlugins[pluginName] = plugin || {
329+
error: serializeError(new Error('Plugin permitted but not installed.')),
330+
};
331+
}
332+
return permittedPlugins;
333+
},
334+
{} as InstalledPlugins,
335+
);
336+
}
337+
338+
async installPlugins(origin: string, requestedPlugins: IRequestedPermissions): Promise<InstalledPlugins> {
339+
const result: InstalledPlugins = {};
340+
341+
// use a for-loop so that we can return an object and await the resolution
342+
// of each call to processRequestedPlugin
343+
await Promise.all(Object.keys(requestedPlugins).map(async (pluginName) => {
344+
const permissionName = PLUGIN_PREFIX + pluginName;
345+
346+
if (this._hasPermission(origin, permissionName)) {
347+
// attempt to install and run the plugin, storing any errors that
348+
// occur during the process
349+
result[pluginName] = {
350+
...(await this.processRequestedPlugin(pluginName)),
351+
};
352+
} else {
353+
// only allow the installation of permitted plugins
354+
result[pluginName] = {
355+
error: ethErrors.provider.unauthorized(
356+
`Not authorized to install plugin '${pluginName}'. Request the permission for the plugin before attempting to install it.`,
357+
),
358+
};
359+
}
360+
}));
361+
return result;
362+
}
363+
304364
/**
305365
* Adds, authorizes, and runs the given plugin with a plugin provider.
306366
* Results from this method should be efficiently serializable.
307367
*
308368
* @param - pluginName - The name of the plugin.
309369
*/
310-
async processRequestedPlugin(pluginName: string): Promise<
311-
SerializablePlugin |
312-
{ error: ReturnType<typeof serializeError> }
313-
> {
314-
370+
async processRequestedPlugin(pluginName: string): Promise<ProcessPluginReturnType> {
315371
// if the plugin is already installed and active, just return it
316372
const plugin = this.get(pluginName);
317373
if (plugin?.isActive) {
@@ -326,7 +382,6 @@ export class PluginController extends EventEmitter {
326382
await this._startPluginInWorker(pluginName, sourceCode);
327383

328384
return this.getSerializable(pluginName) as SerializablePlugin;
329-
330385
} catch (err) {
331386
console.warn(`Error when adding plugin:`, err);
332387
return { error: serializeError(err) };
@@ -358,7 +413,6 @@ export class PluginController extends EventEmitter {
358413
* @param [sourceUrl] - The URL of the source code.
359414
*/
360415
async _add(pluginName: string, sourceUrl?: string): Promise<Plugin> {
361-
362416
const _sourceUrl = sourceUrl || pluginName;
363417

364418
if (!pluginName || typeof pluginName !== 'string') {
@@ -511,9 +565,3 @@ export class PluginController extends EventEmitter {
511565
});
512566
}
513567
}
514-
515-
// const createGetDomainMetadataFunction = (pluginName) => {
516-
// return async () => {
517-
// return { name: pluginName }
518-
// }
519-
// }

packages/post-message-stream/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"build:prep": "mkdir -p dist && rm -rf dist/*",
1313
"lint": "eslint . --ext ts,js,json",
1414
"lint:fix": "yarn lint --fix",
15-
"prepare": "yarn build"
15+
"prepare": "yarn lint:fix && yarn build"
1616
},
1717
"author": "",
1818
"dependencies": {

packages/rpc-methods/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
"build": "yarn build:prep && yarn build:ts",
1717
"build:ts": "tsc --project .",
1818
"build:prep": "mkdir -p dist && rm -rf dist/*",
19-
"prepare": "yarn build"
19+
"prepare": "yarn lint:fix && yarn build"
2020
},
2121
"dependencies": {
22+
"@mm-snap/controllers": "^0.0.1",
2223
"eth-rpc-errors": "^4.0.2"
2324
},
2425
"devDependencies": {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { ethErrors } from 'eth-rpc-errors';
2+
import { PLUGIN_PREFIX, InstalledPlugins } from '@mm-snap/controllers';
3+
import { IRequestedPermissions } from 'rpc-cap/dist/src/@types';
4+
import { isPlainObject } from '../../utils';
5+
6+
export type InstallPluginsResult = InstalledPlugins;
7+
8+
export type InstallPluginsHook = (requestedPlugins: IRequestedPermissions) => Promise<InstallPluginsResult>;
9+
10+
// preprocess requested permissions to support 'wallet_plugin' syntactic sugar
11+
export function preprocessRequestPermissions(requestedPermissions: IRequestedPermissions): IRequestedPermissions {
12+
if (!isPlainObject(requestedPermissions)) {
13+
throw ethErrors.rpc.invalidRequest({ data: { requestedPermissions } });
14+
}
15+
16+
// passthrough if 'wallet_plugin' is not requested
17+
if (!requestedPermissions.wallet_plugin) {
18+
return requestedPermissions;
19+
}
20+
21+
// rewrite permissions request parameter by destructuring plugins into
22+
// proper permissions prefixed with 'wallet_plugin_'
23+
return Object.keys(requestedPermissions).reduce((newRequestedPermissions, permName) => {
24+
if (permName === 'wallet_plugin') {
25+
if (!isPlainObject(requestedPermissions[permName])) {
26+
throw ethErrors.rpc.invalidParams({
27+
message: `Invalid params to 'wallet_requestPermissions'`,
28+
data: { requestedPermissions },
29+
});
30+
}
31+
32+
const requestedPlugins = requestedPermissions[permName] as IRequestedPermissions;
33+
34+
// destructure 'wallet_plugin' object
35+
Object.keys(requestedPlugins).forEach((pluginName) => {
36+
37+
const pluginKey = PLUGIN_PREFIX + pluginName;
38+
39+
// disallow requesting a plugin X under 'wallet_plugins' and
40+
// directly as 'wallet_plugin_X'
41+
if (requestedPermissions[pluginKey]) {
42+
throw ethErrors.rpc.invalidParams({
43+
message: `Plugin '${pluginName}' requested both as direct permission and under 'wallet_plugin'. We recommend using 'wallet_plugin' only.`,
44+
data: { requestedPermissions },
45+
});
46+
}
47+
48+
newRequestedPermissions[pluginKey] = requestedPlugins[pluginName];
49+
});
50+
} else {
51+
// otherwise, leave things as we found them
52+
newRequestedPermissions[permName] = requestedPermissions[permName];
53+
}
54+
55+
return newRequestedPermissions;
56+
}, {} as IRequestedPermissions);
57+
}
58+
59+
/**
60+
* Typechecks the requested plugins and passes them to the permissions
61+
* controller for installation.
62+
*/
63+
export async function handleInstallPlugins(
64+
requestedPlugins: IRequestedPermissions,
65+
installPlugins: InstallPluginsHook,
66+
): Promise<InstallPluginsResult> {
67+
if (!isPlainObject(requestedPlugins)) {
68+
throw ethErrors.rpc.invalidParams({
69+
message: `Invalid plugin installation params.`,
70+
data: { requestedPlugins },
71+
});
72+
} else if (Object.keys(requestedPlugins).length === 0) {
73+
throw ethErrors.rpc.invalidParams({
74+
message: `Must specify at least one plugin to install.`,
75+
data: { requestedPlugins },
76+
});
77+
}
78+
79+
// installPlugins is bound to the origin
80+
return await installPlugins(requestedPlugins);
81+
}

0 commit comments

Comments
 (0)