@metamask/snaps-rpc-methods 14.1.0 → 14.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/endowments/enum.cjs +1 -0
  3. package/dist/endowments/enum.cjs.map +1 -1
  4. package/dist/endowments/enum.d.cts +1 -0
  5. package/dist/endowments/enum.d.cts.map +1 -1
  6. package/dist/endowments/enum.d.mts +1 -0
  7. package/dist/endowments/enum.d.mts.map +1 -1
  8. package/dist/endowments/enum.mjs +1 -0
  9. package/dist/endowments/enum.mjs.map +1 -1
  10. package/dist/endowments/index.cjs +2 -0
  11. package/dist/endowments/index.cjs.map +1 -1
  12. package/dist/endowments/index.d.cts +9 -0
  13. package/dist/endowments/index.d.cts.map +1 -1
  14. package/dist/endowments/index.d.mts +9 -0
  15. package/dist/endowments/index.d.mts.map +1 -1
  16. package/dist/endowments/index.mjs +2 -0
  17. package/dist/endowments/index.mjs.map +1 -1
  18. package/dist/endowments/multichain-provider.cjs +28 -0
  19. package/dist/endowments/multichain-provider.cjs.map +1 -0
  20. package/dist/endowments/multichain-provider.d.cts +15 -0
  21. package/dist/endowments/multichain-provider.d.cts.map +1 -0
  22. package/dist/endowments/multichain-provider.d.mts +15 -0
  23. package/dist/endowments/multichain-provider.d.mts.map +1 -0
  24. package/dist/endowments/multichain-provider.mjs +25 -0
  25. package/dist/endowments/multichain-provider.mjs.map +1 -0
  26. package/dist/permitted/createInterface.d.cts +6 -2
  27. package/dist/permitted/createInterface.d.cts.map +1 -1
  28. package/dist/permitted/createInterface.d.mts +6 -2
  29. package/dist/permitted/createInterface.d.mts.map +1 -1
  30. package/dist/permitted/requestSnaps.cjs +47 -26
  31. package/dist/permitted/requestSnaps.cjs.map +1 -1
  32. package/dist/permitted/requestSnaps.d.cts.map +1 -1
  33. package/dist/permitted/requestSnaps.d.mts.map +1 -1
  34. package/dist/permitted/requestSnaps.mjs +47 -26
  35. package/dist/permitted/requestSnaps.mjs.map +1 -1
  36. package/dist/permitted/setState.cjs +33 -12
  37. package/dist/permitted/setState.cjs.map +1 -1
  38. package/dist/permitted/setState.d.cts.map +1 -1
  39. package/dist/permitted/setState.d.mts.map +1 -1
  40. package/dist/permitted/setState.mjs +33 -12
  41. package/dist/permitted/setState.mjs.map +1 -1
  42. package/dist/permitted/updateInterface.d.cts +6 -2
  43. package/dist/permitted/updateInterface.d.cts.map +1 -1
  44. package/dist/permitted/updateInterface.d.mts +6 -2
  45. package/dist/permitted/updateInterface.d.mts.map +1 -1
  46. package/dist/restricted/dialog.d.cts +12 -4
  47. package/dist/restricted/dialog.d.cts.map +1 -1
  48. package/dist/restricted/dialog.d.mts +12 -4
  49. package/dist/restricted/dialog.d.mts.map +1 -1
  50. package/dist/restricted/notify.d.cts +9 -3
  51. package/dist/restricted/notify.d.cts.map +1 -1
  52. package/dist/restricted/notify.d.mts +9 -3
  53. package/dist/restricted/notify.d.mts.map +1 -1
  54. package/package.json +9 -8
@@ -4,6 +4,7 @@ exports.getSnapPermissionsRequest = exports.hasRequestedSnaps = exports.requestS
4
4
  const rpc_errors_1 = require("@metamask/rpc-errors");
5
5
  const snaps_utils_1 = require("@metamask/snaps-utils");
6
6
  const utils_1 = require("@metamask/utils");
7
+ const async_mutex_1 = require("async-mutex");
7
8
  const invokeSnap_1 = require("../restricted/invokeSnap.cjs");
8
9
  const hookNames = {
9
10
  installSnaps: true,
@@ -64,6 +65,19 @@ function getSnapPermissionsRequest(existingPermissions, requestedPermissions) {
64
65
  return requestedPermissions;
65
66
  }
66
67
  exports.getSnapPermissionsRequest = getSnapPermissionsRequest;
68
+ const mutexes = new Map();
69
+ /**
70
+ * Get the corresponding Snap installation mutex for a given origin.
71
+ *
72
+ * @param origin - The origin of the request.
73
+ * @returns A mutex for that specific origin.
74
+ */
75
+ function getMutex(origin) {
76
+ if (!mutexes.has(origin)) {
77
+ mutexes.set(origin, new async_mutex_1.Mutex());
78
+ }
79
+ return mutexes.get(origin);
80
+ }
67
81
  /**
68
82
  * The `wallet_requestSnaps` method implementation.
69
83
  * Tries to install the requested snaps and adds them to the JSON-RPC response.
@@ -88,34 +102,41 @@ async function requestSnapsImplementation(req, res, _next, end, { installSnaps,
88
102
  message: '"params" must be an object.',
89
103
  }));
90
104
  }
91
- try {
92
- if (Object.keys(requestedSnaps).length === 0) {
93
- return end(rpc_errors_1.rpcErrors.invalidParams({
94
- message: 'Request must have at least one requested snap.',
95
- }));
96
- }
97
- const requestedPermissions = {
98
- [invokeSnap_1.WALLET_SNAP_PERMISSION_KEY]: {
99
- caveats: [{ type: snaps_utils_1.SnapCaveatType.SnapIds, value: requestedSnaps }],
100
- },
101
- };
102
- const existingPermissions = await getPermissions();
103
- if (!existingPermissions) {
104
- const [, metadata] = await requestPermissions(requestedPermissions);
105
- res.result = metadata.data[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY];
106
- }
107
- else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {
108
- res.result = await installSnaps(requestedSnaps);
105
+ if (Object.keys(requestedSnaps).length === 0) {
106
+ return end(rpc_errors_1.rpcErrors.invalidParams({
107
+ message: 'Request must have at least one requested snap.',
108
+ }));
109
+ }
110
+ // We expect the MM middleware stack to always add the origin to requests
111
+ const { origin } = req;
112
+ const mutex = getMutex(origin);
113
+ // Process requests sequentially for each origin as permissions need to be merged
114
+ // for every request.
115
+ await mutex.runExclusive(async () => {
116
+ try {
117
+ const requestedPermissions = {
118
+ [invokeSnap_1.WALLET_SNAP_PERMISSION_KEY]: {
119
+ caveats: [{ type: snaps_utils_1.SnapCaveatType.SnapIds, value: requestedSnaps }],
120
+ },
121
+ };
122
+ const existingPermissions = await getPermissions();
123
+ if (!existingPermissions) {
124
+ const [, metadata] = await requestPermissions(requestedPermissions);
125
+ res.result = metadata.data[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY];
126
+ }
127
+ else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {
128
+ res.result = await installSnaps(requestedSnaps);
129
+ }
130
+ else {
131
+ const mergedPermissionsRequest = getSnapPermissionsRequest(existingPermissions, requestedPermissions);
132
+ const [, metadata] = await requestPermissions(mergedPermissionsRequest);
133
+ res.result = metadata.data[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY];
134
+ }
109
135
  }
110
- else {
111
- const mergedPermissionsRequest = getSnapPermissionsRequest(existingPermissions, requestedPermissions);
112
- const [, metadata] = await requestPermissions(mergedPermissionsRequest);
113
- res.result = metadata.data[invokeSnap_1.WALLET_SNAP_PERMISSION_KEY];
136
+ catch (error) {
137
+ res.error = error;
114
138
  }
115
- }
116
- catch (error) {
117
- res.error = error;
118
- }
139
+ });
119
140
  return end();
120
141
  }
121
142
  //# sourceMappingURL=requestSnaps.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"requestSnaps.cjs","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type {\n PermissionConstraint,\n RequestedPermissions,\n Caveat,\n PermittedHandlerExport,\n} from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type {\n RequestSnapsParams,\n RequestSnapsResult,\n} from '@metamask/snaps-sdk';\nimport type { SnapsPermissionRequest } from '@metamask/snaps-utils';\nimport {\n SnapCaveatType,\n verifyRequestedSnapPermissions,\n} from '@metamask/snaps-utils';\nimport type {\n JsonRpcRequest,\n PendingJsonRpcResponse,\n Json,\n} from '@metamask/utils';\nimport { hasProperty, isObject } from '@metamask/utils';\n\nimport { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap';\nimport type { MethodHooksObject } from '../utils';\n\nconst hookNames: MethodHooksObject<RequestSnapsHooks> = {\n installSnaps: true,\n requestPermissions: true,\n getPermissions: true,\n};\n\n/**\n * `wallet_requestSnaps` installs the requested Snaps and requests permission to use them if necessary.\n */\nexport const requestSnapsHandler: PermittedHandlerExport<\n RequestSnapsHooks,\n RequestSnapsParams,\n RequestSnapsResult\n> = {\n methodNames: ['wallet_requestSnaps'],\n implementation: requestSnapsImplementation,\n hookNames,\n};\n\nexport type RequestSnapsHooks = {\n /**\n * Installs the requested snaps if they are permitted.\n */\n installSnaps: (\n requestedSnaps: RequestSnapsParams,\n ) => Promise<RequestSnapsResult>;\n\n /**\n * Initiates a permission request for the requesting origin.\n *\n * @returns The result of the permissions request.\n */\n requestPermissions: (\n permissions: RequestedPermissions,\n ) => Promise<\n [\n Record<string, PermissionConstraint>,\n { data: Record<string, unknown>; id: string; origin: string },\n ]\n >;\n\n /**\n * Gets the current permissions for the requesting origin.\n *\n * @returns The current permissions of the requesting origin.\n */\n getPermissions: () => Promise<\n Record<string, PermissionConstraint> | undefined\n >;\n};\n\n/**\n * Checks whether an origin has existing `wallet_snap` permission and\n * whether or not it has the requested snapIds caveat.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedSnaps - The requested snaps.\n * @returns True if the existing permissions satisfy the requested snaps, otherwise false.\n */\nexport function hasRequestedSnaps(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedSnaps: Record<string, unknown>,\n): boolean {\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ]?.caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = snapIdCaveat?.value;\n if (isObject(permittedSnaps)) {\n return Object.keys(requestedSnaps).every((requestedSnap) =>\n hasProperty(permittedSnaps, requestedSnap),\n );\n }\n return false;\n}\n\n/**\n * Constructs a valid permission request with merged caveats based on existing permissions\n * and the requested snaps.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedPermissions - The permission request passed into `requestPermissions`.\n * @returns `requestedPermissions`.\n */\nexport function getSnapPermissionsRequest(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedPermissions: unknown,\n): SnapsPermissionRequest {\n verifyRequestedSnapPermissions(requestedPermissions);\n\n if (!existingPermissions[WALLET_SNAP_PERMISSION_KEY]) {\n return requestedPermissions;\n }\n\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ].caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = (snapIdCaveat?.value as Record<string, Json>) ?? {};\n\n const requestedSnaps =\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value;\n\n const snapIdSet = new Set([\n ...Object.keys(permittedSnaps),\n ...Object.keys(requestedSnaps),\n ]);\n\n const mergedCaveatValue = [...snapIdSet].reduce<Record<string, Json>>(\n (request, snapId) => {\n request[snapId] = requestedSnaps[snapId] ?? permittedSnaps[snapId];\n return request;\n },\n {},\n );\n\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value =\n mergedCaveatValue;\n\n return requestedPermissions;\n}\n\n/**\n * The `wallet_requestSnaps` method implementation.\n * Tries to install the requested snaps and adds them to the JSON-RPC response.\n *\n * @param req - The JSON-RPC request object.\n * @param res - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.installSnaps - A function that tries to install a given snap, prompting the user if necessary.\n * @param hooks.requestPermissions - A function that requests permissions on\n * behalf of a subject.\n * @param hooks.getPermissions - A function that gets the current permissions.\n * @returns A promise that resolves once the JSON-RPC response has been modified.\n * @throws If the params are invalid.\n */\nasync function requestSnapsImplementation(\n req: JsonRpcRequest<RequestSnapsParams>,\n res: PendingJsonRpcResponse<RequestSnapsResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n { installSnaps, requestPermissions, getPermissions }: RequestSnapsHooks,\n): Promise<void> {\n const requestedSnaps = req.params;\n if (!isObject(requestedSnaps)) {\n return end(\n rpcErrors.invalidParams({\n message: '\"params\" must be an object.',\n }),\n );\n }\n\n try {\n if (Object.keys(requestedSnaps).length === 0) {\n return end(\n rpcErrors.invalidParams({\n message: 'Request must have at least one requested snap.',\n }),\n );\n }\n\n const requestedPermissions = {\n [WALLET_SNAP_PERMISSION_KEY]: {\n caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }],\n },\n } as RequestedPermissions;\n const existingPermissions = await getPermissions();\n\n if (!existingPermissions) {\n const [, metadata] = await requestPermissions(requestedPermissions);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n } else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {\n res.result = await installSnaps(requestedSnaps);\n } else {\n const mergedPermissionsRequest = getSnapPermissionsRequest(\n existingPermissions,\n requestedPermissions,\n );\n\n const [, metadata] = await requestPermissions(mergedPermissionsRequest);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n }\n } catch (error) {\n res.error = error;\n }\n\n return end();\n}\n"]}
