@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
214 lines • 8.78 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getValidatedParams = exports.getManageStateImplementation = exports.getEncryptionEntropy = exports.STORAGE_SIZE_LIMIT = exports.manageStateBuilder = exports.specificationBuilder = exports.STATE_ENCRYPTION_SALT = void 0;
const permission_controller_1 = require("@metamask/permission-controller");
const rpc_errors_1 = require("@metamask/rpc-errors");
const snaps_sdk_1 = require("@metamask/snaps-sdk");
const snaps_utils_1 = require("@metamask/snaps-utils");
const utils_1 = require("@metamask/utils");
const utils_2 = require("../utils.cjs");
// The salt used for SIP-6-based entropy derivation.
exports.STATE_ENCRYPTION_SALT = 'snap_manageState encryption';
const methodName = 'snap_manageState';
/**
* The specification builder for the `snap_manageState` permission.
* `snap_manageState` lets the Snap store and manage some of its state on
* your device.
*
* @param options - The specification builder options.
* @param options.allowedCaveats - The optional allowed caveats for the permission.
* @param options.messenger - The messenger.
* @param options.methodHooks - The RPC method hooks needed by the method implementation.
* @returns The specification for the `snap_manageState` permission.
*/
const specificationBuilder = ({ allowedCaveats = null, methodHooks, messenger, }) => {
return {
permissionType: permission_controller_1.PermissionType.RestrictedMethod,
targetName: methodName,
allowedCaveats,
methodImplementation: getManageStateImplementation({
methodHooks,
messenger,
}),
subjectTypes: [permission_controller_1.SubjectType.Snap],
};
};
exports.specificationBuilder = specificationBuilder;
const methodHooks = {
getUnlockPromise: true,
};
/**
* Allow the Snap to persist up to 64 MB of data to disk and retrieve it at
* will. By default, the data is automatically encrypted using a Snap-specific
* key and automatically decrypted when retrieved. You can set `encrypted` to
* `false` to use unencrypted storage (available when the client is locked).
*
* @example
* ```json name="Manifest"
* {
* "initialPermissions": {
* "snap_manageState": {}
* }
* }
* ```
* ```ts name="Usage"
* // Persist some data.
* await snap.request({
* method: 'snap_manageState',
* params: {
* operation: 'update',
* newState: { hello: 'world' },
* },
* })
*
* // At a later time, get the stored data.
* const persistedData = await snap.request({
* method: 'snap_manageState',
* params: { operation: 'get' },
* })
*
* console.log(persistedData)
* // { hello: 'world' }
*
* // If there's no need to store data anymore, clear it out.
* await snap.request({
* method: 'snap_manageState',
* params: {
* operation: 'clear',
* },
* })
* ```
*/
exports.manageStateBuilder = Object.freeze({
targetName: methodName,
specificationBuilder: exports.specificationBuilder,
methodHooks,
actionNames: [
'SnapController:clearSnapState',
'SnapController:getSnap',
'SnapController:getSnapState',
'SnapController:updateSnapState',
],
});
exports.STORAGE_SIZE_LIMIT = 64000000; // In bytes (64 MB)
/**
* Get a deterministic encryption key to use for encrypting and decrypting the
* state.
*
* This key should only be used for state encryption using `snap_manageState`.
* To get other encryption keys, a different salt can be used.
*
* @param args - The encryption key args.
* @param args.snapId - The ID of the snap to get the encryption key for.
* @param args.seed - The mnemonic seed to derive the encryption key
* from.
* @param args.cryptographicFunctions - The cryptographic functions to use for
* the client.
* @returns The state encryption key.
*/
async function getEncryptionEntropy({ seed, snapId, cryptographicFunctions, }) {
return await (0, utils_2.deriveEntropyFromSeed)({
seed,
input: snapId,
salt: exports.STATE_ENCRYPTION_SALT,
magic: snaps_utils_1.STATE_ENCRYPTION_MAGIC_VALUE,
cryptographicFunctions,
});
}
exports.getEncryptionEntropy = getEncryptionEntropy;
/**
* Builds the method implementation for `snap_manageState`.
*
* @param options - The options.
* @param options.messenger - The messenger.
* @param options.methodHooks - The RPC method hooks.
* @param options.methodHooks.getUnlockPromise - A function that resolves once the MetaMask
* extension is unlocked and prompts the user to unlock their MetaMask if it is
* locked.
* @returns The method implementation which either returns `null` for a
* successful state update/deletion or returns the decrypted state.
* @throws If the params are invalid.
*/
function getManageStateImplementation({ methodHooks: { getUnlockPromise }, messenger, }) {
return async function manageState(options) {
const { params = {}, method, context: { origin }, } = options;
const validatedParams = getValidatedParams(params, method);
const snap = messenger.call('SnapController:getSnap', origin);
if (!snap?.preinstalled &&
validatedParams.operation === snaps_sdk_1.ManageStateOperation.UpdateState) {
const size = (0, snaps_utils_1.getJsonSizeUnsafe)(validatedParams.newState, true);
if (size > exports.STORAGE_SIZE_LIMIT) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: `Invalid ${method} "newState" parameter: The new state must not exceed ${exports.STORAGE_SIZE_LIMIT / 1000000} MB in size.`,
});
}
}
// If the encrypted param is undefined or null we default to true.
const shouldEncrypt = validatedParams.encrypted ?? true;
// We only need to prompt the user when the mnemonic is needed
// which it isn't for the clear operation or unencrypted storage.
if (shouldEncrypt &&
validatedParams.operation !== snaps_sdk_1.ManageStateOperation.ClearState) {
await getUnlockPromise(true);
}
switch (validatedParams.operation) {
case snaps_sdk_1.ManageStateOperation.ClearState:
messenger.call('SnapController:clearSnapState', origin, shouldEncrypt);
return null;
case snaps_sdk_1.ManageStateOperation.GetState: {
return await messenger.call('SnapController:getSnapState', origin, shouldEncrypt);
}
case snaps_sdk_1.ManageStateOperation.UpdateState: {
await messenger.call('SnapController:updateSnapState', origin, validatedParams.newState, shouldEncrypt);
return null;
}
/* istanbul ignore next */
default:
throw rpc_errors_1.rpcErrors.invalidParams(`Invalid ${method} operation: "${validatedParams.operation}"`);
}
};
}
exports.getManageStateImplementation = getManageStateImplementation;
/**
* Validates the manageState method `params` and returns them cast to the correct
* type. Throws if validation fails.
*
* @param params - The unvalidated params object from the method request.
* @param method - RPC method name used for debugging errors.
* @returns The validated method parameter object.
*/
function getValidatedParams(params, method) {
if (!(0, utils_1.isObject)(params)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: 'Expected params to be a single object.',
});
}
const { operation, newState, encrypted } = params;
if (!operation ||
typeof operation !== 'string' ||
!Object.values(snaps_sdk_1.ManageStateOperation).includes(operation)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: 'Must specify a valid manage state "operation".',
});
}
if (encrypted !== undefined && typeof encrypted !== 'boolean') {
throw rpc_errors_1.rpcErrors.invalidParams({
message: '"encrypted" parameter must be a boolean if specified.',
});
}
if (operation === snaps_sdk_1.ManageStateOperation.UpdateState) {
if (!(0, utils_1.isObject)(newState)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: `Invalid ${method} "newState" parameter: The new state must be a plain object.`,
});
}
if (!(0, utils_1.isValidJson)(newState)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: `Invalid ${method} "newState" parameter: The new state must be JSON serializable.`,
});
}
}
return params;
}
exports.getValidatedParams = getValidatedParams;
//# sourceMappingURL=manageState.cjs.map