@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
160 lines • 7.13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSnapPermissionsRequest = exports.hasRequestedSnaps = exports.requestSnapsHandler = void 0;
const rpc_errors_1 = require("@metamask/rpc-errors");
const snaps_utils_1 = require("@metamask/snaps-utils");
const utils_1 = require("@metamask/utils");
const async_mutex_1 = require("async-mutex");
const invokeSnap_1 = require("../restricted/invokeSnap.cjs");
/**
* Request permission for a dapp to communicate with the specified Snaps and
* attempt to install them if they're not already installed.
*
* If the Snap version range is specified, MetaMask attempts to install a
* version of the Snap that satisfies the range. If a compatible version of the
* Snap is already installed, the request succeeds. If an incompatible version
* is installed, MetaMask attempts to update the Snap to the latest version that
* satisfies the range. The request succeeds if the Snap is successfully
* installed.
*
* If the installation of any Snap fails, or the user rejects the installation
* or permission request, this method returns the error that caused the failure.
*
* @example
* ```ts
* const result = await snap.request({
* method: 'wallet_requestSnaps',
* params: {
* 'npm:@metamask/example-snap': {},
* 'npm:@metamask/another-snap': { version: '1.2.3' },
* },
* });
* ```
*/
exports.requestSnapsHandler = {
implementation: requestSnapsImplementation,
actionNames: [
'SnapController:installSnaps',
'PermissionController:requestPermissions',
'PermissionController:getPermissions',
],
};
/**
* Checks whether an origin has existing `wallet_snap` permission and
* whether or not it has the requested snapIds caveat.
*
* @param existingPermissions - The existing permissions for the origin.
* @param requestedSnaps - The requested snaps.
* @returns True if the existing permissions satisfy the requested snaps, otherwise false.
*/
function hasRequestedSnaps(existingPermissions, requestedSnaps) {
const snapIdCaveat = existingPermissions[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === snaps_utils_1.SnapCaveatType.SnapIds);
const permittedSnaps = snapIdCaveat?.value;
if ((0, utils_1.isObject)(permittedSnaps)) {
return Object.keys(requestedSnaps).every((requestedSnap) => (0, utils_1.hasProperty)(permittedSnaps, requestedSnap));
}
return false;
}
exports.hasRequestedSnaps = hasRequestedSnaps;
/**
* Constructs a valid permission request with merged caveats based on existing permissions
* and the requested snaps.
*
* @param existingPermissions - The existing permissions for the origin.
* @param requestedPermissions - The permission request passed into `requestPermissions`.
* @returns `requestedPermissions`.
*/
function getSnapPermissionsRequest(existingPermissions, requestedPermissions) {
(0, snaps_utils_1.verifyRequestedSnapPermissions)(requestedPermissions);
if (!existingPermissions[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY]) {
return requestedPermissions;
}
const snapIdCaveat = existingPermissions[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY].caveats?.find((caveat) => caveat.type === snaps_utils_1.SnapCaveatType.SnapIds);
const permittedSnaps = snapIdCaveat?.value ?? {};
const requestedSnaps = requestedPermissions[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY].caveats[0].value;
const snapIdSet = new Set([
...Object.keys(permittedSnaps),
...Object.keys(requestedSnaps),
]);
const mergedCaveatValue = [...snapIdSet].reduce((request, snapId) => {
request[snapId] = requestedSnaps[snapId] ?? permittedSnaps[snapId];
return request;
}, {});
requestedPermissions[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY].caveats[0].value =
mergedCaveatValue;
return requestedPermissions;
}
exports.getSnapPermissionsRequest = getSnapPermissionsRequest;
const mutexes = new Map();
/**
* Get the corresponding Snap installation mutex for a given origin.
*
* @param origin - The origin of the request.
* @returns A mutex for that specific origin.
*/
function getMutex(origin) {
if (!mutexes.has(origin)) {
mutexes.set(origin, new async_mutex_1.Mutex());
}
return mutexes.get(origin);
}
/**
* The `wallet_requestSnaps` method implementation.
* Tries to install the requested snaps and adds them to the JSON-RPC response.
*
* @param req - The JSON-RPC request object.
* @param res - The JSON-RPC response object.
* @param _next - The `json-rpc-engine` "next" callback. Not used by this
* function.
* @param end - The `json-rpc-engine` "end" callback.
* @param _hooks - The RPC method hooks. Not used by this function.
* @param messenger - The messenger used to call controller actions.
* @returns A promise that resolves once the JSON-RPC response has been modified.
* @throws If the params are invalid.
*/
async function requestSnapsImplementation(req, res, _next, end, _hooks, messenger) {
const requestedSnaps = req.params;
if (!(0, utils_1.isObject)(requestedSnaps)) {
return end(rpc_errors_1.rpcErrors.invalidParams({
message: '"params" must be an object.',
}));
}
if (Object.keys(requestedSnaps).length === 0) {
return end(rpc_errors_1.rpcErrors.invalidParams({
message: 'Request must have at least one requested snap.',
}));
}
const { origin } = req;
const mutex = getMutex(origin);
// Process requests sequentially for each origin as permissions need to be merged
// for every request.
await mutex.runExclusive(async () => {
try {
const requestedPermissions = {
[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY]: {
caveats: [{ type: snaps_utils_1.SnapCaveatType.SnapIds, value: requestedSnaps }],
},
};
const existingPermissions = messenger.call('PermissionController:getPermissions', origin);
if (!existingPermissions) {
const [, metadata] = await messenger.call('PermissionController:requestPermissions', { origin }, requestedPermissions);
(0, utils_1.assert)(metadata.data);
res.result = metadata.data[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY];
}
else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {
res.result = await messenger.call('SnapController:installSnaps', origin, requestedSnaps);
}
else {
const mergedPermissionsRequest = getSnapPermissionsRequest(existingPermissions, requestedPermissions);
const [, metadata] = await messenger.call('PermissionController:requestPermissions', { origin }, mergedPermissionsRequest);
(0, utils_1.assert)(metadata.data);
res.result = metadata.data[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY];
}
}
catch (error) {
res.error = error;
}
});
return end();
}
//# sourceMappingURL=requestSnaps.cjs.map