{"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 { 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"]}