1
+ {"version":3,"file":"requestSnaps.cjs","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type {\n PermissionConstraint,\n RequestedPermissions,\n Caveat,\n PermittedHandlerExport,\n} from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type {\n RequestSnapsParams,\n RequestSnapsResult,\n} from '@metamask/snaps-sdk';\nimport type { SnapsPermissionRequest } from '@metamask/snaps-utils';\nimport {\n SnapCaveatType,\n verifyRequestedSnapPermissions,\n} from '@metamask/snaps-utils';\nimport type {\n JsonRpcRequest,\n PendingJsonRpcResponse,\n Json,\n} from '@metamask/utils';\nimport { hasProperty, isObject } from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\n\nimport { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap';\nimport type { MethodHooksObject } from '../utils';\n\nconst hookNames: MethodHooksObject<RequestSnapsHooks> = {\n installSnaps: true,\n requestPermissions: true,\n getPermissions: true,\n};\n\n/**\n * `wallet_requestSnaps` installs the requested Snaps and requests permission to use them if necessary.\n */\nexport const requestSnapsHandler: PermittedHandlerExport<\n RequestSnapsHooks,\n RequestSnapsParams,\n RequestSnapsResult\n> = {\n methodNames: ['wallet_requestSnaps'],\n implementation: requestSnapsImplementation,\n hookNames,\n};\n\nexport type RequestSnapsHooks = {\n /**\n * Installs the requested snaps if they are permitted.\n */\n installSnaps: (\n requestedSnaps: RequestSnapsParams,\n ) => Promise<RequestSnapsResult>;\n\n /**\n * Initiates a permission request for the requesting origin.\n *\n * @returns The result of the permissions request.\n */\n requestPermissions: (\n permissions: RequestedPermissions,\n ) => Promise<\n [\n Record<string, PermissionConstraint>,\n { data: Record<string, unknown>; id: string; origin: string },\n ]\n >;\n\n /**\n * Gets the current permissions for the requesting origin.\n *\n * @returns The current permissions of the requesting origin.\n */\n getPermissions: () => Promise<\n Record<string, PermissionConstraint> | undefined\n >;\n};\n\n/**\n * Checks whether an origin has existing `wallet_snap` permission and\n * whether or not it has the requested snapIds caveat.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedSnaps - The requested snaps.\n * @returns True if the existing permissions satisfy the requested snaps, otherwise false.\n */\nexport function hasRequestedSnaps(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedSnaps: Record<string, unknown>,\n): boolean {\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ]?.caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = snapIdCaveat?.value;\n if (isObject(permittedSnaps)) {\n return Object.keys(requestedSnaps).every((requestedSnap) =>\n hasProperty(permittedSnaps, requestedSnap),\n );\n }\n return false;\n}\n\n/**\n * Constructs a valid permission request with merged caveats based on existing permissions\n * and the requested snaps.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedPermissions - The permission request passed into `requestPermissions`.\n * @returns `requestedPermissions`.\n */\nexport function getSnapPermissionsRequest(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedPermissions: unknown,\n): SnapsPermissionRequest {\n verifyRequestedSnapPermissions(requestedPermissions);\n\n if (!existingPermissions[WALLET_SNAP_PERMISSION_KEY]) {\n return requestedPermissions;\n }\n\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ].caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = (snapIdCaveat?.value as Record<string, Json>) ?? {};\n\n const requestedSnaps =\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value;\n\n const snapIdSet = new Set([\n ...Object.keys(permittedSnaps),\n ...Object.keys(requestedSnaps),\n ]);\n\n const mergedCaveatValue = [...snapIdSet].reduce<Record<string, Json>>(\n (request, snapId) => {\n request[snapId] = requestedSnaps[snapId] ?? permittedSnaps[snapId];\n return request;\n },\n {},\n );\n\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value =\n mergedCaveatValue;\n\n return requestedPermissions;\n}\n\nconst mutexes = new Map();\n\n/**\n * Get the corresponding Snap installation mutex for a given origin.\n *\n * @param origin - The origin of the request.\n * @returns A mutex for that specific origin.\n */\nfunction getMutex(origin: string) {\n if (!mutexes.has(origin)) {\n mutexes.set(origin, new Mutex());\n }\n return mutexes.get(origin);\n}\n\n/**\n * The `wallet_requestSnaps` method implementation.\n * Tries to install the requested snaps and adds them to the JSON-RPC response.\n *\n * @param req - The JSON-RPC request object.\n * @param res - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.installSnaps - A function that tries to install a given snap, prompting the user if necessary.\n * @param hooks.requestPermissions - A function that requests permissions on\n * behalf of a subject.\n * @param hooks.getPermissions - A function that gets the current permissions.\n * @returns A promise that resolves once the JSON-RPC response has been modified.\n * @throws If the params are invalid.\n */\nasync function requestSnapsImplementation(\n req: JsonRpcRequest<RequestSnapsParams>,\n res: PendingJsonRpcResponse<RequestSnapsResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n { installSnaps, requestPermissions, getPermissions }: RequestSnapsHooks,\n): Promise<void> {\n const requestedSnaps = req.params;\n if (!isObject(requestedSnaps)) {\n return end(\n rpcErrors.invalidParams({\n message: '\"params\" must be an object.',\n }),\n );\n }\n\n if (Object.keys(requestedSnaps).length === 0) {\n return end(\n rpcErrors.invalidParams({\n message: 'Request must have at least one requested snap.',\n }),\n );\n }\n\n // We expect the MM middleware stack to always add the origin to requests\n const { origin } = req as JsonRpcRequest & { origin: string };\n\n const mutex = getMutex(origin);\n\n // Process requests sequentially for each origin as permissions need to be merged\n // for every request.\n await mutex.runExclusive(async () => {\n try {\n const requestedPermissions = {\n [WALLET_SNAP_PERMISSION_KEY]: {\n caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }],\n },\n } as RequestedPermissions;\n const existingPermissions = await getPermissions();\n\n if (!existingPermissions) {\n const [, metadata] = await requestPermissions(requestedPermissions);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n } else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {\n res.result = await installSnaps(requestedSnaps);\n } else {\n const mergedPermissionsRequest = getSnapPermissionsRequest(\n existingPermissions,\n requestedPermissions,\n );\n\n const [, metadata] = await requestPermissions(mergedPermissionsRequest);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n }\n } catch (error) {\n res.error = error;\n }\n });\n\n return end();\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"requestSnaps.d.cts","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EAEpB,sBAAsB,EACvB,wCAAwC;AAEzC,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EACnB,4BAA4B;AAC7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,8BAA8B;AAqBpE;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CACtD,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,CAKnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,YAAY,EAAE,CACZ,cAAc,EAAE,kBAAkB,KAC/B,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEjC;;;;OAIG;IACH,kBAAkB,EAAE,CAClB,WAAW,EAAE,oBAAoB,KAC9B,OAAO,CACV;QACE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC;QACpC;YAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE;KAC9D,CACF,CAAC;IAEF;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAC3B,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAAG,SAAS,CACjD,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,OAAO,CAcT;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,oBAAoB,EAAE,OAAO,GAC5B,sBAAsB,CAmCxB"}
1
+ {"version":3,"file":"requestSnaps.d.cts","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EAEpB,sBAAsB,EACvB,wCAAwC;AAEzC,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EACnB,4BAA4B;AAC7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,8BAA8B;AAsBpE;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CACtD,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,CAKnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,YAAY,EAAE,CACZ,cAAc,EAAE,kBAAkB,KAC/B,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEjC;;;;OAIG;IACH,kBAAkB,EAAE,CAClB,WAAW,EAAE,oBAAoB,KAC9B,OAAO,CACV;QACE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC;QACpC;YAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE;KAC9D,CACF,CAAC;IAEF;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAC3B,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAAG,SAAS,CACjD,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,OAAO,CAcT;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,oBAAoB,EAAE,OAAO,GAC5B,sBAAsB,CAmCxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"requestSnaps.d.mts","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EAEpB,sBAAsB,EACvB,wCAAwC;AAEzC,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EACnB,4BAA4B;AAC7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,8BAA8B;AAqBpE;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CACtD,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,CAKnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,YAAY,EAAE,CACZ,cAAc,EAAE,kBAAkB,KAC/B,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEjC;;;;OAIG;IACH,kBAAkB,EAAE,CAClB,WAAW,EAAE,oBAAoB,KAC9B,OAAO,CACV;QACE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC;QACpC;YAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE;KAC9D,CACF,CAAC;IAEF;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAC3B,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAAG,SAAS,CACjD,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,OAAO,CAcT;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,oBAAoB,EAAE,OAAO,GAC5B,sBAAsB,CAmCxB"}
1
+ {"version":3,"file":"requestSnaps.d.mts","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EAEpB,sBAAsB,EACvB,wCAAwC;AAEzC,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EACnB,4BAA4B;AAC7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,8BAA8B;AAsBpE;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CACtD,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,CAKnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,YAAY,EAAE,CACZ,cAAc,EAAE,kBAAkB,KAC/B,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEjC;;;;OAIG;IACH,kBAAkB,EAAE,CAClB,WAAW,EAAE,oBAAoB,KAC9B,OAAO,CACV;QACE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC;QACpC;YAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE;KAC9D,CACF,CAAC;IAEF;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAC3B,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAAG,SAAS,CACjD,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,OAAO,CAcT;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,EACzD,oBAAoB,EAAE,OAAO,GAC5B,sBAAsB,CAmCxB"}
@@ -1,6 +1,7 @@
1
1
  import { rpcErrors } from "@metamask/rpc-errors";
2
2
  import { SnapCaveatType, verifyRequestedSnapPermissions } from "@metamask/snaps-utils";
3
3
  import { hasProperty, isObject } from "@metamask/utils";
4
+ import { Mutex } from "async-mutex";
4
5
  import { WALLET_SNAP_PERMISSION_KEY } from "../restricted/invokeSnap.mjs";
5
6
  const hookNames = {
6
7
  installSnaps: true,
@@ -59,6 +60,19 @@ export function getSnapPermissionsRequest(existingPermissions, requestedPermissi
59
60
  mergedCaveatValue;
60
61
  return requestedPermissions;
61
62
  }
63
+ const mutexes = new Map();
64
+ /**
65
+ * Get the corresponding Snap installation mutex for a given origin.
66
+ *
67
+ * @param origin - The origin of the request.
68
+ * @returns A mutex for that specific origin.
69
+ */
70
+ function getMutex(origin) {
71
+ if (!mutexes.has(origin)) {
72
+ mutexes.set(origin, new Mutex());
73
+ }
74
+ return mutexes.get(origin);
75
+ }
62
76
  /**
63
77
  * The `wallet_requestSnaps` method implementation.
64
78
  * Tries to install the requested snaps and adds them to the JSON-RPC response.
@@ -83,34 +97,41 @@ async function requestSnapsImplementation(req, res, _next, end, { installSnaps,
83
97
  message: '"params" must be an object.',
84
98
  }));
85
99
  }
86
- try {
87
- if (Object.keys(requestedSnaps).length === 0) {
88
- return end(rpcErrors.invalidParams({
89
- message: 'Request must have at least one requested snap.',
90
- }));
91
- }
92
- const requestedPermissions = {
93
- [WALLET_SNAP_PERMISSION_KEY]: {
94
- caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }],
95
- },
96
- };
97
- const existingPermissions = await getPermissions();
98
- if (!existingPermissions) {
99
- const [, metadata] = await requestPermissions(requestedPermissions);
100
- res.result = metadata.data[WALLET_SNAP_PERMISSION_KEY];
101
- }
102
- else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {
103
- res.result = await installSnaps(requestedSnaps);
100
+ if (Object.keys(requestedSnaps).length === 0) {
101
+ return end(rpcErrors.invalidParams({
102
+ message: 'Request must have at least one requested snap.',
103
+ }));
104
+ }
105
+ // We expect the MM middleware stack to always add the origin to requests
106
+ const { origin } = req;
107
+ const mutex = getMutex(origin);
108
+ // Process requests sequentially for each origin as permissions need to be merged
109
+ // for every request.
110
+ await mutex.runExclusive(async () => {
111
+ try {
112
+ const requestedPermissions = {
113
+ [WALLET_SNAP_PERMISSION_KEY]: {
114
+ caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }],
115
+ },
116
+ };
117
+ const existingPermissions = await getPermissions();
118
+ if (!existingPermissions) {
119
+ const [, metadata] = await requestPermissions(requestedPermissions);
120
+ res.result = metadata.data[WALLET_SNAP_PERMISSION_KEY];
121
+ }
122
+ else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {
123
+ res.result = await installSnaps(requestedSnaps);
124
+ }
125
+ else {
126
+ const mergedPermissionsRequest = getSnapPermissionsRequest(existingPermissions, requestedPermissions);
127
+ const [, metadata] = await requestPermissions(mergedPermissionsRequest);
128
+ res.result = metadata.data[WALLET_SNAP_PERMISSION_KEY];
129
+ }
104
130
  }
105
- else {
106
- const mergedPermissionsRequest = getSnapPermissionsRequest(existingPermissions, requestedPermissions);
107
- const [, metadata] = await requestPermissions(mergedPermissionsRequest);
108
- res.result = metadata.data[WALLET_SNAP_PERMISSION_KEY];
131
+ catch (error) {
132
+ res.error = error;
109
133
  }
110
- }
111
- catch (error) {
112
- res.error = error;
113
- }
134
+ });
114
135
  return end();
115
136
  }
116
137
  //# sourceMappingURL=requestSnaps.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"requestSnaps.mjs","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type {\n PermissionConstraint,\n RequestedPermissions,\n Caveat,\n PermittedHandlerExport,\n} from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type {\n RequestSnapsParams,\n RequestSnapsResult,\n} from '@metamask/snaps-sdk';\nimport type { SnapsPermissionRequest } from '@metamask/snaps-utils';\nimport {\n SnapCaveatType,\n verifyRequestedSnapPermissions,\n} from '@metamask/snaps-utils';\nimport type {\n JsonRpcRequest,\n PendingJsonRpcResponse,\n Json,\n} from '@metamask/utils';\nimport { hasProperty, isObject } from '@metamask/utils';\n\nimport { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap';\nimport type { MethodHooksObject } from '../utils';\n\nconst hookNames: MethodHooksObject<RequestSnapsHooks> = {\n installSnaps: true,\n requestPermissions: true,\n getPermissions: true,\n};\n\n/**\n * `wallet_requestSnaps` installs the requested Snaps and requests permission to use them if necessary.\n */\nexport const requestSnapsHandler: PermittedHandlerExport<\n RequestSnapsHooks,\n RequestSnapsParams,\n RequestSnapsResult\n> = {\n methodNames: ['wallet_requestSnaps'],\n implementation: requestSnapsImplementation,\n hookNames,\n};\n\nexport type RequestSnapsHooks = {\n /**\n * Installs the requested snaps if they are permitted.\n */\n installSnaps: (\n requestedSnaps: RequestSnapsParams,\n ) => Promise<RequestSnapsResult>;\n\n /**\n * Initiates a permission request for the requesting origin.\n *\n * @returns The result of the permissions request.\n */\n requestPermissions: (\n permissions: RequestedPermissions,\n ) => Promise<\n [\n Record<string, PermissionConstraint>,\n { data: Record<string, unknown>; id: string; origin: string },\n ]\n >;\n\n /**\n * Gets the current permissions for the requesting origin.\n *\n * @returns The current permissions of the requesting origin.\n */\n getPermissions: () => Promise<\n Record<string, PermissionConstraint> | undefined\n >;\n};\n\n/**\n * Checks whether an origin has existing `wallet_snap` permission and\n * whether or not it has the requested snapIds caveat.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedSnaps - The requested snaps.\n * @returns True if the existing permissions satisfy the requested snaps, otherwise false.\n */\nexport function hasRequestedSnaps(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedSnaps: Record<string, unknown>,\n): boolean {\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ]?.caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = snapIdCaveat?.value;\n if (isObject(permittedSnaps)) {\n return Object.keys(requestedSnaps).every((requestedSnap) =>\n hasProperty(permittedSnaps, requestedSnap),\n );\n }\n return false;\n}\n\n/**\n * Constructs a valid permission request with merged caveats based on existing permissions\n * and the requested snaps.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedPermissions - The permission request passed into `requestPermissions`.\n * @returns `requestedPermissions`.\n */\nexport function getSnapPermissionsRequest(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedPermissions: unknown,\n): SnapsPermissionRequest {\n verifyRequestedSnapPermissions(requestedPermissions);\n\n if (!existingPermissions[WALLET_SNAP_PERMISSION_KEY]) {\n return requestedPermissions;\n }\n\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ].caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = (snapIdCaveat?.value as Record<string, Json>) ?? {};\n\n const requestedSnaps =\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value;\n\n const snapIdSet = new Set([\n ...Object.keys(permittedSnaps),\n ...Object.keys(requestedSnaps),\n ]);\n\n const mergedCaveatValue = [...snapIdSet].reduce<Record<string, Json>>(\n (request, snapId) => {\n request[snapId] = requestedSnaps[snapId] ?? permittedSnaps[snapId];\n return request;\n },\n {},\n );\n\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value =\n mergedCaveatValue;\n\n return requestedPermissions;\n}\n\n/**\n * The `wallet_requestSnaps` method implementation.\n * Tries to install the requested snaps and adds them to the JSON-RPC response.\n *\n * @param req - The JSON-RPC request object.\n * @param res - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.installSnaps - A function that tries to install a given snap, prompting the user if necessary.\n * @param hooks.requestPermissions - A function that requests permissions on\n * behalf of a subject.\n * @param hooks.getPermissions - A function that gets the current permissions.\n * @returns A promise that resolves once the JSON-RPC response has been modified.\n * @throws If the params are invalid.\n */\nasync function requestSnapsImplementation(\n req: JsonRpcRequest<RequestSnapsParams>,\n res: PendingJsonRpcResponse<RequestSnapsResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n { installSnaps, requestPermissions, getPermissions }: RequestSnapsHooks,\n): Promise<void> {\n const requestedSnaps = req.params;\n if (!isObject(requestedSnaps)) {\n return end(\n rpcErrors.invalidParams({\n message: '\"params\" must be an object.',\n }),\n );\n }\n\n try {\n if (Object.keys(requestedSnaps).length === 0) {\n return end(\n rpcErrors.invalidParams({\n message: 'Request must have at least one requested snap.',\n }),\n );\n }\n\n const requestedPermissions = {\n [WALLET_SNAP_PERMISSION_KEY]: {\n caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }],\n },\n } as RequestedPermissions;\n const existingPermissions = await getPermissions();\n\n if (!existingPermissions) {\n const [, metadata] = await requestPermissions(requestedPermissions);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n } else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {\n res.result = await installSnaps(requestedSnaps);\n } else {\n const mergedPermissionsRequest = getSnapPermissionsRequest(\n existingPermissions,\n requestedPermissions,\n );\n\n const [, metadata] = await requestPermissions(mergedPermissionsRequest);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n }\n } catch (error) {\n res.error = error;\n }\n\n return end();\n}\n"]}
