UNPKG

@metamask/snaps-rpc-methods

Version:
1 lines 12.4 kB
{"version":3,"file":"manageState.cjs","sourceRoot":"","sources":["../../src/restricted/manageState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CryptographicFunctions } from '@metamask/key-tree';\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 { STATE_ENCRYPTION_MAGIC_VALUE } from '@metamask/snaps-utils';\nimport type { Json, NonEmptyArray } from '@metamask/utils';\nimport { isObject, getJsonSize } from '@metamask/utils';\n\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 /**\n * A function that clears the state of the requesting Snap.\n */\n clearSnapState: (snapId: string, encrypted: boolean) => void;\n\n /**\n * A function that gets the encrypted state of the requesting Snap.\n *\n * @returns The current state of the Snap.\n */\n getSnapState: (\n snapId: string,\n encrypted: boolean,\n ) => Promise<Record<string, Json>>;\n\n /**\n * A function that updates the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n */\n updateSnapState: (\n snapId: string,\n newState: Record<string, Json>,\n encrypted: boolean,\n ) => Promise<void>;\n};\n\ntype ManageStateSpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: ManageStateMethodHooks;\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.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}: ManageStateSpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getManageStateImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<ManageStateMethodHooks> = {\n getUnlockPromise: true,\n clearSnapState: true,\n getSnapState: true,\n updateSnapState: true,\n};\n\nexport const manageStateBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\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 hooks - The RPC method hooks.\n * @param hooks.clearSnapState - A function that clears the state stored for a\n * snap.\n * @param hooks.getSnapState - A function that fetches the persisted decrypted\n * state for a snap.\n * @param hooks.updateSnapState - A function that updates the state stored for a\n * snap.\n * @param hooks.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 getUnlockPromise,\n clearSnapState,\n getSnapState,\n updateSnapState,\n}: ManageStateMethodHooks) {\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 // 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 clearSnapState(origin, shouldEncrypt);\n return null;\n\n case ManageStateOperation.GetState: {\n return await getSnapState(origin, shouldEncrypt);\n }\n\n case ManageStateOperation.UpdateState: {\n await updateSnapState(origin, validatedParams.newState, shouldEncrypt);\n return null;\n }\n\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 * @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(\n params: unknown,\n method: string,\n storageSizeLimit = STORAGE_SIZE_LIMIT,\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 let size;\n try {\n // `getJsonSize` will throw if the state is not JSON serializable.\n size = getJsonSize(newState);\n } catch {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"newState\" parameter: The new state must be JSON serializable.`,\n });\n }\n\n if (size > storageSizeLimit) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"newState\" parameter: The new state must not exceed ${\n storageSizeLimit / 1_000_000\n } MB in size.`,\n });\n }\n }\n\n return params as ManageStateParams;\n}\n"]}