UNPKG

@metamask/snaps-rpc-methods

Version:
154 lines 5.77 kB
import { providerErrors, rpcErrors } from "@metamask/rpc-errors"; import { boolean, create, object as objectStruct, optional, StructError } from "@metamask/superstruct"; import { getJsonSize, hasProperty, isObject, assert, JsonStruct } from "@metamask/utils"; import { manageStateBuilder, STORAGE_SIZE_LIMIT } from "../restricted/manageState.mjs"; import { FORBIDDEN_KEYS, StateKeyStruct } from "../utils.mjs"; const hookNames = { hasPermission: true, getSnapState: true, getUnlockPromise: true, updateSnapState: true, }; /** * `snap_setState` sets the state of the Snap. */ export const setStateHandler = { methodNames: ['snap_setState'], implementation: setStateImplementation, hookNames, }; const SetStateParametersStruct = objectStruct({ key: optional(StateKeyStruct), value: JsonStruct, encrypted: optional(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.hasPermission - Check whether a given origin has a given * permission. * @param hooks.getSnapState - Get the state of the requesting Snap. * @param hooks.getUnlockPromise - Wait for the extension to be unlocked. * @param hooks.updateSnapState - Update the state of the requesting Snap. * @returns Nothing. */ async function setStateImplementation(request, response, _next, end, { hasPermission, getSnapState, getUnlockPromise, updateSnapState, }) { const { params } = request; if (!hasPermission(manageStateBuilder.targetName)) { return end(providerErrors.unauthorized()); } try { const validatedParams = getValidatedParams(params); const { key, value, encrypted = true } = validatedParams; if (key === undefined && !isObject(value)) { return end(rpcErrors.invalidParams('Invalid params: Value must be an object if key is not provided.')); } if (encrypted) { await getUnlockPromise(true); } const newState = await getNewState(key, value, encrypted, getSnapState); const size = getJsonSize(newState); if (size > STORAGE_SIZE_LIMIT) { throw rpcErrors.invalidParams({ message: `Invalid params: The new state must not exceed ${STORAGE_SIZE_LIMIT / 1000000} MB in size.`, }); } await updateSnapState(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 create(params, SetStateParametersStruct); } catch (error) { if (error instanceof StructError) { throw rpcErrors.invalidParams({ message: `Invalid params: ${error.message}.`, }); } /* istanbul ignore next */ throw 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 key - The key to set. * @param value - The value to set the key to. * @param encrypted - Whether the state is encrypted. * @param getSnapState - The `getSnapState` hook. * @returns The new state of the Snap. */ async function getNewState(key, value, encrypted, getSnapState) { if (key === undefined) { assert(isObject(value)); return value; } const state = await getSnapState(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. */ export 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 (FORBIDDEN_KEYS.includes(currentKey)) { throw rpcErrors.invalidParams('Invalid params: Key contains forbidden characters.'); } if (i === keys.length - 1) { currentObject[currentKey] = value; return requiredObject; } if (!hasProperty(currentObject, currentKey) || currentObject[currentKey] === null) { currentObject[currentKey] = {}; } else if (!isObject(currentObject[currentKey])) { throw 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.'); } //# sourceMappingURL=setState.mjs.map