1
+ {"version":3,"file":"requestSnaps.mjs","sourceRoot":"","sources":["../../src/permitted/requestSnaps.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type {\n PermissionConstraint,\n RequestedPermissions,\n Caveat,\n PermittedHandlerExport,\n} from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type {\n RequestSnapsParams,\n RequestSnapsResult,\n} from '@metamask/snaps-sdk';\nimport type { SnapsPermissionRequest } from '@metamask/snaps-utils';\nimport {\n SnapCaveatType,\n verifyRequestedSnapPermissions,\n} from '@metamask/snaps-utils';\nimport type {\n JsonRpcRequest,\n PendingJsonRpcResponse,\n Json,\n} from '@metamask/utils';\nimport { hasProperty, isObject } from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\n\nimport { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap';\nimport type { MethodHooksObject } from '../utils';\n\nconst hookNames: MethodHooksObject<RequestSnapsHooks> = {\n installSnaps: true,\n requestPermissions: true,\n getPermissions: true,\n};\n\n/**\n * `wallet_requestSnaps` installs the requested Snaps and requests permission to use them if necessary.\n */\nexport const requestSnapsHandler: PermittedHandlerExport<\n RequestSnapsHooks,\n RequestSnapsParams,\n RequestSnapsResult\n> = {\n methodNames: ['wallet_requestSnaps'],\n implementation: requestSnapsImplementation,\n hookNames,\n};\n\nexport type RequestSnapsHooks = {\n /**\n * Installs the requested snaps if they are permitted.\n */\n installSnaps: (\n requestedSnaps: RequestSnapsParams,\n ) => Promise<RequestSnapsResult>;\n\n /**\n * Initiates a permission request for the requesting origin.\n *\n * @returns The result of the permissions request.\n */\n requestPermissions: (\n permissions: RequestedPermissions,\n ) => Promise<\n [\n Record<string, PermissionConstraint>,\n { data: Record<string, unknown>; id: string; origin: string },\n ]\n >;\n\n /**\n * Gets the current permissions for the requesting origin.\n *\n * @returns The current permissions of the requesting origin.\n */\n getPermissions: () => Promise<\n Record<string, PermissionConstraint> | undefined\n >;\n};\n\n/**\n * Checks whether an origin has existing `wallet_snap` permission and\n * whether or not it has the requested snapIds caveat.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedSnaps - The requested snaps.\n * @returns True if the existing permissions satisfy the requested snaps, otherwise false.\n */\nexport function hasRequestedSnaps(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedSnaps: Record<string, unknown>,\n): boolean {\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ]?.caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = snapIdCaveat?.value;\n if (isObject(permittedSnaps)) {\n return Object.keys(requestedSnaps).every((requestedSnap) =>\n hasProperty(permittedSnaps, requestedSnap),\n );\n }\n return false;\n}\n\n/**\n * Constructs a valid permission request with merged caveats based on existing permissions\n * and the requested snaps.\n *\n * @param existingPermissions - The existing permissions for the origin.\n * @param requestedPermissions - The permission request passed into `requestPermissions`.\n * @returns `requestedPermissions`.\n */\nexport function getSnapPermissionsRequest(\n existingPermissions: Record<string, PermissionConstraint>,\n requestedPermissions: unknown,\n): SnapsPermissionRequest {\n verifyRequestedSnapPermissions(requestedPermissions);\n\n if (!existingPermissions[WALLET_SNAP_PERMISSION_KEY]) {\n return requestedPermissions;\n }\n\n const snapIdCaveat = existingPermissions[\n WALLET_SNAP_PERMISSION_KEY\n ].caveats?.find(\n (caveat: Caveat<string, Json>) => caveat.type === SnapCaveatType.SnapIds,\n );\n\n const permittedSnaps = (snapIdCaveat?.value as Record<string, Json>) ?? {};\n\n const requestedSnaps =\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value;\n\n const snapIdSet = new Set([\n ...Object.keys(permittedSnaps),\n ...Object.keys(requestedSnaps),\n ]);\n\n const mergedCaveatValue = [...snapIdSet].reduce<Record<string, Json>>(\n (request, snapId) => {\n request[snapId] = requestedSnaps[snapId] ?? permittedSnaps[snapId];\n return request;\n },\n {},\n );\n\n requestedPermissions[WALLET_SNAP_PERMISSION_KEY].caveats[0].value =\n mergedCaveatValue;\n\n return requestedPermissions;\n}\n\nconst mutexes = new Map();\n\n/**\n * Get the corresponding Snap installation mutex for a given origin.\n *\n * @param origin - The origin of the request.\n * @returns A mutex for that specific origin.\n */\nfunction getMutex(origin: string) {\n if (!mutexes.has(origin)) {\n mutexes.set(origin, new Mutex());\n }\n return mutexes.get(origin);\n}\n\n/**\n * The `wallet_requestSnaps` method implementation.\n * Tries to install the requested snaps and adds them to the JSON-RPC response.\n *\n * @param req - The JSON-RPC request object.\n * @param res - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.installSnaps - A function that tries to install a given snap, prompting the user if necessary.\n * @param hooks.requestPermissions - A function that requests permissions on\n * behalf of a subject.\n * @param hooks.getPermissions - A function that gets the current permissions.\n * @returns A promise that resolves once the JSON-RPC response has been modified.\n * @throws If the params are invalid.\n */\nasync function requestSnapsImplementation(\n req: JsonRpcRequest<RequestSnapsParams>,\n res: PendingJsonRpcResponse<RequestSnapsResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n { installSnaps, requestPermissions, getPermissions }: RequestSnapsHooks,\n): Promise<void> {\n const requestedSnaps = req.params;\n if (!isObject(requestedSnaps)) {\n return end(\n rpcErrors.invalidParams({\n message: '\"params\" must be an object.',\n }),\n );\n }\n\n if (Object.keys(requestedSnaps).length === 0) {\n return end(\n rpcErrors.invalidParams({\n message: 'Request must have at least one requested snap.',\n }),\n );\n }\n\n // We expect the MM middleware stack to always add the origin to requests\n const { origin } = req as JsonRpcRequest & { origin: string };\n\n const mutex = getMutex(origin);\n\n // Process requests sequentially for each origin as permissions need to be merged\n // for every request.\n await mutex.runExclusive(async () => {\n try {\n const requestedPermissions = {\n [WALLET_SNAP_PERMISSION_KEY]: {\n caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }],\n },\n } as RequestedPermissions;\n const existingPermissions = await getPermissions();\n\n if (!existingPermissions) {\n const [, metadata] = await requestPermissions(requestedPermissions);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n } else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) {\n res.result = await installSnaps(requestedSnaps);\n } else {\n const mergedPermissionsRequest = getSnapPermissionsRequest(\n existingPermissions,\n requestedPermissions,\n );\n\n const [, metadata] = await requestPermissions(mergedPermissionsRequest);\n res.result = metadata.data[\n WALLET_SNAP_PERMISSION_KEY\n ] as RequestSnapsResult;\n }\n } catch (error) {\n res.error = error;\n }\n });\n\n return end();\n}\n"]}
@@ -5,6 +5,7 @@ const rpc_errors_1 = require("@metamask/rpc-errors");
5
5
  const snaps_utils_1 = require("@metamask/snaps-utils");
