UNPKG

@metamask/snaps-rpc-methods

Version:
1 lines 14.1 kB
{"version":3,"file":"manageState.mjs","sourceRoot":"","sources":["../../src/restricted/manageState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CryptographicFunctions } from '@metamask/key-tree';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { ManageStateParams, ManageStateResult } from '@metamask/snaps-sdk';\nimport { ManageStateOperation } from '@metamask/snaps-sdk';\nimport {\n getJsonSizeUnsafe,\n STATE_ENCRYPTION_MAGIC_VALUE,\n} from '@metamask/snaps-utils';\nimport type { NonEmptyArray } from '@metamask/utils';\nimport { isObject, isValidJson } from '@metamask/utils';\n\nimport type {\n SnapControllerClearSnapStateAction,\n SnapControllerGetSnapAction,\n SnapControllerGetSnapStateAction,\n SnapControllerUpdateSnapStateAction,\n} from '../types';\nimport type { MethodHooksObject } from '../utils';\nimport { deriveEntropyFromSeed } from '../utils';\n\n// The salt used for SIP-6-based entropy derivation.\nexport const STATE_ENCRYPTION_SALT = 'snap_manageState encryption';\n\nconst methodName = 'snap_manageState';\n\nexport type ManageStateMethodHooks = {\n /**\n * Waits for the extension to be unlocked.\n *\n * @returns A promise that resolves once the extension is unlocked.\n */\n getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise<void>;\n};\n\nexport type ManageStateMessengerActions =\n | SnapControllerClearSnapStateAction\n | SnapControllerGetSnapAction\n | SnapControllerGetSnapStateAction\n | SnapControllerUpdateSnapStateAction;\n\ntype ManageStateSpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: ManageStateMethodHooks;\n messenger: Messenger<string, ManageStateMessengerActions>;\n};\n\ntype ManageStateSpecification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getManageStateImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_manageState` permission.\n * `snap_manageState` lets the Snap store and manage some of its state on\n * your device.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.messenger - The messenger.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_manageState` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n ManageStateSpecificationBuilderOptions,\n ManageStateSpecification\n> = ({\n allowedCaveats = null,\n methodHooks,\n messenger,\n}: ManageStateSpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getManageStateImplementation({\n methodHooks,\n messenger,\n }),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<ManageStateMethodHooks> = {\n getUnlockPromise: true,\n};\n\n/**\n * Allow the Snap to persist up to 64 MB of data to disk and retrieve it at\n * will. By default, the data is automatically encrypted using a Snap-specific\n * key and automatically decrypted when retrieved. You can set `encrypted` to\n * `false` to use unencrypted storage (available when the client is locked).\n *\n * @example\n * ```json name=\"Manifest\"\n * {\n * \"initialPermissions\": {\n * \"snap_manageState\": {}\n * }\n * }\n * ```\n * ```ts name=\"Usage\"\n * // Persist some data.\n * await snap.request({\n * method: 'snap_manageState',\n * params: {\n * operation: 'update',\n * newState: { hello: 'world' },\n * },\n * })\n *\n * // At a later time, get the stored data.\n * const persistedData = await snap.request({\n * method: 'snap_manageState',\n * params: { operation: 'get' },\n * })\n *\n * console.log(persistedData)\n * // { hello: 'world' }\n *\n * // If there's no need to store data anymore, clear it out.\n * await snap.request({\n * method: 'snap_manageState',\n * params: {\n * operation: 'clear',\n * },\n * })\n * ```\n */\nexport const manageStateBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n actionNames: [\n 'SnapController:clearSnapState',\n 'SnapController:getSnap',\n 'SnapController:getSnapState',\n 'SnapController:updateSnapState',\n ],\n} as const);\n\nexport const STORAGE_SIZE_LIMIT = 64_000_000; // In bytes (64 MB)\n\ntype GetEncryptionKeyArgs = {\n snapId: string;\n seed: Uint8Array;\n cryptographicFunctions?: CryptographicFunctions | undefined;\n};\n\n/**\n * Get a deterministic encryption key to use for encrypting and decrypting the\n * state.\n *\n * This key should only be used for state encryption using `snap_manageState`.\n * To get other encryption keys, a different salt can be used.\n *\n * @param args - The encryption key args.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.seed - The mnemonic seed to derive the encryption key\n * from.\n * @param args.cryptographicFunctions - The cryptographic functions to use for\n * the client.\n * @returns The state encryption key.\n */\nexport async function getEncryptionEntropy({\n seed,\n snapId,\n cryptographicFunctions,\n}: GetEncryptionKeyArgs) {\n return await deriveEntropyFromSeed({\n seed,\n input: snapId,\n salt: STATE_ENCRYPTION_SALT,\n magic: STATE_ENCRYPTION_MAGIC_VALUE,\n cryptographicFunctions,\n });\n}\n\n/**\n * Builds the method implementation for `snap_manageState`.\n *\n * @param options - The options.\n * @param options.messenger - The messenger.\n * @param options.methodHooks - The RPC method hooks.\n * @param options.methodHooks.getUnlockPromise - A function that resolves once the MetaMask\n * extension is unlocked and prompts the user to unlock their MetaMask if it is\n * locked.\n * @returns The method implementation which either returns `null` for a\n * successful state update/deletion or returns the decrypted state.\n * @throws If the params are invalid.\n */\nexport function getManageStateImplementation({\n methodHooks: { getUnlockPromise },\n messenger,\n}: ManageStateSpecificationBuilderOptions) {\n return async function manageState(\n options: RestrictedMethodOptions<ManageStateParams>,\n ): Promise<ManageStateResult> {\n const {\n params = {},\n method,\n context: { origin },\n } = options;\n const validatedParams = getValidatedParams(params, method);\n\n const snap = messenger.call('SnapController:getSnap', origin);\n\n if (\n !snap?.preinstalled &&\n validatedParams.operation === ManageStateOperation.UpdateState\n ) {\n const size = getJsonSizeUnsafe(validatedParams.newState, true);\n\n if (size > STORAGE_SIZE_LIMIT) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"newState\" parameter: The new state must not exceed ${\n STORAGE_SIZE_LIMIT / 1_000_000\n } MB in size.`,\n });\n }\n }\n\n // If the encrypted param is undefined or null we default to true.\n const shouldEncrypt = validatedParams.encrypted ?? true;\n\n // We only need to prompt the user when the mnemonic is needed\n // which it isn't for the clear operation or unencrypted storage.\n if (\n shouldEncrypt &&\n validatedParams.operation !== ManageStateOperation.ClearState\n ) {\n await getUnlockPromise(true);\n }\n\n switch (validatedParams.operation) {\n case ManageStateOperation.ClearState:\n messenger.call('SnapController:clearSnapState', origin, shouldEncrypt);\n return null;\n\n case ManageStateOperation.GetState: {\n return await messenger.call(\n 'SnapController:getSnapState',\n origin,\n shouldEncrypt,\n );\n }\n\n case ManageStateOperation.UpdateState: {\n await messenger.call(\n 'SnapController:updateSnapState',\n origin,\n validatedParams.newState,\n shouldEncrypt,\n );\n return null;\n }\n\n /* istanbul ignore next */\n default:\n throw rpcErrors.invalidParams(\n `Invalid ${method} operation: \"${\n validatedParams.operation as string\n }\"`,\n );\n }\n };\n}\n\n/**\n * Validates the manageState method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @param method - RPC method name used for debugging errors.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(\n params: unknown,\n method: string,\n): ManageStateParams {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { operation, newState, encrypted } = params;\n\n if (\n !operation ||\n typeof operation !== 'string' ||\n !Object.values(ManageStateOperation).includes(\n operation as ManageStateOperation,\n )\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid manage state \"operation\".',\n });\n }\n\n if (encrypted !== undefined && typeof encrypted !== 'boolean') {\n throw rpcErrors.invalidParams({\n message: '\"encrypted\" parameter must be a boolean if specified.',\n });\n }\n\n if (operation === ManageStateOperation.UpdateState) {\n if (!isObject(newState)) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"newState\" parameter: The new state must be a plain object.`,\n });\n }\n\n if (!isValidJson(newState)) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"newState\" parameter: The new state must be JSON serializable.`,\n });\n }\n }\n\n return params as ManageStateParams;\n}\n"]}