UNPKG

@metamask/snaps-rpc-methods

Version:
229 lines 8.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.set = exports.setStateHandler = void 0; const rpc_errors_1 = require("@metamask/rpc-errors"); const snaps_utils_1 = require("@metamask/snaps-utils"); const superstruct_1 = require("@metamask/superstruct"); const utils_1 = require("@metamask/utils"); const async_mutex_1 = require("async-mutex"); const manageState_1 = require("../restricted/manageState.cjs"); const utils_2 = require("../utils.cjs"); const hookNames = { 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). * * If the key is `undefined`, the value is expected to be an object. In this * case, the value is set as the new root state. * * If the key is not `undefined`, the value is set in the state at the key. If * the key does not exist, it is created (and any missing intermediate keys are * created as well). * * @example * ```json name="Manifest" * { * "initialPermissions": { * "snap_manageState": {} * } * } * ``` * ```ts name="Usage" * // Set the entire state: * await snap.request({ * method: 'snap_setState', * params: { * value: { * some: { * nested: { * value: 'Hello, world!', * }, * }, * }, * encrypted: true, // Optional, defaults to `true` * }, * }); * * // Set a specific value within the state: * await snap.request({ * method: 'snap_setState', * params: { * key: 'some.nested.value', * value: 'Hello, world!', * encrypted: true, // Optional, defaults to `true` * }, * }); * ``` */ exports.setStateHandler = { implementation: setStateImplementation, hookNames, actionNames: [ 'PermissionController:hasPermission', 'SnapController:getSnapState', 'SnapController:updateSnapState', 'SnapController:getSnap', ], }; const mutexes = new Map(); /** * Get the corresponding state modification mutex for a given Snap ID. * * @param snapId - The Snap ID. * @returns A mutex for that specific Snap. */ function getMutex(snapId) { if (!mutexes.has(snapId)) { mutexes.set(snapId, new async_mutex_1.Mutex()); } return mutexes.get(snapId); } const SetStateParametersStruct = (0, superstruct_1.object)({ key: (0, superstruct_1.optional)(utils_2.StateKeyStruct), value: utils_1.JsonStruct, encrypted: (0, superstruct_1.optional)((0, superstruct_1.boolean)()), }); /** * The `snap_setState` method implementation. * * @param request - The JSON-RPC request object. * @param response - 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. * @param hooks.getUnlockPromise - Wait for the extension to be unlocked. * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function setStateImplementation(request, response, _next, end, { getUnlockPromise }, messenger) { const { params, origin } = request; if (!messenger.call('PermissionController:hasPermission', origin, manageState_1.manageStateBuilder.targetName)) { return end(rpc_errors_1.providerErrors.unauthorized()); } try { const validatedParams = getValidatedParams(params); const { key, value, encrypted = true } = validatedParams; if (key === undefined && !(0, utils_1.isObject)(value)) { return end(rpc_errors_1.rpcErrors.invalidParams('Invalid params: Value must be an object if key is not provided.')); } if (encrypted) { await getUnlockPromise(true); } const snapId = origin; const mutex = getMutex(snapId); // The expectation when using `snap_setState` is for the operation to safe // to do in parallel. The mutex ensures that and prevents a bug that was // mostly prevalent on mobile and caused data loss. await mutex.runExclusive(async () => { const newState = await getNewState(snapId, key, value, encrypted, messenger); const snap = messenger.call('SnapController:getSnap', origin); if (!snap?.preinstalled) { // We know that the state is valid JSON as per previous validation. const size = (0, snaps_utils_1.getJsonSizeUnsafe)(newState, true); if (size > manageState_1.STORAGE_SIZE_LIMIT) { throw rpc_errors_1.rpcErrors.invalidParams({ message: `Invalid params: The new state must not exceed ${manageState_1.STORAGE_SIZE_LIMIT / 1000000} MB in size.`, }); } } await messenger.call('SnapController:updateSnapState', origin, newState, encrypted); response.result = null; }); } catch (error) { return end(error); } return end(); } /** * Validate the parameters of the `snap_setState` method. * * @param params - The parameters to validate. * @returns The validated parameters. */ function getValidatedParams(params) { try { return (0, superstruct_1.create)(params, SetStateParametersStruct); } catch (error) { if (error instanceof superstruct_1.StructError) { throw rpc_errors_1.rpcErrors.invalidParams({ message: `Invalid params: ${error.message}.`, }); } /* istanbul ignore next */ throw rpc_errors_1.rpcErrors.internal(); } } /** * Get the new state of the Snap. * * If the key is `undefined`, the value is expected to be an object. In this * case, the value is returned as the new state. * * If the key is not `undefined`, the value is set in the state at the key. If * the key does not exist, it is created (and any missing intermediate keys are * created as well). * * @param snapId - The Snap ID. * @param key - The key to set. * @param value - The value to set the key to. * @param encrypted - Whether the state is encrypted. * @param messenger - The messenger used to call controller actions. * @returns The new state of the Snap. */ async function getNewState(snapId, key, value, encrypted, messenger) { if (key === undefined) { (0, utils_1.assert)((0, utils_1.isObject)(value)); return value; } const state = await messenger.call('SnapController:getSnapState', snapId, encrypted); return set(state, key, value); } /** * Set the value of a key in an object. The key may contain Lodash-style path * syntax, e.g., `a.b.c` (with the exception of array syntax). If the key does * not exist, it is created (and any missing intermediate keys are created as * well). * * This is a simplified version of Lodash's `set` function, but Lodash doesn't * seem to be maintained anymore, so we're using our own implementation. * * @param object - The object to get the key from. * @param key - The key to set. * @param value - The value to set the key to. * @returns The new object with the key set to the value. */ function set(object, key, value) { const keys = key.split('.'); const requiredObject = object ?? {}; let currentObject = requiredObject; for (let i = 0; i < keys.length; i++) { const currentKey = keys[i]; if (utils_2.FORBIDDEN_KEYS.includes(currentKey)) { throw rpc_errors_1.rpcErrors.invalidParams('Invalid params: Key contains forbidden characters.'); } if (i === keys.length - 1) { currentObject[currentKey] = value; return requiredObject; } if (!(0, utils_1.hasProperty)(currentObject, currentKey) || currentObject[currentKey] === null) { currentObject[currentKey] = {}; } else if (!(0, utils_1.isObject)(currentObject[currentKey])) { throw rpc_errors_1.rpcErrors.invalidParams('Invalid params: Cannot overwrite non-object value.'); } currentObject = currentObject[currentKey]; } // This should never be reached. /* istanbul ignore next */ throw new Error('Unexpected error while setting the state.'); } exports.set = set; //# sourceMappingURL=setState.cjs.map