6
6
  const superstruct_1 = require("@metamask/superstruct");
7
7
  const utils_1 = require("@metamask/utils");
8
+ const async_mutex_1 = require("async-mutex");
8
9
  const manageState_1 = require("../restricted/manageState.cjs");
9
10
  const utils_2 = require("../utils.cjs");
10
11
  const hookNames = {
@@ -22,6 +23,19 @@ exports.setStateHandler = {
22
23
  implementation: setStateImplementation,
23
24
  hookNames,
24
25
  };
26
+ const mutexes = new Map();
27
+ /**
28
+ * Get the corresponding state modification mutex for a given Snap ID.
29
+ *
30
+ * @param snapId - The Snap ID.
31
+ * @returns A mutex for that specific Snap.
32
+ */
33
+ function getMutex(snapId) {
34
+ if (!mutexes.has(snapId)) {
35
+ mutexes.set(snapId, new async_mutex_1.Mutex());
36
+ }
37
+ return mutexes.get(snapId);
38
+ }
25
39
  const SetStateParametersStruct = (0, superstruct_1.object)({
26
40
  key: (0, superstruct_1.optional)(utils_2.StateKeyStruct),
27
41
  value: utils_1.JsonStruct,
@@ -58,19 +72,26 @@ async function setStateImplementation(request, response, _next, end, { hasPermis
58
72
  if (encrypted) {
59
73
  await getUnlockPromise(true);
60
74
  }
61
- const newState = await getNewState(key, value, encrypted, getSnapState);
62
- const snap = getSnap(request.origin);
63
- if (!snap?.preinstalled) {
64
- // We know that the state is valid JSON as per previous validation.
65
- const size = (0, snaps_utils_1.getJsonSizeUnsafe)(newState, true);
66
- if (size > manageState_1.STORAGE_SIZE_LIMIT) {
67
- throw rpc_errors_1.rpcErrors.invalidParams({
68
- message: `Invalid params: The new state must not exceed ${manageState_1.STORAGE_SIZE_LIMIT / 1000000} MB in size.`,
69
- });
75
+ const snapId = request.origin;
76
+ const mutex = getMutex(snapId);
77
+ // The expectation when using `snap_setState` is for the operation to safe
78
+ // to do in parallel. The mutex ensures that and prevents a bug that was
79
+ // mostly prevalent on mobile and caused data loss.
80
+ await mutex.runExclusive(async () => {
81
+ const newState = await getNewState(key, value, encrypted, getSnapState);
82
+ const snap = getSnap(snapId);
83
+ if (!snap?.preinstalled) {
84
+ // We know that the state is valid JSON as per previous validation.
85
+ const size = (0, snaps_utils_1.getJsonSizeUnsafe)(newState, true);
86
+ if (size > manageState_1.STORAGE_SIZE_LIMIT) {
87
+ throw rpc_errors_1.rpcErrors.invalidParams({
88
+ message: `Invalid params: The new state must not exceed ${manageState_1.STORAGE_SIZE_LIMIT / 1000000} MB in size.`,
89
+ });
90
+ }
70
91
  }
71
- }
72
- await updateSnapState(newState, encrypted);
73
- response.result = null;
92
+ await updateSnapState(newState, encrypted);
93
+ response.result = null;
94
+ });
74
95
  }
75
96
  catch (error) {
76
97
  return end(error);
@@ -1 +1 @@
1
- {"version":3,"file":"setState.cjs","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type { PermittedHandlerExport } from '@metamask/permission-controller';\nimport { providerErrors, rpcErrors } from '@metamask/rpc-errors';\nimport type { SetStateParams, SetStateResult } from '@metamask/snaps-sdk';\nimport type { JsonObject } from '@metamask/snaps-sdk/jsx';\nimport {\n getJsonSizeUnsafe,\n type InferMatching,\n type Snap,\n} from '@metamask/snaps-utils';\nimport {\n boolean,\n create,\n object as objectStruct,\n optional,\n StructError,\n} from '@metamask/superstruct';\nimport type {\n PendingJsonRpcResponse,\n Json,\n JsonRpcRequest,\n} from '@metamask/utils';\nimport { hasProperty, isObject, assert, JsonStruct } from '@metamask/utils';\n\nimport {\n manageStateBuilder,\n STORAGE_SIZE_LIMIT,\n} from '../restricted/manageState';\nimport type { MethodHooksObject } from '../utils';\nimport { FORBIDDEN_KEYS, StateKeyStruct } from '../utils';\n\nconst hookNames: MethodHooksObject<SetStateHooks> = {\n hasPermission: true,\n getSnapState: true,\n getUnlockPromise: true,\n updateSnapState: true,\n getSnap: true,\n};\n\n/**\n * `snap_setState` sets the state of the Snap.\n */\nexport const setStateHandler: PermittedHandlerExport<\n SetStateHooks,\n SetStateParameters,\n SetStateResult\n> = {\n methodNames: ['snap_setState'],\n implementation: setStateImplementation,\n hookNames,\n};\n\nexport type SetStateHooks = {\n /**\n * Check if the requesting origin has a given permission.\n *\n * @param permissionName - The name of the permission to check.\n * @returns Whether the origin has the permission.\n */\n hasPermission: (permissionName: string) => boolean;\n\n /**\n * Get the state of the requesting Snap.\n *\n * @param encrypted - Whether the state is encrypted.\n * @returns The current state of the Snap.\n */\n getSnapState: (encrypted: boolean) => Promise<Record<string, Json>>;\n\n /**\n * Wait 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 * Update the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n * @param encrypted - Whether the state should be encrypted.\n */\n updateSnapState: (\n newState: Record<string, Json>,\n encrypted: boolean,\n ) => Promise<void>;\n\n /**\n * Get Snap metadata.\n *\n * @param snapId - The ID of a Snap.\n */\n getSnap: (snapId: string) => Snap | undefined;\n};\n\nconst SetStateParametersStruct = objectStruct({\n key: optional(StateKeyStruct),\n value: JsonStruct,\n encrypted: optional(boolean()),\n});\n\nexport type SetStateParameters = InferMatching<\n typeof SetStateParametersStruct,\n SetStateParams\n>;\n\n/**\n * The `snap_setState` method implementation.\n *\n * @param request - The JSON-RPC request object.\n * @param response - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.hasPermission - Check whether a given origin has a given\n * permission.\n * @param hooks.getSnapState - Get the state of the requesting Snap.\n * @param hooks.getUnlockPromise - Wait for the extension to be unlocked.\n * @param hooks.updateSnapState - Update the state of the requesting Snap.\n * @param hooks.getSnap - The hook function to get Snap metadata.\n * @returns Nothing.\n */\nasync function setStateImplementation(\n request: JsonRpcRequest<SetStateParameters>,\n response: PendingJsonRpcResponse<SetStateResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n {\n hasPermission,\n getSnapState,\n getUnlockPromise,\n updateSnapState,\n getSnap,\n }: SetStateHooks,\n): Promise<void> {\n const { params } = request;\n\n if (!hasPermission(manageStateBuilder.targetName)) {\n return end(providerErrors.unauthorized());\n }\n\n try {\n const validatedParams = getValidatedParams(params);\n const { key, value, encrypted = true } = validatedParams;\n\n if (key === undefined && !isObject(value)) {\n return end(\n rpcErrors.invalidParams(\n 'Invalid params: Value must be an object if key is not provided.',\n ),\n );\n }\n\n if (encrypted) {\n await getUnlockPromise(true);\n }\n\n const newState = await getNewState(key, value, encrypted, getSnapState);\n\n const snap = getSnap(\n (request as JsonRpcRequest<SetStateParams> & { origin: string }).origin,\n );\n\n if (!snap?.preinstalled) {\n // We know that the state is valid JSON as per previous validation.\n const size = getJsonSizeUnsafe(newState, true);\n if (size > STORAGE_SIZE_LIMIT) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: The new state must not exceed ${\n STORAGE_SIZE_LIMIT / 1_000_000\n } MB in size.`,\n });\n }\n }\n\n await updateSnapState(newState, encrypted);\n response.result = null;\n } catch (error) {\n return end(error);\n }\n\n return end();\n}\n\n/**\n * Validate the parameters of the `snap_setState` method.\n *\n * @param params - The parameters to validate.\n * @returns The validated parameters.\n */\nfunction getValidatedParams(params?: unknown) {\n try {\n return create(params, SetStateParametersStruct);\n } catch (error) {\n if (error instanceof StructError) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: ${error.message}.`,\n });\n }\n\n /* istanbul ignore next */\n throw rpcErrors.internal();\n }\n}\n\n/**\n * Get the new state of the Snap.\n *\n * If the key is `undefined`, the value is expected to be an object. In this\n * case, the value is returned as the new state.\n *\n * If the key is not `undefined`, the value is set in the state at the key. If\n * the key does not exist, it is created (and any missing intermediate keys are\n * created as well).\n *\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @param encrypted - Whether the state is encrypted.\n * @param getSnapState - The `getSnapState` hook.\n * @returns The new state of the Snap.\n */\nasync function getNewState(\n key: string | undefined,\n value: Json,\n encrypted: boolean,\n getSnapState: SetStateHooks['getSnapState'],\n) {\n if (key === undefined) {\n assert(isObject(value));\n return value;\n }\n\n const state = await getSnapState(encrypted);\n return set(state, key, value);\n}\n\n/**\n * Set the value of a key in an object. The key may contain Lodash-style path\n * syntax, e.g., `a.b.c` (with the exception of array syntax). If the key does\n * not exist, it is created (and any missing intermediate keys are created as\n * well).\n *\n * This is a simplified version of Lodash's `set` function, but Lodash doesn't\n * seem to be maintained anymore, so we're using our own implementation.\n *\n * @param object - The object to get the key from.\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @returns The new object with the key set to the value.\n */\nexport function set(\n object: Record<string, Json> | null,\n key: string,\n value: Json,\n): JsonObject {\n const keys = key.split('.');\n const requiredObject = object ?? {};\n let currentObject: Record<string, Json> = requiredObject;\n\n for (let i = 0; i < keys.length; i++) {\n const currentKey = keys[i];\n if (FORBIDDEN_KEYS.includes(currentKey)) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Key contains forbidden characters.',\n );\n }\n\n if (i === keys.length - 1) {\n currentObject[currentKey] = value;\n return requiredObject;\n }\n\n if (\n !hasProperty(currentObject, currentKey) ||\n currentObject[currentKey] === null\n ) {\n currentObject[currentKey] = {};\n } else if (!isObject(currentObject[currentKey])) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Cannot overwrite non-object value.',\n );\n }\n\n currentObject = currentObject[currentKey] as Record<string, Json>;\n }\n\n // This should never be reached.\n /* istanbul ignore next */\n throw new Error('Unexpected error while setting the state.');\n}\n"]}
1
+ {"version":3,"file":"setState.cjs","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type { PermittedHandlerExport } from '@metamask/permission-controller';\nimport { providerErrors, rpcErrors } from '@metamask/rpc-errors';\nimport type {\n SetStateParams,\n SetStateResult,\n SnapId,\n} from '@metamask/snaps-sdk';\nimport type { JsonObject } from '@metamask/snaps-sdk/jsx';\nimport {\n getJsonSizeUnsafe,\n type InferMatching,\n type Snap,\n} from '@metamask/snaps-utils';\nimport {\n boolean,\n create,\n object as objectStruct,\n optional,\n StructError,\n} from '@metamask/superstruct';\nimport type {\n PendingJsonRpcResponse,\n Json,\n JsonRpcRequest,\n} from '@metamask/utils';\nimport { hasProperty, isObject, assert, JsonStruct } from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\n\nimport {\n manageStateBuilder,\n STORAGE_SIZE_LIMIT,\n} from '../restricted/manageState';\nimport type { MethodHooksObject } from '../utils';\nimport { FORBIDDEN_KEYS, StateKeyStruct } from '../utils';\n\nconst hookNames: MethodHooksObject<SetStateHooks> = {\n hasPermission: true,\n getSnapState: true,\n getUnlockPromise: true,\n updateSnapState: true,\n getSnap: true,\n};\n\n/**\n * `snap_setState` sets the state of the Snap.\n */\nexport const setStateHandler: PermittedHandlerExport<\n SetStateHooks,\n SetStateParameters,\n SetStateResult\n> = {\n methodNames: ['snap_setState'],\n implementation: setStateImplementation,\n hookNames,\n};\n\nexport type SetStateHooks = {\n /**\n * Check if the requesting origin has a given permission.\n *\n * @param permissionName - The name of the permission to check.\n * @returns Whether the origin has the permission.\n */\n hasPermission: (permissionName: string) => boolean;\n\n /**\n * Get the state of the requesting Snap.\n *\n * @param encrypted - Whether the state is encrypted.\n * @returns The current state of the Snap.\n */\n getSnapState: (encrypted: boolean) => Promise<Record<string, Json>>;\n\n /**\n * Wait 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 * Update the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n * @param encrypted - Whether the state should be encrypted.\n */\n updateSnapState: (\n newState: Record<string, Json>,\n encrypted: boolean,\n ) => Promise<void>;\n\n /**\n * Get Snap metadata.\n *\n * @param snapId - The ID of a Snap.\n */\n getSnap: (snapId: string) => Snap | undefined;\n};\n\nconst mutexes = new Map();\n\n/**\n * Get the corresponding state modification mutex for a given Snap ID.\n *\n * @param snapId - The Snap ID.\n * @returns A mutex for that specific Snap.\n */\nfunction getMutex(snapId: SnapId) {\n if (!mutexes.has(snapId)) {\n mutexes.set(snapId, new Mutex());\n }\n return mutexes.get(snapId);\n}\n\nconst SetStateParametersStruct = objectStruct({\n key: optional(StateKeyStruct),\n value: JsonStruct,\n encrypted: optional(boolean()),\n});\n\nexport type SetStateParameters = InferMatching<\n typeof SetStateParametersStruct,\n SetStateParams\n>;\n\n/**\n * The `snap_setState` method implementation.\n *\n * @param request - The JSON-RPC request object.\n * @param response - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.hasPermission - Check whether a given origin has a given\n * permission.\n * @param hooks.getSnapState - Get the state of the requesting Snap.\n * @param hooks.getUnlockPromise - Wait for the extension to be unlocked.\n * @param hooks.updateSnapState - Update the state of the requesting Snap.\n * @param hooks.getSnap - The hook function to get Snap metadata.\n * @returns Nothing.\n */\nasync function setStateImplementation(\n request: JsonRpcRequest<SetStateParameters>,\n response: PendingJsonRpcResponse<SetStateResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n {\n hasPermission,\n getSnapState,\n getUnlockPromise,\n updateSnapState,\n getSnap,\n }: SetStateHooks,\n): Promise<void> {\n const { params } = request;\n\n if (!hasPermission(manageStateBuilder.targetName)) {\n return end(providerErrors.unauthorized());\n }\n\n try {\n const validatedParams = getValidatedParams(params);\n const { key, value, encrypted = true } = validatedParams;\n\n if (key === undefined && !isObject(value)) {\n return end(\n rpcErrors.invalidParams(\n 'Invalid params: Value must be an object if key is not provided.',\n ),\n );\n }\n\n if (encrypted) {\n await getUnlockPromise(true);\n }\n\n const snapId = (\n request as JsonRpcRequest<SetStateParams> & { origin: string }\n ).origin as SnapId;\n\n const mutex = getMutex(snapId);\n\n // The expectation when using `snap_setState` is for the operation to safe\n // to do in parallel. The mutex ensures that and prevents a bug that was\n // mostly prevalent on mobile and caused data loss.\n await mutex.runExclusive(async () => {\n const newState = await getNewState(key, value, encrypted, getSnapState);\n\n const snap = getSnap(snapId);\n\n if (!snap?.preinstalled) {\n // We know that the state is valid JSON as per previous validation.\n const size = getJsonSizeUnsafe(newState, true);\n if (size > STORAGE_SIZE_LIMIT) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: The new state must not exceed ${\n STORAGE_SIZE_LIMIT / 1_000_000\n } MB in size.`,\n });\n }\n }\n\n await updateSnapState(newState, encrypted);\n response.result = null;\n });\n } catch (error) {\n return end(error);\n }\n\n return end();\n}\n\n/**\n * Validate the parameters of the `snap_setState` method.\n *\n * @param params - The parameters to validate.\n * @returns The validated parameters.\n */\nfunction getValidatedParams(params?: unknown) {\n try {\n return create(params, SetStateParametersStruct);\n } catch (error) {\n if (error instanceof StructError) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: ${error.message}.`,\n });\n }\n\n /* istanbul ignore next */\n throw rpcErrors.internal();\n }\n}\n\n/**\n * Get the new state of the Snap.\n *\n * If the key is `undefined`, the value is expected to be an object. In this\n * case, the value is returned as the new state.\n *\n * If the key is not `undefined`, the value is set in the state at the key. If\n * the key does not exist, it is created (and any missing intermediate keys are\n * created as well).\n *\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @param encrypted - Whether the state is encrypted.\n * @param getSnapState - The `getSnapState` hook.\n * @returns The new state of the Snap.\n */\nasync function getNewState(\n key: string | undefined,\n value: Json,\n encrypted: boolean,\n getSnapState: SetStateHooks['getSnapState'],\n) {\n if (key === undefined) {\n assert(isObject(value));\n return value;\n }\n\n const state = await getSnapState(encrypted);\n return set(state, key, value);\n}\n\n/**\n * Set the value of a key in an object. The key may contain Lodash-style path\n * syntax, e.g., `a.b.c` (with the exception of array syntax). If the key does\n * not exist, it is created (and any missing intermediate keys are created as\n * well).\n *\n * This is a simplified version of Lodash's `set` function, but Lodash doesn't\n * seem to be maintained anymore, so we're using our own implementation.\n *\n * @param object - The object to get the key from.\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @returns The new object with the key set to the value.\n */\nexport function set(\n object: Record<string, Json> | null,\n key: string,\n value: Json,\n): JsonObject {\n const keys = key.split('.');\n const requiredObject = object ?? {};\n let currentObject: Record<string, Json> = requiredObject;\n\n for (let i = 0; i < keys.length; i++) {\n const currentKey = keys[i];\n if (FORBIDDEN_KEYS.includes(currentKey)) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Key contains forbidden characters.',\n );\n }\n\n if (i === keys.length - 1) {\n currentObject[currentKey] = value;\n return requiredObject;\n }\n\n if (\n !hasProperty(currentObject, currentKey) ||\n currentObject[currentKey] === null\n ) {\n currentObject[currentKey] = {};\n } else if (!isObject(currentObject[currentKey])) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Cannot overwrite non-object value.',\n );\n }\n\n currentObject = currentObject[currentKey] as Record<string, Json>;\n }\n\n // This should never be reached.\n /* istanbul ignore next */\n throw new Error('Unexpected error while setting the state.');\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"setState.d.cts","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,wCAAwC;AAE9E,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,4BAA4B;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,gCAAgC;AAC1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,8BAA8B;AAQ/B,OAAO,KAAK,EAEV,IAAI,EAEL,wBAAwB;AAkBzB;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAClD,aAAa,EACb,kBAAkB,EAClB,cAAc,CAKf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC;IAEnD;;;;;OAKG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAEpE;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,uBAAuB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAC9B,SAAS,EAAE,OAAO,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnB;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;CAC/C,CAAC;AAEF,QAAA,MAAM,wBAAwB;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAC5C,OAAO,wBAAwB,EAC/B,cAAc,CACf,CAAC;AAqIF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,GAAG,CACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,EACnC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,IAAI,GACV,UAAU,CAmCZ"}
1
+ {"version":3,"file":"setState.d.cts","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,wCAAwC;AAE9E,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EAEf,4BAA4B;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,gCAAgC;AAC1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,8BAA8B;AAQ/B,OAAO,KAAK,EAEV,IAAI,EAEL,wBAAwB;AAmBzB;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAClD,aAAa,EACb,kBAAkB,EAClB,cAAc,CAKf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC;IAEnD;;;;;OAKG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAEpE;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,uBAAuB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAC9B,SAAS,EAAE,OAAO,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnB;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;CAC/C,CAAC;AAiBF,QAAA,MAAM,wBAAwB;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAC5C,OAAO,wBAAwB,EAC/B,cAAc,CACf,CAAC;AA8IF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,GAAG,CACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,EACnC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,IAAI,GACV,UAAU,CAmCZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"setState.d.mts","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,wCAAwC;AAE9E,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,4BAA4B;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,gCAAgC;AAC1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,8BAA8B;AAQ/B,OAAO,KAAK,EAEV,IAAI,EAEL,wBAAwB;AAkBzB;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAClD,aAAa,EACb,kBAAkB,EAClB,cAAc,CAKf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC;IAEnD;;;;;OAKG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAEpE;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,uBAAuB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAC9B,SAAS,EAAE,OAAO,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnB;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;CAC/C,CAAC;AAEF,QAAA,MAAM,wBAAwB;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAC5C,OAAO,wBAAwB,EAC/B,cAAc,CACf,CAAC;AAqIF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,GAAG,CACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,EACnC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,IAAI,GACV,UAAU,CAmCZ"}
1
+ {"version":3,"file":"setState.d.mts","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,wCAAwC;AAE9E,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EAEf,4BAA4B;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,gCAAgC;AAC1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,8BAA8B;AAQ/B,OAAO,KAAK,EAEV,IAAI,EAEL,wBAAwB;AAmBzB;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAClD,aAAa,EACb,kBAAkB,EAClB,cAAc,CAKf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC;IAEnD;;;;;OAKG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAEpE;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,uBAAuB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAC9B,SAAS,EAAE,OAAO,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnB;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;CAC/C,CAAC;AAiBF,QAAA,MAAM,wBAAwB;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAC5C,OAAO,wBAAwB,EAC/B,cAAc,CACf,CAAC;AA8IF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,GAAG,CACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,EACnC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,IAAI,GACV,UAAU,CAmCZ"}
@@ -2,6 +2,7 @@ import { providerErrors, rpcErrors } from "@metamask/rpc-errors";
2
2
  import { getJsonSizeUnsafe } from "@metamask/snaps-utils";
3
3
  import { boolean, create, object as objectStruct, optional, StructError } from "@metamask/superstruct";
4
4
  import { hasProperty, isObject, assert, JsonStruct } from "@metamask/utils";
5
+ import { Mutex } from "async-mutex";
5
6
  import { manageStateBuilder, STORAGE_SIZE_LIMIT } from "../restricted/manageState.mjs";
6
7
  import { FORBIDDEN_KEYS, StateKeyStruct } from "../utils.mjs";
7
8
  const hookNames = {
@@ -19,6 +20,19 @@ export const setStateHandler = {
19
20
  implementation: setStateImplementation,
20
21
  hookNames,
21
22
  };
23
+ const mutexes = new Map();
24
+ /**
25
+ * Get the corresponding state modification mutex for a given Snap ID.
26
+ *
27
+ * @param snapId - The Snap ID.
28
+ * @returns A mutex for that specific Snap.
29
+ */
30
+ function getMutex(snapId) {
31
+ if (!mutexes.has(snapId)) {
32
+ mutexes.set(snapId, new Mutex());
33
+ }
34
+ return mutexes.get(snapId);
35
+ }
22
36
  const SetStateParametersStruct = objectStruct({
23
37
  key: optional(StateKeyStruct),
24
38
  value: JsonStruct,
@@ -55,19 +69,26 @@ async function setStateImplementation(request, response, _next, end, { hasPermis
55
69
  if (encrypted) {
56
70
  await getUnlockPromise(true);
57
71
  }
58
- const newState = await getNewState(key, value, encrypted, getSnapState);
59
- const snap = getSnap(request.origin);
60
- if (!snap?.preinstalled) {
61
- // We know that the state is valid JSON as per previous validation.
62
- const size = getJsonSizeUnsafe(newState, true);
63
- if (size > STORAGE_SIZE_LIMIT) {
64
- throw rpcErrors.invalidParams({
65
- message: `Invalid params: The new state must not exceed ${STORAGE_SIZE_LIMIT / 1000000} MB in size.`,
66
- });
72
+ const snapId = request.origin;
73
+ const mutex = getMutex(snapId);
74
+ // The expectation when using `snap_setState` is for the operation to safe
75
+ // to do in parallel. The mutex ensures that and prevents a bug that was
76
+ // mostly prevalent on mobile and caused data loss.
77
+ await mutex.runExclusive(async () => {
78
+ const newState = await getNewState(key, value, encrypted, getSnapState);
79
+ const snap = getSnap(snapId);
80
+ if (!snap?.preinstalled) {
81
+ // We know that the state is valid JSON as per previous validation.
82
+ const size = getJsonSizeUnsafe(newState, true);
83
+ if (size > STORAGE_SIZE_LIMIT) {
84
+ throw rpcErrors.invalidParams({
85
+ message: `Invalid params: The new state must not exceed ${STORAGE_SIZE_LIMIT / 1000000} MB in size.`,
86
+ });
87
+ }
67
88
  }
68
- }
69
- await updateSnapState(newState, encrypted);
70
- response.result = null;
89
+ await updateSnapState(newState, encrypted);
90
+ response.result = null;
91
+ });
71
92
  }
72
93
  catch (error) {
73
94
  return end(error);
@@ -1 +1 @@
1
- {"version":3,"file":"setState.mjs","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type { PermittedHandlerExport } from '@metamask/permission-controller';\nimport { providerErrors, rpcErrors } from '@metamask/rpc-errors';\nimport type { SetStateParams, SetStateResult } from '@metamask/snaps-sdk';\nimport type { JsonObject } from '@metamask/snaps-sdk/jsx';\nimport {\n getJsonSizeUnsafe,\n type InferMatching,\n type Snap,\n} from '@metamask/snaps-utils';\nimport {\n boolean,\n create,\n object as objectStruct,\n optional,\n StructError,\n} from '@metamask/superstruct';\nimport type {\n PendingJsonRpcResponse,\n Json,\n JsonRpcRequest,\n} from '@metamask/utils';\nimport { hasProperty, isObject, assert, JsonStruct } from '@metamask/utils';\n\nimport {\n manageStateBuilder,\n STORAGE_SIZE_LIMIT,\n} from '../restricted/manageState';\nimport type { MethodHooksObject } from '../utils';\nimport { FORBIDDEN_KEYS, StateKeyStruct } from '../utils';\n\nconst hookNames: MethodHooksObject<SetStateHooks> = {\n hasPermission: true,\n getSnapState: true,\n getUnlockPromise: true,\n updateSnapState: true,\n getSnap: true,\n};\n\n/**\n * `snap_setState` sets the state of the Snap.\n */\nexport const setStateHandler: PermittedHandlerExport<\n SetStateHooks,\n SetStateParameters,\n SetStateResult\n> = {\n methodNames: ['snap_setState'],\n implementation: setStateImplementation,\n hookNames,\n};\n\nexport type SetStateHooks = {\n /**\n * Check if the requesting origin has a given permission.\n *\n * @param permissionName - The name of the permission to check.\n * @returns Whether the origin has the permission.\n */\n hasPermission: (permissionName: string) => boolean;\n\n /**\n * Get the state of the requesting Snap.\n *\n * @param encrypted - Whether the state is encrypted.\n * @returns The current state of the Snap.\n */\n getSnapState: (encrypted: boolean) => Promise<Record<string, Json>>;\n\n /**\n * Wait 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 * Update the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n * @param encrypted - Whether the state should be encrypted.\n */\n updateSnapState: (\n newState: Record<string, Json>,\n encrypted: boolean,\n ) => Promise<void>;\n\n /**\n * Get Snap metadata.\n *\n * @param snapId - The ID of a Snap.\n */\n getSnap: (snapId: string) => Snap | undefined;\n};\n\nconst SetStateParametersStruct = objectStruct({\n key: optional(StateKeyStruct),\n value: JsonStruct,\n encrypted: optional(boolean()),\n});\n\nexport type SetStateParameters = InferMatching<\n typeof SetStateParametersStruct,\n SetStateParams\n>;\n\n/**\n * The `snap_setState` method implementation.\n *\n * @param request - The JSON-RPC request object.\n * @param response - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.hasPermission - Check whether a given origin has a given\n * permission.\n * @param hooks.getSnapState - Get the state of the requesting Snap.\n * @param hooks.getUnlockPromise - Wait for the extension to be unlocked.\n * @param hooks.updateSnapState - Update the state of the requesting Snap.\n * @param hooks.getSnap - The hook function to get Snap metadata.\n * @returns Nothing.\n */\nasync function setStateImplementation(\n request: JsonRpcRequest<SetStateParameters>,\n response: PendingJsonRpcResponse<SetStateResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n {\n hasPermission,\n getSnapState,\n getUnlockPromise,\n updateSnapState,\n getSnap,\n }: SetStateHooks,\n): Promise<void> {\n const { params } = request;\n\n if (!hasPermission(manageStateBuilder.targetName)) {\n return end(providerErrors.unauthorized());\n }\n\n try {\n const validatedParams = getValidatedParams(params);\n const { key, value, encrypted = true } = validatedParams;\n\n if (key === undefined && !isObject(value)) {\n return end(\n rpcErrors.invalidParams(\n 'Invalid params: Value must be an object if key is not provided.',\n ),\n );\n }\n\n if (encrypted) {\n await getUnlockPromise(true);\n }\n\n const newState = await getNewState(key, value, encrypted, getSnapState);\n\n const snap = getSnap(\n (request as JsonRpcRequest<SetStateParams> & { origin: string }).origin,\n );\n\n if (!snap?.preinstalled) {\n // We know that the state is valid JSON as per previous validation.\n const size = getJsonSizeUnsafe(newState, true);\n if (size > STORAGE_SIZE_LIMIT) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: The new state must not exceed ${\n STORAGE_SIZE_LIMIT / 1_000_000\n } MB in size.`,\n });\n }\n }\n\n await updateSnapState(newState, encrypted);\n response.result = null;\n } catch (error) {\n return end(error);\n }\n\n return end();\n}\n\n/**\n * Validate the parameters of the `snap_setState` method.\n *\n * @param params - The parameters to validate.\n * @returns The validated parameters.\n */\nfunction getValidatedParams(params?: unknown) {\n try {\n return create(params, SetStateParametersStruct);\n } catch (error) {\n if (error instanceof StructError) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: ${error.message}.`,\n });\n }\n\n /* istanbul ignore next */\n throw rpcErrors.internal();\n }\n}\n\n/**\n * Get the new state of the Snap.\n *\n * If the key is `undefined`, the value is expected to be an object. In this\n * case, the value is returned as the new state.\n *\n * If the key is not `undefined`, the value is set in the state at the key. If\n * the key does not exist, it is created (and any missing intermediate keys are\n * created as well).\n *\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @param encrypted - Whether the state is encrypted.\n * @param getSnapState - The `getSnapState` hook.\n * @returns The new state of the Snap.\n */\nasync function getNewState(\n key: string | undefined,\n value: Json,\n encrypted: boolean,\n getSnapState: SetStateHooks['getSnapState'],\n) {\n if (key === undefined) {\n assert(isObject(value));\n return value;\n }\n\n const state = await getSnapState(encrypted);\n return set(state, key, value);\n}\n\n/**\n * Set the value of a key in an object. The key may contain Lodash-style path\n * syntax, e.g., `a.b.c` (with the exception of array syntax). If the key does\n * not exist, it is created (and any missing intermediate keys are created as\n * well).\n *\n * This is a simplified version of Lodash's `set` function, but Lodash doesn't\n * seem to be maintained anymore, so we're using our own implementation.\n *\n * @param object - The object to get the key from.\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @returns The new object with the key set to the value.\n */\nexport function set(\n object: Record<string, Json> | null,\n key: string,\n value: Json,\n): JsonObject {\n const keys = key.split('.');\n const requiredObject = object ?? {};\n let currentObject: Record<string, Json> = requiredObject;\n\n for (let i = 0; i < keys.length; i++) {\n const currentKey = keys[i];\n if (FORBIDDEN_KEYS.includes(currentKey)) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Key contains forbidden characters.',\n );\n }\n\n if (i === keys.length - 1) {\n currentObject[currentKey] = value;\n return requiredObject;\n }\n\n if (\n !hasProperty(currentObject, currentKey) ||\n currentObject[currentKey] === null\n ) {\n currentObject[currentKey] = {};\n } else if (!isObject(currentObject[currentKey])) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Cannot overwrite non-object value.',\n );\n }\n\n currentObject = currentObject[currentKey] as Record<string, Json>;\n }\n\n // This should never be reached.\n /* istanbul ignore next */\n throw new Error('Unexpected error while setting the state.');\n}\n"]}
1
+ {"version":3,"file":"setState.mjs","sourceRoot":"","sources":["../../src/permitted/setState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';\nimport type { PermittedHandlerExport } from '@metamask/permission-controller';\nimport { providerErrors, rpcErrors } from '@metamask/rpc-errors';\nimport type {\n SetStateParams,\n SetStateResult,\n SnapId,\n} from '@metamask/snaps-sdk';\nimport type { JsonObject } from '@metamask/snaps-sdk/jsx';\nimport {\n getJsonSizeUnsafe,\n type InferMatching,\n type Snap,\n} from '@metamask/snaps-utils';\nimport {\n boolean,\n create,\n object as objectStruct,\n optional,\n StructError,\n} from '@metamask/superstruct';\nimport type {\n PendingJsonRpcResponse,\n Json,\n JsonRpcRequest,\n} from '@metamask/utils';\nimport { hasProperty, isObject, assert, JsonStruct } from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\n\nimport {\n manageStateBuilder,\n STORAGE_SIZE_LIMIT,\n} from '../restricted/manageState';\nimport type { MethodHooksObject } from '../utils';\nimport { FORBIDDEN_KEYS, StateKeyStruct } from '../utils';\n\nconst hookNames: MethodHooksObject<SetStateHooks> = {\n hasPermission: true,\n getSnapState: true,\n getUnlockPromise: true,\n updateSnapState: true,\n getSnap: true,\n};\n\n/**\n * `snap_setState` sets the state of the Snap.\n */\nexport const setStateHandler: PermittedHandlerExport<\n SetStateHooks,\n SetStateParameters,\n SetStateResult\n> = {\n methodNames: ['snap_setState'],\n implementation: setStateImplementation,\n hookNames,\n};\n\nexport type SetStateHooks = {\n /**\n * Check if the requesting origin has a given permission.\n *\n * @param permissionName - The name of the permission to check.\n * @returns Whether the origin has the permission.\n */\n hasPermission: (permissionName: string) => boolean;\n\n /**\n * Get the state of the requesting Snap.\n *\n * @param encrypted - Whether the state is encrypted.\n * @returns The current state of the Snap.\n */\n getSnapState: (encrypted: boolean) => Promise<Record<string, Json>>;\n\n /**\n * Wait 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 * Update the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n * @param encrypted - Whether the state should be encrypted.\n */\n updateSnapState: (\n newState: Record<string, Json>,\n encrypted: boolean,\n ) => Promise<void>;\n\n /**\n * Get Snap metadata.\n *\n * @param snapId - The ID of a Snap.\n */\n getSnap: (snapId: string) => Snap | undefined;\n};\n\nconst mutexes = new Map();\n\n/**\n * Get the corresponding state modification mutex for a given Snap ID.\n *\n * @param snapId - The Snap ID.\n * @returns A mutex for that specific Snap.\n */\nfunction getMutex(snapId: SnapId) {\n if (!mutexes.has(snapId)) {\n mutexes.set(snapId, new Mutex());\n }\n return mutexes.get(snapId);\n}\n\nconst SetStateParametersStruct = objectStruct({\n key: optional(StateKeyStruct),\n value: JsonStruct,\n encrypted: optional(boolean()),\n});\n\nexport type SetStateParameters = InferMatching<\n typeof SetStateParametersStruct,\n SetStateParams\n>;\n\n/**\n * The `snap_setState` method implementation.\n *\n * @param request - The JSON-RPC request object.\n * @param response - The JSON-RPC response object.\n * @param _next - The `json-rpc-engine` \"next\" callback. Not used by this\n * function.\n * @param end - The `json-rpc-engine` \"end\" callback.\n * @param hooks - The RPC method hooks.\n * @param hooks.hasPermission - Check whether a given origin has a given\n * permission.\n * @param hooks.getSnapState - Get the state of the requesting Snap.\n * @param hooks.getUnlockPromise - Wait for the extension to be unlocked.\n * @param hooks.updateSnapState - Update the state of the requesting Snap.\n * @param hooks.getSnap - The hook function to get Snap metadata.\n * @returns Nothing.\n */\nasync function setStateImplementation(\n request: JsonRpcRequest<SetStateParameters>,\n response: PendingJsonRpcResponse<SetStateResult>,\n _next: unknown,\n end: JsonRpcEngineEndCallback,\n {\n hasPermission,\n getSnapState,\n getUnlockPromise,\n updateSnapState,\n getSnap,\n }: SetStateHooks,\n): Promise<void> {\n const { params } = request;\n\n if (!hasPermission(manageStateBuilder.targetName)) {\n return end(providerErrors.unauthorized());\n }\n\n try {\n const validatedParams = getValidatedParams(params);\n const { key, value, encrypted = true } = validatedParams;\n\n if (key === undefined && !isObject(value)) {\n return end(\n rpcErrors.invalidParams(\n 'Invalid params: Value must be an object if key is not provided.',\n ),\n );\n }\n\n if (encrypted) {\n await getUnlockPromise(true);\n }\n\n const snapId = (\n request as JsonRpcRequest<SetStateParams> & { origin: string }\n ).origin as SnapId;\n\n const mutex = getMutex(snapId);\n\n // The expectation when using `snap_setState` is for the operation to safe\n // to do in parallel. The mutex ensures that and prevents a bug that was\n // mostly prevalent on mobile and caused data loss.\n await mutex.runExclusive(async () => {\n const newState = await getNewState(key, value, encrypted, getSnapState);\n\n const snap = getSnap(snapId);\n\n if (!snap?.preinstalled) {\n // We know that the state is valid JSON as per previous validation.\n const size = getJsonSizeUnsafe(newState, true);\n if (size > STORAGE_SIZE_LIMIT) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: The new state must not exceed ${\n STORAGE_SIZE_LIMIT / 1_000_000\n } MB in size.`,\n });\n }\n }\n\n await updateSnapState(newState, encrypted);\n response.result = null;\n });\n } catch (error) {\n return end(error);\n }\n\n return end();\n}\n\n/**\n * Validate the parameters of the `snap_setState` method.\n *\n * @param params - The parameters to validate.\n * @returns The validated parameters.\n */\nfunction getValidatedParams(params?: unknown) {\n try {\n return create(params, SetStateParametersStruct);\n } catch (error) {\n if (error instanceof StructError) {\n throw rpcErrors.invalidParams({\n message: `Invalid params: ${error.message}.`,\n });\n }\n\n /* istanbul ignore next */\n throw rpcErrors.internal();\n }\n}\n\n/**\n * Get the new state of the Snap.\n *\n * If the key is `undefined`, the value is expected to be an object. In this\n * case, the value is returned as the new state.\n *\n * If the key is not `undefined`, the value is set in the state at the key. If\n * the key does not exist, it is created (and any missing intermediate keys are\n * created as well).\n *\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @param encrypted - Whether the state is encrypted.\n * @param getSnapState - The `getSnapState` hook.\n * @returns The new state of the Snap.\n */\nasync function getNewState(\n key: string | undefined,\n value: Json,\n encrypted: boolean,\n getSnapState: SetStateHooks['getSnapState'],\n) {\n if (key === undefined) {\n assert(isObject(value));\n return value;\n }\n\n const state = await getSnapState(encrypted);\n return set(state, key, value);\n}\n\n/**\n * Set the value of a key in an object. The key may contain Lodash-style path\n * syntax, e.g., `a.b.c` (with the exception of array syntax). If the key does\n * not exist, it is created (and any missing intermediate keys are created as\n * well).\n *\n * This is a simplified version of Lodash's `set` function, but Lodash doesn't\n * seem to be maintained anymore, so we're using our own implementation.\n *\n * @param object - The object to get the key from.\n * @param key - The key to set.\n * @param value - The value to set the key to.\n * @returns The new object with the key set to the value.\n */\nexport function set(\n object: Record<string, Json> | null,\n key: string,\n value: Json,\n): JsonObject {\n const keys = key.split('.');\n const requiredObject = object ?? {};\n let currentObject: Record<string, Json> = requiredObject;\n\n for (let i = 0; i < keys.length; i++) {\n const currentKey = keys[i];\n if (FORBIDDEN_KEYS.includes(currentKey)) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Key contains forbidden characters.',\n );\n }\n\n if (i === keys.length - 1) {\n currentObject[currentKey] = value;\n return requiredObject;\n }\n\n if (\n !hasProperty(currentObject, currentKey) ||\n currentObject[currentKey] === null\n ) {\n currentObject[currentKey] = {};\n } else if (!isObject(currentObject[currentKey])) {\n throw rpcErrors.invalidParams(\n 'Invalid params: Cannot overwrite non-object value.',\n );\n }\n\n currentObject = currentObject[currentKey] as Record<string, Json>;\n }\n\n // This should never be reached.\n /* istanbul ignore next */\n throw new Error('Unexpected error while setting the state.');\n}\n"]}