@metamask/snaps-controllers 12.3.1 → 13.1.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 (128) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/cronjob/CronjobController.cjs +250 -276
  3. package/dist/cronjob/CronjobController.cjs.map +1 -1
  4. package/dist/cronjob/CronjobController.d.cts +61 -78
  5. package/dist/cronjob/CronjobController.d.cts.map +1 -1
  6. package/dist/cronjob/CronjobController.d.mts +61 -78
  7. package/dist/cronjob/CronjobController.d.mts.map +1 -1
  8. package/dist/cronjob/CronjobController.mjs +251 -277
  9. package/dist/cronjob/CronjobController.mjs.map +1 -1
  10. package/dist/cronjob/utils.cjs +79 -0
  11. package/dist/cronjob/utils.cjs.map +1 -0
  12. package/dist/cronjob/utils.d.cts +25 -0
  13. package/dist/cronjob/utils.d.cts.map +1 -0
  14. package/dist/cronjob/utils.d.mts +25 -0
  15. package/dist/cronjob/utils.d.mts.map +1 -0
  16. package/dist/cronjob/utils.mjs +75 -0
  17. package/dist/cronjob/utils.mjs.map +1 -0
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -0
  21. package/dist/index.d.cts.map +1 -1
  22. package/dist/index.d.mts +1 -0
  23. package/dist/index.d.mts.map +1 -1
  24. package/dist/index.mjs +1 -0
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/insights/SnapInsightsController.cjs +199 -149
  27. package/dist/insights/SnapInsightsController.cjs.map +1 -1
  28. package/dist/insights/SnapInsightsController.mjs +198 -148
  29. package/dist/insights/SnapInsightsController.mjs.map +1 -1
  30. package/dist/interface/SnapInterfaceController.cjs +160 -101
  31. package/dist/interface/SnapInterfaceController.cjs.map +1 -1
  32. package/dist/interface/SnapInterfaceController.mjs +160 -101
  33. package/dist/interface/SnapInterfaceController.mjs.map +1 -1
  34. package/dist/multichain/MultichainRouter.cjs +117 -114
  35. package/dist/multichain/MultichainRouter.cjs.map +1 -1
  36. package/dist/multichain/MultichainRouter.mjs +117 -114
  37. package/dist/multichain/MultichainRouter.mjs.map +1 -1
  38. package/dist/services/AbstractExecutionService.cjs +131 -139
  39. package/dist/services/AbstractExecutionService.cjs.map +1 -1
  40. package/dist/services/AbstractExecutionService.mjs +131 -139
  41. package/dist/services/AbstractExecutionService.mjs.map +1 -1
  42. package/dist/services/ProxyPostMessageStream.cjs +19 -26
  43. package/dist/services/ProxyPostMessageStream.cjs.map +1 -1
  44. package/dist/services/ProxyPostMessageStream.mjs +19 -26
  45. package/dist/services/ProxyPostMessageStream.mjs.map +1 -1
  46. package/dist/services/iframe/IframeExecutionService.cjs +1 -0
  47. package/dist/services/iframe/IframeExecutionService.cjs.map +1 -1
  48. package/dist/services/iframe/IframeExecutionService.mjs +1 -0
  49. package/dist/services/iframe/IframeExecutionService.mjs.map +1 -1
  50. package/dist/services/offscreen/OffscreenExecutionService.cjs +3 -16
  51. package/dist/services/offscreen/OffscreenExecutionService.cjs.map +1 -1
  52. package/dist/services/offscreen/OffscreenExecutionService.mjs +3 -16
  53. package/dist/services/offscreen/OffscreenExecutionService.mjs.map +1 -1
  54. package/dist/services/proxy/ProxyExecutionService.cjs +4 -17
  55. package/dist/services/proxy/ProxyExecutionService.cjs.map +1 -1
  56. package/dist/services/proxy/ProxyExecutionService.mjs +4 -17
  57. package/dist/services/proxy/ProxyExecutionService.mjs.map +1 -1
  58. package/dist/services/webview/WebViewExecutionService.cjs +6 -19
  59. package/dist/services/webview/WebViewExecutionService.cjs.map +1 -1
  60. package/dist/services/webview/WebViewExecutionService.mjs +6 -19
  61. package/dist/services/webview/WebViewExecutionService.mjs.map +1 -1
  62. package/dist/services/webview/WebViewMessageStream.cjs +13 -26
  63. package/dist/services/webview/WebViewMessageStream.cjs.map +1 -1
  64. package/dist/services/webview/WebViewMessageStream.mjs +13 -26
  65. package/dist/services/webview/WebViewMessageStream.mjs.map +1 -1
  66. package/dist/snaps/SnapController.cjs +1432 -1204
  67. package/dist/snaps/SnapController.cjs.map +1 -1
  68. package/dist/snaps/SnapController.d.cts +20 -5
  69. package/dist/snaps/SnapController.d.cts.map +1 -1
  70. package/dist/snaps/SnapController.d.mts +20 -5
  71. package/dist/snaps/SnapController.d.mts.map +1 -1
  72. package/dist/snaps/SnapController.mjs +1432 -1204
  73. package/dist/snaps/SnapController.mjs.map +1 -1
  74. package/dist/snaps/Timer.cjs +4 -0
  75. package/dist/snaps/Timer.cjs.map +1 -1
  76. package/dist/snaps/Timer.mjs +4 -0
  77. package/dist/snaps/Timer.mjs.map +1 -1
  78. package/dist/snaps/constants.cjs +1 -0
  79. package/dist/snaps/constants.cjs.map +1 -1
  80. package/dist/snaps/constants.d.cts.map +1 -1
  81. package/dist/snaps/constants.d.mts.map +1 -1
  82. package/dist/snaps/constants.mjs +1 -0
  83. package/dist/snaps/constants.mjs.map +1 -1
  84. package/dist/snaps/index.cjs +1 -0
  85. package/dist/snaps/index.cjs.map +1 -1
  86. package/dist/snaps/index.d.cts +1 -0
  87. package/dist/snaps/index.d.cts.map +1 -1
  88. package/dist/snaps/index.d.mts +1 -0
  89. package/dist/snaps/index.d.mts.map +1 -1
  90. package/dist/snaps/index.mjs +1 -0
  91. package/dist/snaps/index.mjs.map +1 -1
  92. package/dist/snaps/location/http.cjs +20 -4
  93. package/dist/snaps/location/http.cjs.map +1 -1
  94. package/dist/snaps/location/http.mjs +20 -4
  95. package/dist/snaps/location/http.mjs.map +1 -1
  96. package/dist/snaps/location/local.cjs +4 -17
  97. package/dist/snaps/location/local.cjs.map +1 -1
  98. package/dist/snaps/location/local.mjs +4 -17
  99. package/dist/snaps/location/local.mjs.map +1 -1
  100. package/dist/snaps/location/npm.cjs +28 -48
  101. package/dist/snaps/location/npm.cjs.map +1 -1
  102. package/dist/snaps/location/npm.d.cts.map +1 -1
  103. package/dist/snaps/location/npm.d.mts.map +1 -1
  104. package/dist/snaps/location/npm.mjs +28 -48
  105. package/dist/snaps/location/npm.mjs.map +1 -1
  106. package/dist/snaps/registry/json.cjs +173 -166
  107. package/dist/snaps/registry/json.cjs.map +1 -1
  108. package/dist/snaps/registry/json.mjs +172 -165
  109. package/dist/snaps/registry/json.mjs.map +1 -1
  110. package/dist/utils.d.cts +1 -1
  111. package/dist/utils.d.mts +1 -1
  112. package/dist/websocket/WebSocketService.cjs +194 -0
  113. package/dist/websocket/WebSocketService.cjs.map +1 -0
  114. package/dist/websocket/WebSocketService.d.cts +33 -0
  115. package/dist/websocket/WebSocketService.d.cts.map +1 -0
  116. package/dist/websocket/WebSocketService.d.mts +33 -0
  117. package/dist/websocket/WebSocketService.d.mts.map +1 -0
  118. package/dist/websocket/WebSocketService.mjs +190 -0
  119. package/dist/websocket/WebSocketService.mjs.map +1 -0
  120. package/dist/websocket/index.cjs +18 -0
  121. package/dist/websocket/index.cjs.map +1 -0
  122. package/dist/websocket/index.d.cts +2 -0
  123. package/dist/websocket/index.d.cts.map +1 -0
  124. package/dist/websocket/index.d.mts +2 -0
  125. package/dist/websocket/index.d.mts.map +1 -0
  126. package/dist/websocket/index.mjs +2 -0
  127. package/dist/websocket/index.mjs.map +1 -0
  128. package/package.json +10 -10
@@ -1,22 +1,10 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _SnapController_instances, _SnapController_closeAllConnections, _SnapController_dynamicPermissions, _SnapController_environmentEndowmentPermissions, _SnapController_excludedPermissions, _SnapController_featureFlags, _SnapController_fetchFunction, _SnapController_idleTimeCheckInterval, _SnapController_maxIdleTime, _SnapController_encryptor, _SnapController_getMnemonicSeed, _SnapController_getFeatureFlags, _SnapController_clientCryptography, _SnapController_detectSnapLocation, _SnapController_snapsRuntimeData, _SnapController_rollbackSnapshots, _SnapController_timeoutForLastRequestStatus, _SnapController_statusMachine, _SnapController_preinstalledSnaps, _SnapController_trackEvent, _SnapController_trackSnapExport, _SnapController_initializeStateMachine, _SnapController_registerMessageHandlers, _SnapController_handlePreinstalledSnaps, _SnapController_pollForLastRequestStatus, _SnapController_blockSnap, _SnapController_unblockSnap, _SnapController_assertIsInstallAllowed, _SnapController_assertCanInstallSnaps, _SnapController_assertCanUsePlatform, _SnapController_stopSnapsLastRequestPastMax, _SnapController_transition, _SnapController_terminateSnap, _SnapController_hasCachedEncryptionKey, _SnapController_getSnapEncryptionKey, _SnapController_decryptSnapState, _SnapController_encryptSnapState, _SnapController_getStateToPersist, _SnapController_persistSnapState, _SnapController_handleInitialConnections, _SnapController_addSnapToSubject, _SnapController_removeSnapFromSubjects, _SnapController_revokeAllSnapPermissions, _SnapController_createApproval, _SnapController_updateApproval, _SnapController_resolveAllowlistVersion, _SnapController_add, _SnapController_startSnap, _SnapController_getEndowments, _SnapController_set, _SnapController_validateSnapPermissions, _SnapController_validatePlatformVersion, _SnapController_getExecutionTimeout, _SnapController_createInterface, _SnapController_assertInterfaceExists, _SnapController_transformSnapRpcResponse, _SnapController_transformOnAssetsLookupResult, _SnapController_transformOnAssetsConversionResult, _SnapController_transformSnapRpcRequest, _SnapController_assertSnapRpcResponse, _SnapController_recordSnapRpcRequestStart, _SnapController_recordSnapRpcRequestFinish, _SnapController_getRollbackSnapshot, _SnapController_createRollbackSnapshot, _SnapController_rollbackSnap, _SnapController_rollbackSnaps, _SnapController_getRuntime, _SnapController_getRuntimeExpect, _SnapController_setupRuntime, _SnapController_calculatePermissionsChange, _SnapController_isSubjectConnectedToSnap, _SnapController_calculateConnectionsChange, _SnapController_getPermissionsToGrant, _SnapController_updatePermissions, _SnapController_isValidUpdate, _SnapController_callLifecycleHook, _SnapController_handleLock;
13
1
  import { BaseController } from "@metamask/base-controller";
14
2
  import { SubjectType } from "@metamask/permission-controller";
15
3
  import { rpcErrors } from "@metamask/rpc-errors";
16
4
  import { WALLET_SNAP_PERMISSION_KEY, getMaxRequestTimeCaveat, handlerEndowments, SnapEndowments, getKeyringCaveatOrigins, getRpcCaveatOrigins, processSnapPermissions, getEncryptionEntropy, getChainIdsCaveat } from "@metamask/snaps-rpc-methods";
17
5
  import { AuxiliaryFileEncoding, getErrorMessage, OnAssetsLookupResponseStruct } from "@metamask/snaps-sdk";
18
6
  import { logWarning, getPlatformVersion, assertIsSnapManifest, assertIsValidSnapId, DEFAULT_ENDOWMENTS, DEFAULT_REQUESTED_SNAP_VERSION, encodeAuxiliaryFile, HandlerType, isOriginAllowed, logError, normalizeRelative, OnTransactionResponseStruct, OnSignatureResponseStruct, resolveVersionRange, SnapCaveatType, SnapStatus, SnapStatusEvents, unwrapError, OnHomePageResponseStruct, getValidatedLocalizationFiles, VirtualFile, NpmSnapFileNames, OnNameLookupResponseStruct, getLocalizedSnapManifest, MAX_FILE_SIZE, OnSettingsPageResponseStruct, isValidUrl, OnAssetHistoricalPriceResponseStruct, OnAssetsConversionResponseStruct } from "@metamask/snaps-utils";
19
- import { hexToNumber, assert, assertIsJsonRpcRequest, assertStruct, Duration, gtRange, gtVersion, hasProperty, inMilliseconds, isNonEmptyArray, isValidSemVerRange, satisfiesVersionRange, timeSince } from "@metamask/utils";
7
+ import { hexToNumber, assert, assertIsJsonRpcRequest, assertStruct, Duration, gtRange, gtVersion, hasProperty, inMilliseconds, isNonEmptyArray, isValidSemVerRange, satisfiesVersionRange, timeSince, createDeferredPromise } from "@metamask/utils";
20
8
  import { createMachine, interpret } from "@xstate/fsm";
21
9
  import { Mutex } from "async-mutex";
22
10
  import { nanoid } from "nanoid";
@@ -68,6 +56,29 @@ function truncateSnap(snap) {
68
56
  * - Start: Initializes the snap in its SES realm with the authorized permissions.
69
57
  */
70
58
  export class SnapController extends BaseController {
59
+ #closeAllConnections;
60
+ #dynamicPermissions;
61
+ #environmentEndowmentPermissions;
62
+ #excludedPermissions;
63
+ #featureFlags;
64
+ #fetchFunction;
65
+ #idleTimeCheckInterval;
66
+ #maxIdleTime;
67
+ // This property cannot be hash private yet because of tests.
68
+ // eslint-disable-next-line no-restricted-syntax
69
+ maxRequestTime;
70
+ #encryptor;
71
+ #getMnemonicSeed;
72
+ #getFeatureFlags;
73
+ #clientCryptography;
74
+ #detectSnapLocation;
75
+ #snapsRuntimeData;
76
+ #rollbackSnapshots;
77
+ #timeoutForLastRequestStatus;
78
+ #statusMachine;
79
+ #preinstalledSnaps;
80
+ #trackEvent;
81
+ #trackSnapExport;
71
82
  constructor({ closeAllConnections, messenger, state, dynamicPermissions = ['eth_accounts'], environmentEndowmentPermissions = [], excludedPermissions = {}, idleTimeCheckInterval = inMilliseconds(5, Duration.Second), maxIdleTime = inMilliseconds(30, Duration.Second), maxRequestTime = inMilliseconds(60, Duration.Second), fetchFunction = globalThis.fetch.bind(undefined), featureFlags = {}, detectSnapLocation: detectSnapLocationFunction = detectSnapLocation, preinstalledSnaps = null, encryptor, getMnemonicSeed, getFeatureFlags = () => ({}), clientCryptography, trackEvent, }) {
72
83
  super({
73
84
  messenger,
@@ -107,103 +118,53 @@ export class SnapController extends BaseController {
107
118
  ...state,
108
119
  },
109
120
  });
110
- _SnapController_instances.add(this);
111
- _SnapController_closeAllConnections.set(this, void 0);
112
- _SnapController_dynamicPermissions.set(this, void 0);
113
- _SnapController_environmentEndowmentPermissions.set(this, void 0);
114
- _SnapController_excludedPermissions.set(this, void 0);
115
- _SnapController_featureFlags.set(this, void 0);
116
- _SnapController_fetchFunction.set(this, void 0);
117
- _SnapController_idleTimeCheckInterval.set(this, void 0);
118
- _SnapController_maxIdleTime.set(this, void 0);
119
- _SnapController_encryptor.set(this, void 0);
120
- _SnapController_getMnemonicSeed.set(this, void 0);
121
- _SnapController_getFeatureFlags.set(this, void 0);
122
- _SnapController_clientCryptography.set(this, void 0);
123
- _SnapController_detectSnapLocation.set(this, void 0);
124
- _SnapController_snapsRuntimeData.set(this, void 0);
125
- _SnapController_rollbackSnapshots.set(this, void 0);
126
- _SnapController_timeoutForLastRequestStatus.set(this, void 0);
127
- _SnapController_statusMachine.set(this, void 0);
128
- _SnapController_preinstalledSnaps.set(this, void 0);
129
- _SnapController_trackEvent.set(this, void 0);
130
- _SnapController_trackSnapExport.set(this, void 0);
131
- /**
132
- * Persist the state of a Snap.
133
- *
134
- * This function is debounced per Snap, meaning that multiple calls to this
135
- * function for the same Snap will only result in one state update. It also
136
- * uses a mutex to ensure that only one state update per Snap is processed at
137
- * a time, avoiding possible race conditions.
138
- *
139
- * @param snapId - The Snap ID.
140
- * @param newSnapState - The new state of the Snap.
141
- * @param encrypted - A flag to indicate whether to use encrypted storage or
142
- * not.
143
- */
144
- _SnapController_persistSnapState.set(this, debouncePersistState((snapId, newSnapState, encrypted) => {
145
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
146
- runtime.stateMutex
147
- .runExclusive(async () => {
148
- const newState = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getStateToPersist).call(this, snapId, newSnapState, encrypted);
149
- if (encrypted) {
150
- return this.update((state) => {
151
- state.snapStates[snapId] = newState;
152
- });
153
- }
154
- return this.update((state) => {
155
- state.unencryptedSnapStates[snapId] = newState;
156
- });
157
- })
158
- .catch(logError);
159
- }, STATE_DEBOUNCE_TIMEOUT));
160
- __classPrivateFieldSet(this, _SnapController_closeAllConnections, closeAllConnections, "f");
161
- __classPrivateFieldSet(this, _SnapController_dynamicPermissions, dynamicPermissions, "f");
162
- __classPrivateFieldSet(this, _SnapController_environmentEndowmentPermissions, environmentEndowmentPermissions, "f");
163
- __classPrivateFieldSet(this, _SnapController_excludedPermissions, excludedPermissions, "f");
164
- __classPrivateFieldSet(this, _SnapController_featureFlags, featureFlags, "f");
165
- __classPrivateFieldSet(this, _SnapController_fetchFunction, fetchFunction, "f");
166
- __classPrivateFieldSet(this, _SnapController_idleTimeCheckInterval, idleTimeCheckInterval, "f");
167
- __classPrivateFieldSet(this, _SnapController_maxIdleTime, maxIdleTime, "f");
121
+ this.#closeAllConnections = closeAllConnections;
122
+ this.#dynamicPermissions = dynamicPermissions;
123
+ this.#environmentEndowmentPermissions = environmentEndowmentPermissions;
124
+ this.#excludedPermissions = excludedPermissions;
125
+ this.#featureFlags = featureFlags;
126
+ this.#fetchFunction = fetchFunction;
127
+ this.#idleTimeCheckInterval = idleTimeCheckInterval;
128
+ this.#maxIdleTime = maxIdleTime;
168
129
  this.maxRequestTime = maxRequestTime;
169
- __classPrivateFieldSet(this, _SnapController_detectSnapLocation, detectSnapLocationFunction, "f");
170
- __classPrivateFieldSet(this, _SnapController_encryptor, encryptor, "f");
171
- __classPrivateFieldSet(this, _SnapController_getMnemonicSeed, getMnemonicSeed, "f");
172
- __classPrivateFieldSet(this, _SnapController_getFeatureFlags, getFeatureFlags, "f");
173
- __classPrivateFieldSet(this, _SnapController_clientCryptography, clientCryptography, "f");
174
- __classPrivateFieldSet(this, _SnapController_preinstalledSnaps, preinstalledSnaps, "f");
130
+ this.#detectSnapLocation = detectSnapLocationFunction;
131
+ this.#encryptor = encryptor;
132
+ this.#getMnemonicSeed = getMnemonicSeed;
133
+ this.#getFeatureFlags = getFeatureFlags;
134
+ this.#clientCryptography = clientCryptography;
135
+ this.#preinstalledSnaps = preinstalledSnaps;
175
136
  this._onUnhandledSnapError = this._onUnhandledSnapError.bind(this);
176
137
  this._onOutboundRequest = this._onOutboundRequest.bind(this);
177
138
  this._onOutboundResponse = this._onOutboundResponse.bind(this);
178
- __classPrivateFieldSet(this, _SnapController_rollbackSnapshots, new Map(), "f");
179
- __classPrivateFieldSet(this, _SnapController_snapsRuntimeData, new Map(), "f");
180
- __classPrivateFieldSet(this, _SnapController_trackEvent, trackEvent, "f");
181
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_pollForLastRequestStatus).call(this);
139
+ this.#rollbackSnapshots = new Map();
140
+ this.#snapsRuntimeData = new Map();
141
+ this.#trackEvent = trackEvent;
142
+ this.#pollForLastRequestStatus();
182
143
  /* eslint-disable @typescript-eslint/unbound-method */
183
144
  this.messagingSystem.subscribe('ExecutionService:unhandledError', this._onUnhandledSnapError);
184
145
  this.messagingSystem.subscribe('ExecutionService:outboundRequest', this._onOutboundRequest);
185
146
  this.messagingSystem.subscribe('ExecutionService:outboundResponse', this._onOutboundResponse);
186
147
  /* eslint-enable @typescript-eslint/unbound-method */
187
148
  this.messagingSystem.subscribe('SnapController:snapInstalled', ({ id }, origin) => {
188
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_callLifecycleHook).call(this, origin, id, HandlerType.OnInstall).catch((error) => {
149
+ this.#callLifecycleHook(origin, id, HandlerType.OnInstall).catch((error) => {
189
150
  logError(`Error when calling \`onInstall\` lifecycle hook for snap "${id}": ${getErrorMessage(error)}`);
190
151
  });
191
152
  });
192
153
  this.messagingSystem.subscribe('SnapController:snapUpdated', ({ id }, _oldVersion, origin) => {
193
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_callLifecycleHook).call(this, origin, id, HandlerType.OnUpdate).catch((error) => {
154
+ this.#callLifecycleHook(origin, id, HandlerType.OnUpdate).catch((error) => {
194
155
  logError(`Error when calling \`onUpdate\` lifecycle hook for snap "${id}": ${getErrorMessage(error)}`);
195
156
  });
196
157
  });
197
- this.messagingSystem.subscribe('KeyringController:lock', __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleLock).bind(this));
198
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_initializeStateMachine).call(this);
199
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_registerMessageHandlers).call(this);
200
- Object.values(this.state?.snaps ?? {}).forEach((snap) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_setupRuntime).call(this, snap.id));
201
- if (__classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f")) {
202
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handlePreinstalledSnaps).call(this, __classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f"));
203
- }
204
- __classPrivateFieldSet(this, _SnapController_trackSnapExport, throttleTracking((snapId, handler, success, origin) => {
158
+ this.messagingSystem.subscribe('KeyringController:lock', this.#handleLock.bind(this));
159
+ this.#initializeStateMachine();
160
+ this.#registerMessageHandlers();
161
+ Object.values(this.state?.snaps ?? {}).forEach((snap) => this.#setupRuntime(snap.id));
162
+ if (this.#preinstalledSnaps) {
163
+ this.#handlePreinstalledSnaps(this.#preinstalledSnaps);
164
+ }
165
+ this.#trackSnapExport = throttleTracking((snapId, handler, success, origin) => {
205
166
  const snapMetadata = this.messagingSystem.call('SnapsRegistry:getMetadata', snapId);
206
- __classPrivateFieldGet(this, _SnapController_trackEvent, "f").call(this, {
167
+ this.#trackEvent({
207
168
  event: 'Snap Export Used',
208
169
  category: 'Snaps',
209
170
  properties: {
@@ -216,7 +177,190 @@ export class SnapController extends BaseController {
216
177
  origin,
217
178
  },
218
179
  });
219
- }), "f");
180
+ });
181
+ }
182
+ /**
183
+ * We track status of a Snap using a finite-state-machine.
184
+ * It keeps track of whether the snap is started / stopped / etc.
185
+ *
186
+ * @see {@link SnapController.transition} for interacting with the machine.
187
+ */
188
+ // We initialize the machine in the instance because the status is currently tightly coupled
189
+ // with the SnapController - the guard checks for enabled status inside the SnapController state.
190
+ // In the future, side-effects could be added to the machine during transitions.
191
+ #initializeStateMachine() {
192
+ const disableGuard = ({ snapId }) => {
193
+ return this.getExpect(snapId).enabled;
194
+ };
195
+ const statusConfig = {
196
+ initial: SnapStatus.Installing,
197
+ states: {
198
+ [SnapStatus.Installing]: {
199
+ on: {
200
+ [SnapStatusEvents.Start]: {
201
+ target: SnapStatus.Running,
202
+ cond: disableGuard,
203
+ },
204
+ },
205
+ },
206
+ [SnapStatus.Updating]: {
207
+ on: {
208
+ [SnapStatusEvents.Start]: {
209
+ target: SnapStatus.Running,
210
+ cond: disableGuard,
211
+ },
212
+ [SnapStatusEvents.Stop]: SnapStatus.Stopped,
213
+ },
214
+ },
215
+ [SnapStatus.Running]: {
216
+ on: {
217
+ [SnapStatusEvents.Stop]: SnapStatus.Stopped,
218
+ [SnapStatusEvents.Crash]: SnapStatus.Crashed,
219
+ },
220
+ },
221
+ [SnapStatus.Stopped]: {
222
+ on: {
223
+ [SnapStatusEvents.Start]: {
224
+ target: SnapStatus.Running,
225
+ cond: disableGuard,
226
+ },
227
+ [SnapStatusEvents.Update]: SnapStatus.Updating,
228
+ },
229
+ },
230
+ [SnapStatus.Crashed]: {
231
+ on: {
232
+ [SnapStatusEvents.Start]: {
233
+ target: SnapStatus.Running,
234
+ cond: disableGuard,
235
+ },
236
+ [SnapStatusEvents.Update]: SnapStatus.Updating,
237
+ },
238
+ },
239
+ },
240
+ };
241
+ this.#statusMachine = createMachine(statusConfig);
242
+ validateMachine(this.#statusMachine);
243
+ }
244
+ /**
245
+ * Constructor helper for registering the controller's messaging system
246
+ * actions.
247
+ */
248
+ #registerMessageHandlers() {
249
+ this.messagingSystem.registerActionHandler(`${controllerName}:init`, (...args) => this.init(...args));
250
+ this.messagingSystem.registerActionHandler(`${controllerName}:clearSnapState`, (...args) => this.clearSnapState(...args));
251
+ this.messagingSystem.registerActionHandler(`${controllerName}:get`, (...args) => this.get(...args));
252
+ this.messagingSystem.registerActionHandler(`${controllerName}:getSnapState`, async (...args) => this.getSnapState(...args));
253
+ this.messagingSystem.registerActionHandler(`${controllerName}:handleRequest`, async (...args) => this.handleRequest(...args));
254
+ this.messagingSystem.registerActionHandler(`${controllerName}:has`, (...args) => this.has(...args));
255
+ this.messagingSystem.registerActionHandler(`${controllerName}:updateBlockedSnaps`, async () => this.updateBlockedSnaps());
256
+ this.messagingSystem.registerActionHandler(`${controllerName}:updateSnapState`, async (...args) => this.updateSnapState(...args));
257
+ this.messagingSystem.registerActionHandler(`${controllerName}:enable`, (...args) => this.enableSnap(...args));
258
+ this.messagingSystem.registerActionHandler(`${controllerName}:disable`, async (...args) => this.disableSnap(...args));
259
+ this.messagingSystem.registerActionHandler(`${controllerName}:remove`, async (...args) => this.removeSnap(...args));
260
+ this.messagingSystem.registerActionHandler(`${controllerName}:getPermitted`, (...args) => this.getPermittedSnaps(...args));
261
+ this.messagingSystem.registerActionHandler(`${controllerName}:install`, async (...args) => this.installSnaps(...args));
262
+ this.messagingSystem.registerActionHandler(`${controllerName}:getAll`, (...args) => this.getAllSnaps(...args));
263
+ this.messagingSystem.registerActionHandler(`${controllerName}:getRunnableSnaps`, (...args) => this.getRunnableSnaps(...args));
264
+ this.messagingSystem.registerActionHandler(`${controllerName}:incrementActiveReferences`, (...args) => this.incrementActiveReferences(...args));
265
+ this.messagingSystem.registerActionHandler(`${controllerName}:decrementActiveReferences`, (...args) => this.decrementActiveReferences(...args));
266
+ this.messagingSystem.registerActionHandler(`${controllerName}:disconnectOrigin`, (...args) => this.removeSnapFromSubject(...args));
267
+ this.messagingSystem.registerActionHandler(`${controllerName}:revokeDynamicPermissions`, (...args) => this.revokeDynamicSnapPermissions(...args));
268
+ this.messagingSystem.registerActionHandler(`${controllerName}:getFile`, async (...args) => this.getSnapFile(...args));
269
+ this.messagingSystem.registerActionHandler(`${controllerName}:stopAllSnaps`, async (...args) => this.stopAllSnaps(...args));
270
+ this.messagingSystem.registerActionHandler(`${controllerName}:isMinimumPlatformVersion`, (...args) => this.isMinimumPlatformVersion(...args));
271
+ }
272
+ /**
273
+ * Initialise the SnapController.
274
+ *
275
+ * Currently this method calls the `onStart` lifecycle hook for all
276
+ * installed Snaps.
277
+ */
278
+ init() {
279
+ const snaps = this.getRunnableSnaps();
280
+ for (const { id } of snaps) {
281
+ const hasLifecycleHooksEndowment = this.messagingSystem.call('PermissionController:hasPermission', id, SnapEndowments.LifecycleHooks);
282
+ if (!hasLifecycleHooksEndowment) {
283
+ continue;
284
+ }
285
+ this.#callLifecycleHook(METAMASK_ORIGIN, id, HandlerType.OnStart).catch((error) => {
286
+ logError(`Error when calling \`onStart\` lifecycle hook for Snap "${id}": ${getErrorMessage(error)}`);
287
+ });
288
+ }
289
+ }
290
+ #handlePreinstalledSnaps(preinstalledSnaps) {
291
+ for (const { snapId, manifest, files, removable, hidden, hideSnapBranding, } of preinstalledSnaps) {
292
+ const existingSnap = this.get(snapId);
293
+ const isAlreadyInstalled = existingSnap !== undefined;
294
+ const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version);
295
+ // Disallow downgrades and overwriting non preinstalled snaps
296
+ if (isAlreadyInstalled &&
297
+ (!isUpdate || existingSnap.preinstalled !== true)) {
298
+ continue;
299
+ }
300
+ const manifestFile = new VirtualFile({
301
+ path: NpmSnapFileNames.Manifest,
302
+ value: JSON.stringify(manifest),
303
+ result: manifest,
304
+ });
305
+ const virtualFiles = files.map(({ path, value }) => new VirtualFile({ value, path }));
306
+ const { filePath, iconPath } = manifest.source.location.npm;
307
+ const sourceCode = virtualFiles.find((file) => file.path === filePath);
308
+ const svgIcon = iconPath
309
+ ? virtualFiles.find((file) => file.path === iconPath)
310
+ : undefined;
311
+ assert(sourceCode, 'Source code not provided for preinstalled snap.');
312
+ assert(!iconPath || (iconPath && svgIcon), 'Icon not provided for preinstalled snap.');
313
+ assert(manifest.source.files === undefined, 'Auxiliary files are not currently supported for preinstalled snaps.');
314
+ const localizationFiles = manifest.source.locales?.map((path) => virtualFiles.find((file) => file.path === path)) ?? [];
315
+ const validatedLocalizationFiles = getValidatedLocalizationFiles(localizationFiles.filter(Boolean));
316
+ assert(localizationFiles.length === validatedLocalizationFiles.length, 'Missing localization files for preinstalled snap.');
317
+ const filesObject = {
318
+ manifest: manifestFile,
319
+ sourceCode,
320
+ svgIcon,
321
+ auxiliaryFiles: [],
322
+ localizationFiles: validatedLocalizationFiles,
323
+ };
324
+ // Add snap to the SnapController state
325
+ this.#set({
326
+ id: snapId,
327
+ origin: METAMASK_ORIGIN,
328
+ files: filesObject,
329
+ removable,
330
+ hidden,
331
+ hideSnapBranding,
332
+ preinstalled: true,
333
+ });
334
+ // Setup permissions
335
+ const processedPermissions = processSnapPermissions(manifest.initialPermissions);
336
+ this.#validateSnapPermissions(processedPermissions);
337
+ const { newPermissions, unusedPermissions } = this.#calculatePermissionsChange(snapId, processedPermissions);
338
+ this.#updatePermissions({ snapId, newPermissions, unusedPermissions });
339
+ if (manifest.initialConnections) {
340
+ this.#handleInitialConnections(snapId, existingSnap?.initialConnections ?? null, manifest.initialConnections);
341
+ }
342
+ // Set status
343
+ this.update((state) => {
344
+ state.snaps[snapId].status = SnapStatus.Stopped;
345
+ });
346
+ this.#setupRuntime(snapId);
347
+ // Emit events
348
+ if (isUpdate) {
349
+ this.messagingSystem.publish('SnapController:snapUpdated', this.getTruncatedExpect(snapId), existingSnap.version, METAMASK_ORIGIN, true);
350
+ }
351
+ else {
352
+ this.messagingSystem.publish('SnapController:snapInstalled', this.getTruncatedExpect(snapId), METAMASK_ORIGIN, true);
353
+ }
354
+ }
355
+ }
356
+ #pollForLastRequestStatus() {
357
+ this.#timeoutForLastRequestStatus = setTimeout(() => {
358
+ this.#stopSnapsLastRequestPastMax().catch((error) => {
359
+ // TODO: Decide how to handle errors.
360
+ logError(error);
361
+ });
362
+ this.#pollForLastRequestStatus();
363
+ }, this.#idleTimeCheckInterval);
220
364
  }
221
365
  /**
222
366
  * Checks all installed snaps against the block list and
@@ -224,7 +368,7 @@ export class SnapController extends BaseController {
224
368
  * for more information.
225
369
  */
226
370
  async updateBlockedSnaps() {
227
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
371
+ this.#assertCanUsePlatform();
228
372
  await this.messagingSystem.call('SnapsRegistry:update');
229
373
  const blockedSnaps = await this.messagingSystem.call('SnapsRegistry:get', Object.values(this.state.snaps).reduce((blockListArg, snap) => {
230
374
  blockListArg[snap.id] = {
@@ -235,11 +379,92 @@ export class SnapController extends BaseController {
235
379
  }, {}));
236
380
  await Promise.all(Object.entries(blockedSnaps).map(async ([snapId, { status, reason }]) => {
237
381
  if (status === SnapsRegistryStatus.Blocked) {
238
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_blockSnap).call(this, snapId, reason);
382
+ return this.#blockSnap(snapId, reason);
239
383
  }
240
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_unblockSnap).call(this, snapId);
384
+ return this.#unblockSnap(snapId);
241
385
  }));
242
386
  }
387
+ /**
388
+ * Blocks an installed snap and prevents it from being started again. Emits
389
+ * {@link SnapBlocked}. Does nothing if the snap is not installed.
390
+ *
391
+ * @param snapId - The snap to block.
392
+ * @param blockedSnapInfo - Information detailing why the snap is blocked.
393
+ */
394
+ async #blockSnap(snapId, blockedSnapInfo) {
395
+ if (!this.has(snapId)) {
396
+ return;
397
+ }
398
+ try {
399
+ this.update((state) => {
400
+ state.snaps[snapId].blocked = true;
401
+ state.snaps[snapId].blockInformation = blockedSnapInfo;
402
+ });
403
+ await this.disableSnap(snapId);
404
+ }
405
+ catch (error) {
406
+ logError(`Encountered error when stopping blocked snap "${snapId}".`, error);
407
+ }
408
+ this.messagingSystem.publish(`${controllerName}:snapBlocked`, snapId, blockedSnapInfo);
409
+ }
410
+ /**
411
+ * Unblocks a snap so that it can be enabled and started again. Emits
412
+ * {@link SnapUnblocked}. Does nothing if the snap is not installed or already
413
+ * unblocked.
414
+ *
415
+ * @param snapId - The id of the snap to unblock.
416
+ */
417
+ #unblockSnap(snapId) {
418
+ if (!this.has(snapId) || !this.state.snaps[snapId].blocked) {
419
+ return;
420
+ }
421
+ this.update((state) => {
422
+ state.snaps[snapId].blocked = false;
423
+ delete state.snaps[snapId].blockInformation;
424
+ });
425
+ this.messagingSystem.publish(`${controllerName}:snapUnblocked`, snapId);
426
+ }
427
+ async #assertIsInstallAllowed(snapId, { platformVersion, ...snapInfo }) {
428
+ const results = await this.messagingSystem.call('SnapsRegistry:get', {
429
+ [snapId]: snapInfo,
430
+ });
431
+ const result = results[snapId];
432
+ if (result.status === SnapsRegistryStatus.Blocked) {
433
+ throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": The version is blocked. ${result.reason?.explanation ?? ''}`);
434
+ }
435
+ const isAllowlistingRequired = Object.keys(snapInfo.permissions).some((permission) => !ALLOWED_PERMISSIONS.includes(permission));
436
+ if (this.#featureFlags.requireAllowlist &&
437
+ isAllowlistingRequired &&
438
+ result.status !== SnapsRegistryStatus.Verified) {
439
+ throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": ${result.status === SnapsRegistryStatus.Unavailable
440
+ ? 'The registry is temporarily unavailable.'
441
+ : 'The snap is not on the allowlist.'}`);
442
+ }
443
+ this.#validatePlatformVersion(snapId, platformVersion);
444
+ }
445
+ /**
446
+ * Asserts whether new Snaps are allowed to be installed.
447
+ */
448
+ #assertCanInstallSnaps() {
449
+ assert(this.#featureFlags.disableSnapInstallation !== true, 'Installing Snaps is currently disabled in this version of MetaMask.');
450
+ }
451
+ /**
452
+ * Asserts whether the Snaps platform is allowed to run.
453
+ */
454
+ #assertCanUsePlatform() {
455
+ const flags = this.#getFeatureFlags();
456
+ assert(flags.disableSnaps !== true, 'The Snaps platform requires basic functionality to be used. Enable basic functionality in the settings to use the Snaps platform.');
457
+ }
458
+ async #stopSnapsLastRequestPastMax() {
459
+ const entries = [...this.#snapsRuntimeData.entries()];
460
+ return Promise.all(entries
461
+ .filter(([_snapId, runtime]) => runtime.activeReferences === 0 &&
462
+ runtime.pendingInboundRequests.length === 0 &&
463
+ runtime.lastRequest &&
464
+ this.#maxIdleTime &&
465
+ timeSince(runtime.lastRequest) > this.#maxIdleTime)
466
+ .map(async ([snapId]) => this.stopSnap(snapId, SnapStatusEvents.Stop)));
467
+ }
243
468
  _onUnhandledSnapError(snapId, error) {
244
469
  // Log the error that caused the crash
245
470
  // so it gets raised to the developer for debugging purposes.
@@ -250,7 +475,7 @@ export class SnapController extends BaseController {
250
475
  });
251
476
  }
252
477
  _onOutboundRequest(snapId) {
253
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
478
+ const runtime = this.#getRuntimeExpect(snapId);
254
479
  // Ideally we would only pause the pending request that is making the outbound request
255
480
  // but right now we don't have a way to know which request initiated the outbound request
256
481
  runtime.pendingInboundRequests
@@ -259,7 +484,7 @@ export class SnapController extends BaseController {
259
484
  runtime.pendingOutboundRequests += 1;
260
485
  }
261
486
  _onOutboundResponse(snapId) {
262
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
487
+ const runtime = this.#getRuntimeExpect(snapId);
263
488
  runtime.pendingOutboundRequests -= 1;
264
489
  if (runtime.pendingOutboundRequests === 0) {
265
490
  runtime.pendingInboundRequests
@@ -267,6 +492,25 @@ export class SnapController extends BaseController {
267
492
  .forEach((pendingRequest) => pendingRequest.timer.resume());
268
493
  }
269
494
  }
495
+ /**
496
+ * Transitions between states using `snapStatusStateMachineConfig` as the template to figure out
497
+ * the next state. This transition function uses a very minimal subset of XState conventions:
498
+ * - supports initial state
499
+ * - .on supports raw event target string
500
+ * - .on supports {target, cond} object
501
+ * - the arguments for `cond` is the `SerializedSnap` instead of Xstate convention of `(event,
502
+ * context) => boolean`
503
+ *
504
+ * @param snapId - The id of the snap to transition.
505
+ * @param event - The event enum to use to transition.
506
+ */
507
+ #transition(snapId, event) {
508
+ const { interpreter } = this.#getRuntimeExpect(snapId);
509
+ interpreter.send(event);
510
+ this.update((state) => {
511
+ state.snaps[snapId].status = interpreter.state.value;
512
+ });
513
+ }
270
514
  /**
271
515
  * Starts the given snap. Throws an error if no such snap exists
272
516
  * or if it is already running.
@@ -274,12 +518,12 @@ export class SnapController extends BaseController {
274
518
  * @param snapId - The id of the Snap to start.
275
519
  */
276
520
  async startSnap(snapId) {
277
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
521
+ this.#assertCanUsePlatform();
278
522
  const snap = this.state.snaps[snapId];
279
523
  if (!snap.enabled) {
280
524
  throw new Error(`Snap "${snapId}" is disabled.`);
281
525
  }
282
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_startSnap).call(this, {
526
+ await this.#startSnap({
283
527
  snapId,
284
528
  sourceCode: snap.sourceCode,
285
529
  });
@@ -327,21 +571,23 @@ export class SnapController extends BaseController {
327
571
  * stopped.
328
572
  */
329
573
  async stopSnap(snapId, statusEvent = SnapStatusEvents.Stop) {
330
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntime).call(this, snapId);
574
+ const runtime = this.#getRuntime(snapId);
331
575
  if (!runtime) {
332
576
  throw new Error(`The snap "${snapId}" is not running.`);
333
577
  }
334
- // No-op if the Snap is already stopping.
335
- if (runtime.stopping) {
578
+ // If we are already stopping, wait for that to finish.
579
+ if (runtime.stopPromise) {
580
+ await runtime.stopPromise;
336
581
  return;
337
582
  }
338
583
  // Flag that the Snap is actively stopping, this prevents other calls to stopSnap
339
584
  // while we are handling termination of the Snap
340
- runtime.stopping = true;
585
+ const { promise, resolve } = createDeferredPromise();
586
+ runtime.stopPromise = promise;
341
587
  try {
342
588
  if (this.isRunning(snapId)) {
343
- __classPrivateFieldGet(this, _SnapController_closeAllConnections, "f")?.call(this, snapId);
344
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_terminateSnap).call(this, snapId);
589
+ this.#closeAllConnections?.(snapId);
590
+ await this.#terminateSnap(snapId);
345
591
  }
346
592
  }
347
593
  finally {
@@ -349,10 +595,11 @@ export class SnapController extends BaseController {
349
595
  runtime.lastRequest = null;
350
596
  runtime.pendingInboundRequests = [];
351
597
  runtime.pendingOutboundRequests = 0;
352
- runtime.stopping = false;
598
+ runtime.stopPromise = null;
353
599
  if (this.isRunning(snapId)) {
354
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, statusEvent);
600
+ this.#transition(snapId, statusEvent);
355
601
  }
602
+ resolve();
356
603
  }
357
604
  }
358
605
  /**
@@ -367,6 +614,24 @@ export class SnapController extends BaseController {
367
614
  const promises = snaps.map(async (snap) => this.stopSnap(snap.id, statusEvent));
368
615
  await Promise.allSettled(promises);
369
616
  }
617
+ /**
618
+ * Terminates the specified snap and emits the `snapTerminated` event.
619
+ *
620
+ * @param snapId - The snap to terminate.
621
+ */
622
+ async #terminateSnap(snapId) {
623
+ await this.messagingSystem.call('ExecutionService:terminateSnap', snapId);
624
+ // Hack to give up execution for a bit to let gracefully terminating Snaps return.
625
+ await new Promise((resolve) => setTimeout(resolve, 1));
626
+ const runtime = this.#getRuntimeExpect(snapId);
627
+ // Unresponsive requests may still be timed, time them out.
628
+ runtime.pendingInboundRequests
629
+ .filter((pendingRequest) => pendingRequest.timer.status !== 'finished')
630
+ .forEach((pendingRequest) => pendingRequest.timer.finish());
631
+ // Hack to give up execution for a bit to let timed out requests return.
632
+ await new Promise((resolve) => setTimeout(resolve, 1));
633
+ this.messagingSystem.publish('SnapController:snapTerminated', this.getTruncatedExpect(snapId));
634
+ }
370
635
  /**
371
636
  * Returns whether the given snap is running.
372
637
  * Throws an error if the snap doesn't exist.
@@ -434,6 +699,157 @@ export class SnapController extends BaseController {
434
699
  getTruncatedExpect(snapId) {
435
700
  return truncateSnap(this.getExpect(snapId));
436
701
  }
702
+ /**
703
+ * Check if a given Snap has a cached encryption key stored in the runtime.
704
+ *
705
+ * @param snapId - The Snap ID.
706
+ * @param runtime - The Snap runtime data.
707
+ * @returns True if the Snap has a cached encryption key, otherwise false.
708
+ */
709
+ #hasCachedEncryptionKey(snapId, runtime = this.#getRuntimeExpect(snapId)) {
710
+ return runtime.encryptionKey !== null && runtime.encryptionSalt !== null;
711
+ }
712
+ /**
713
+ * Generate an encryption key to be used for state encryption for a given Snap.
714
+ *
715
+ * @param options - An options bag.
716
+ * @param options.snapId - The Snap ID.
717
+ * @param options.salt - A salt to be used for the encryption key.
718
+ * @param options.useCache - Whether to use caching or not.
719
+ * @param options.keyMetadata - Optional metadata about how to derive the encryption key.
720
+ * @returns An encryption key.
721
+ */
722
+ async #getSnapEncryptionKey({ snapId, salt: passedSalt, useCache, keyMetadata, }) {
723
+ const runtime = this.#getRuntimeExpect(snapId);
724
+ if (this.#hasCachedEncryptionKey(snapId, runtime) && useCache) {
725
+ return {
726
+ key: await this.#encryptor.importKey(runtime.encryptionKey),
727
+ salt: runtime.encryptionSalt,
728
+ };
729
+ }
730
+ const salt = passedSalt ?? this.#encryptor.generateSalt();
731
+ const seed = await this.#getMnemonicSeed();
732
+ const entropy = await getEncryptionEntropy({
733
+ snapId,
734
+ seed,
735
+ cryptographicFunctions: this.#clientCryptography,
736
+ });
737
+ const encryptionKey = await this.#encryptor.keyFromPassword(entropy, salt, true, keyMetadata);
738
+ const exportedKey = await this.#encryptor.exportKey(encryptionKey);
739
+ // Cache exported encryption key in runtime
740
+ if (useCache) {
741
+ runtime.encryptionKey = exportedKey;
742
+ runtime.encryptionSalt = salt;
743
+ }
744
+ return { key: encryptionKey, salt };
745
+ }
746
+ /**
747
+ * Decrypt the encrypted state for a given Snap.
748
+ *
749
+ * @param snapId - The Snap ID.
750
+ * @param state - The encrypted state as a string.
751
+ * @returns A valid JSON object derived from the encrypted state.
752
+ * @throws If the decryption fails or the decrypted state is not valid JSON.
753
+ */
754
+ async #decryptSnapState(snapId, state) {
755
+ try {
756
+ // We assume that the state string here is valid JSON since we control serialization.
757
+ // This lets us skip JSON validation.
758
+ const parsed = JSON.parse(state);
759
+ const { salt, keyMetadata } = parsed;
760
+ // We only cache encryption keys if they are already cached or if the encryption key is using the latest key derivation params.
761
+ const useCache = this.#hasCachedEncryptionKey(snapId) ||
762
+ this.#encryptor.isVaultUpdated(state);
763
+ const { key } = await this.#getSnapEncryptionKey({
764
+ snapId,
765
+ salt,
766
+ useCache,
767
+ // When decrypting state we expect key metadata to be present.
768
+ // If it isn't present, we assume that the Snap state we are decrypting is old enough to use the legacy encryption params.
769
+ keyMetadata: keyMetadata ?? LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS,
770
+ });
771
+ const decryptedState = await this.#encryptor.decryptWithKey(key, parsed);
772
+ // We assume this to be valid JSON, since all RPC requests from a Snap are validated and sanitized.
773
+ return decryptedState;
774
+ }
775
+ catch {
776
+ throw rpcErrors.internal({
777
+ message: 'Failed to decrypt snap state, the state must be corrupted.',
778
+ });
779
+ }
780
+ }
781
+ /**
782
+ * Encrypt a JSON state object for a given Snap.
783
+ *
784
+ * Note: This function does not assert the validity of the object,
785
+ * please ensure only valid JSON is passed to it.
786
+ *
787
+ * @param snapId - The Snap ID.
788
+ * @param state - The state object.
789
+ * @returns A string containing the encrypted JSON object.
790
+ */
791
+ async #encryptSnapState(snapId, state) {
792
+ const { key, salt } = await this.#getSnapEncryptionKey({
793
+ snapId,
794
+ useCache: true,
795
+ });
796
+ const encryptedState = await this.#encryptor.encryptWithKey(key, state);
797
+ encryptedState.salt = salt;
798
+ return JSON.stringify(encryptedState);
799
+ }
800
+ /**
801
+ * Get the new Snap state to persist based on the given state and encryption
802
+ * flag.
803
+ *
804
+ * - If the state is null, return null.
805
+ * - If the state should be encrypted, return the encrypted state.
806
+ * - Otherwise, if the state should not be encrypted, return the JSON-
807
+ * stringified state.
808
+ *
809
+ * @param snapId - The Snap ID.
810
+ * @param state - The state to persist.
811
+ * @param encrypted - A flag to indicate whether to use encrypted storage or
812
+ * not.
813
+ * @returns The state to persist.
814
+ */
815
+ async #getStateToPersist(snapId, state, encrypted) {
816
+ if (state === null) {
817
+ return null;
818
+ }
819
+ if (encrypted) {
820
+ return await this.#encryptSnapState(snapId, state);
821
+ }
822
+ return JSON.stringify(state);
823
+ }
824
+ /**
825
+ * Persist the state of a Snap.
826
+ *
827
+ * This function is debounced per Snap, meaning that multiple calls to this
828
+ * function for the same Snap will only result in one state update. It also
829
+ * uses a mutex to ensure that only one state update per Snap is processed at
830
+ * a time, avoiding possible race conditions.
831
+ *
832
+ * @param snapId - The Snap ID.
833
+ * @param newSnapState - The new state of the Snap.
834
+ * @param encrypted - A flag to indicate whether to use encrypted storage or
835
+ * not.
836
+ */
837
+ #persistSnapState = debouncePersistState((snapId, newSnapState, encrypted) => {
838
+ const runtime = this.#getRuntimeExpect(snapId);
839
+ runtime.stateMutex
840
+ .runExclusive(async () => {
841
+ const newState = await this.#getStateToPersist(snapId, newSnapState, encrypted);
842
+ if (encrypted) {
843
+ return this.update((state) => {
844
+ state.snapStates[snapId] = newState;
845
+ });
846
+ }
847
+ return this.update((state) => {
848
+ state.unencryptedSnapStates[snapId] = newState;
849
+ });
850
+ })
851
+ .catch(logError);
852
+ }, STATE_DEBOUNCE_TIMEOUT);
437
853
  /**
438
854
  * Updates the own state of the snap with the given id.
439
855
  * This is distinct from the state MetaMask uses to manage snaps.
@@ -443,14 +859,14 @@ export class SnapController extends BaseController {
443
859
  * @param encrypted - A flag to indicate whether to use encrypted storage or not.
444
860
  */
445
861
  async updateSnapState(snapId, newSnapState, encrypted) {
446
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
862
+ const runtime = this.#getRuntimeExpect(snapId);
447
863
  if (encrypted) {
448
864
  runtime.state = newSnapState;
449
865
  }
450
866
  else {
451
867
  runtime.unencryptedState = newSnapState;
452
868
  }
453
- __classPrivateFieldGet(this, _SnapController_persistSnapState, "f").call(this, snapId, newSnapState, encrypted);
869
+ this.#persistSnapState(snapId, newSnapState, encrypted);
454
870
  }
455
871
  /**
456
872
  * Clears the state of the snap with the given id.
@@ -460,14 +876,14 @@ export class SnapController extends BaseController {
460
876
  * @param encrypted - A flag to indicate whether to use encrypted storage or not.
461
877
  */
462
878
  clearSnapState(snapId, encrypted) {
463
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
879
+ const runtime = this.#getRuntimeExpect(snapId);
464
880
  if (encrypted) {
465
881
  runtime.state = null;
466
882
  }
467
883
  else {
468
884
  runtime.unencryptedState = null;
469
885
  }
470
- __classPrivateFieldGet(this, _SnapController_persistSnapState, "f").call(this, snapId, null, encrypted);
886
+ this.#persistSnapState(snapId, null, encrypted);
471
887
  }
472
888
  /**
473
889
  * Gets the own state of the snap with the given id.
@@ -478,7 +894,7 @@ export class SnapController extends BaseController {
478
894
  * @returns The requested snap state or null if no state exists.
479
895
  */
480
896
  async getSnapState(snapId, encrypted) {
481
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
897
+ const runtime = this.#getRuntimeExpect(snapId);
482
898
  return await runtime.getStateMutex.runExclusive(async () => {
483
899
  const cachedState = encrypted ? runtime.state : runtime.unencryptedState;
484
900
  if (cachedState !== undefined) {
@@ -497,7 +913,7 @@ export class SnapController extends BaseController {
497
913
  runtime.unencryptedState = json;
498
914
  return json;
499
915
  }
500
- const decrypted = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_decryptSnapState).call(this, snapId, state);
916
+ const decrypted = await this.#decryptSnapState(snapId, state);
501
917
  // eslint-disable-next-line require-atomic-updates
502
918
  runtime.state = decrypted;
503
919
  return decrypted;
@@ -544,23 +960,23 @@ export class SnapController extends BaseController {
544
960
  */
545
961
  async clearState() {
546
962
  const snapIds = Object.keys(this.state.snaps);
547
- if (__classPrivateFieldGet(this, _SnapController_closeAllConnections, "f")) {
963
+ if (this.#closeAllConnections) {
548
964
  snapIds.forEach((snapId) => {
549
- __classPrivateFieldGet(this, _SnapController_closeAllConnections, "f")?.call(this, snapId);
965
+ this.#closeAllConnections?.(snapId);
550
966
  });
551
967
  }
552
968
  await this.messagingSystem.call('ExecutionService:terminateAllSnaps');
553
- snapIds.forEach((snapId) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_revokeAllSnapPermissions).call(this, snapId));
969
+ snapIds.forEach((snapId) => this.#revokeAllSnapPermissions(snapId));
554
970
  this.update((state) => {
555
971
  state.snaps = {};
556
972
  state.snapStates = {};
557
973
  state.unencryptedSnapStates = {};
558
974
  });
559
- __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").clear();
560
- __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").clear();
975
+ this.#snapsRuntimeData.clear();
976
+ this.#rollbackSnapshots.clear();
561
977
  // We want to remove all snaps & permissions, except for preinstalled snaps
562
- if (__classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f")) {
563
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handlePreinstalledSnaps).call(this, __classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f"));
978
+ if (this.#preinstalledSnaps) {
979
+ this.#handlePreinstalledSnaps(this.#preinstalledSnaps);
564
980
  }
565
981
  }
566
982
  /**
@@ -594,9 +1010,9 @@ export class SnapController extends BaseController {
594
1010
  // it. This ensures that the snap will not be restarted or otherwise
595
1011
  // affect the host environment while we are deleting it.
596
1012
  await this.disableSnap(snapId);
597
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_revokeAllSnapPermissions).call(this, snapId);
598
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_removeSnapFromSubjects).call(this, snapId);
599
- __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").delete(snapId);
1013
+ this.#revokeAllSnapPermissions(snapId);
1014
+ this.#removeSnapFromSubjects(snapId);
1015
+ this.#snapsRuntimeData.delete(snapId);
600
1016
  this.update((state) => {
601
1017
  delete state.snaps[snapId];
602
1018
  delete state.snapStates[snapId];
@@ -608,6 +1024,47 @@ export class SnapController extends BaseController {
608
1024
  }
609
1025
  }));
610
1026
  }
1027
+ #handleInitialConnections(snapId, previousInitialConnections, initialConnections) {
1028
+ if (previousInitialConnections) {
1029
+ const revokedInitialConnections = setDiff(previousInitialConnections, initialConnections);
1030
+ for (const origin of Object.keys(revokedInitialConnections)) {
1031
+ this.removeSnapFromSubject(origin, snapId);
1032
+ }
1033
+ }
1034
+ for (const origin of Object.keys(initialConnections)) {
1035
+ this.#addSnapToSubject(origin, snapId);
1036
+ }
1037
+ }
1038
+ #addSnapToSubject(origin, snapId) {
1039
+ const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
1040
+ const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
1041
+ const subjectHasSnap = Boolean(existingCaveat?.value?.[snapId]);
1042
+ // If the subject is already connected to the snap, this is a no-op.
1043
+ if (subjectHasSnap) {
1044
+ return;
1045
+ }
1046
+ // If an existing caveat exists, we add the snap to that.
1047
+ if (existingCaveat) {
1048
+ this.messagingSystem.call('PermissionController:updateCaveat', origin, WALLET_SNAP_PERMISSION_KEY, SnapCaveatType.SnapIds, { ...existingCaveat.value, [snapId]: {} });
1049
+ return;
1050
+ }
1051
+ const approvedPermissions = {
1052
+ [WALLET_SNAP_PERMISSION_KEY]: {
1053
+ caveats: [
1054
+ {
1055
+ type: SnapCaveatType.SnapIds,
1056
+ value: {
1057
+ [snapId]: {},
1058
+ },
1059
+ },
1060
+ ],
1061
+ },
1062
+ };
1063
+ this.messagingSystem.call('PermissionController:grantPermissions', {
1064
+ approvedPermissions,
1065
+ subject: { origin },
1066
+ });
1067
+ }
611
1068
  /**
612
1069
  * Removes a snap's permission (caveat) from the specified subject.
613
1070
  *
@@ -644,18 +1101,39 @@ export class SnapController extends BaseController {
644
1101
  * @throws If non-dynamic permissions are passed.
645
1102
  */
646
1103
  revokeDynamicSnapPermissions(snapId, permissionNames) {
647
- assert(permissionNames.every((permissionName) => __classPrivateFieldGet(this, _SnapController_dynamicPermissions, "f").includes(permissionName)), 'Non-dynamic permissions cannot be revoked');
1104
+ assert(permissionNames.every((permissionName) => this.#dynamicPermissions.includes(permissionName)), 'Non-dynamic permissions cannot be revoked');
648
1105
  this.messagingSystem.call('PermissionController:revokePermissions', {
649
1106
  [snapId]: permissionNames,
650
1107
  });
651
1108
  }
1109
+ /**
1110
+ * Removes a snap's permission (caveat) from all subjects.
1111
+ *
1112
+ * @param snapId - The id of the Snap.
1113
+ */
1114
+ #removeSnapFromSubjects(snapId) {
1115
+ const subjects = this.messagingSystem.call('PermissionController:getSubjectNames');
1116
+ for (const subject of subjects) {
1117
+ this.removeSnapFromSubject(subject, snapId);
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Safely revokes all permissions granted to a Snap.
1122
+ *
1123
+ * @param snapId - The snap ID.
1124
+ */
1125
+ #revokeAllSnapPermissions(snapId) {
1126
+ if (this.messagingSystem.call('PermissionController:hasPermissions', snapId)) {
1127
+ this.messagingSystem.call('PermissionController:revokeAllPermissions', snapId);
1128
+ }
1129
+ }
652
1130
  /**
653
1131
  * Handles incrementing the activeReferences counter.
654
1132
  *
655
1133
  * @param snapId - The snap id of the snap that was referenced.
656
1134
  */
657
1135
  incrementActiveReferences(snapId) {
658
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1136
+ const runtime = this.#getRuntimeExpect(snapId);
659
1137
  runtime.activeReferences += 1;
660
1138
  }
661
1139
  /**
@@ -664,7 +1142,7 @@ export class SnapController extends BaseController {
664
1142
  * @param snapId - The snap id of the snap that was referenced..
665
1143
  */
666
1144
  decrementActiveReferences(snapId) {
667
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1145
+ const runtime = this.#getRuntimeExpect(snapId);
668
1146
  assert(runtime.activeReferences > 0, 'SnapController reference management is in an invalid state.');
669
1147
  runtime.activeReferences -= 1;
670
1148
  }
@@ -713,7 +1191,7 @@ export class SnapController extends BaseController {
713
1191
  * snap couldn't be installed.
714
1192
  */
715
1193
  async installSnaps(origin, requestedSnaps) {
716
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1194
+ this.#assertCanUsePlatform();
717
1195
  const result = {};
718
1196
  const snapIds = Object.keys(requestedSnaps);
719
1197
  const pendingUpdates = [];
@@ -725,23 +1203,23 @@ export class SnapController extends BaseController {
725
1203
  if (error) {
726
1204
  throw rpcErrors.invalidParams(`The "version" field must be a valid SemVer version range if specified. Received: "${rawVersion}".`);
727
1205
  }
728
- const location = __classPrivateFieldGet(this, _SnapController_detectSnapLocation, "f").call(this, snapId, {
1206
+ const location = this.#detectSnapLocation(snapId, {
729
1207
  versionRange: version,
730
- fetch: __classPrivateFieldGet(this, _SnapController_fetchFunction, "f"),
731
- allowLocal: __classPrivateFieldGet(this, _SnapController_featureFlags, "f").allowLocalSnaps,
732
- resolveVersion: async (range) => __classPrivateFieldGet(this, _SnapController_featureFlags, "f").requireAllowlist
733
- ? await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_resolveAllowlistVersion).call(this, snapId, range)
1208
+ fetch: this.#fetchFunction,
1209
+ allowLocal: this.#featureFlags.allowLocalSnaps,
1210
+ resolveVersion: async (range) => this.#featureFlags.requireAllowlist
1211
+ ? await this.#resolveAllowlistVersion(snapId, range)
734
1212
  : range,
735
1213
  });
736
1214
  // Existing snaps may need to be updated, unless they should be re-installed (e.g. local snaps)
737
1215
  // Everything else is treated as an install
738
1216
  const isUpdate = this.has(snapId) && !location.shouldAlwaysReload;
739
- if (isUpdate && __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_isValidUpdate).call(this, snapId, version)) {
1217
+ if (isUpdate && this.#isValidUpdate(snapId, version)) {
740
1218
  const existingSnap = this.getExpect(snapId);
741
1219
  pendingUpdates.push({ snapId, oldVersion: existingSnap.version });
742
- let rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1220
+ let rollbackSnapshot = this.#getRollbackSnapshot(snapId);
743
1221
  if (rollbackSnapshot === undefined) {
744
- rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createRollbackSnapshot).call(this, snapId);
1222
+ rollbackSnapshot = this.#createRollbackSnapshot(snapId);
745
1223
  rollbackSnapshot.newVersion = version;
746
1224
  }
747
1225
  else {
@@ -756,16 +1234,16 @@ export class SnapController extends BaseController {
756
1234
  // Once we finish all installs / updates, emit events.
757
1235
  pendingInstalls.forEach((snapId) => this.messagingSystem.publish(`SnapController:snapInstalled`, this.getTruncatedExpect(snapId), origin, false));
758
1236
  pendingUpdates.forEach(({ snapId, oldVersion }) => this.messagingSystem.publish(`SnapController:snapUpdated`, this.getTruncatedExpect(snapId), oldVersion, origin, false));
759
- snapIds.forEach((snapId) => __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").delete(snapId));
1237
+ snapIds.forEach((snapId) => this.#rollbackSnapshots.delete(snapId));
760
1238
  }
761
1239
  catch (error) {
762
1240
  const installed = pendingInstalls.filter((snapId) => this.has(snapId));
763
1241
  await this.removeSnaps(installed);
764
- const snapshottedSnaps = [...__classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").keys()];
1242
+ const snapshottedSnaps = [...this.#rollbackSnapshots.keys()];
765
1243
  const snapsToRollback = pendingUpdates
766
1244
  .map(({ snapId }) => snapId)
767
1245
  .filter((snapId) => snapshottedSnaps.includes(snapId));
768
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_rollbackSnaps).call(this, snapsToRollback);
1246
+ await this.#rollbackSnaps(snapsToRollback);
769
1247
  throw error;
770
1248
  }
771
1249
  return result;
@@ -798,8 +1276,8 @@ export class SnapController extends BaseController {
798
1276
  // and we don't want to emit events prematurely.
799
1277
  false);
800
1278
  }
801
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanInstallSnaps).call(this);
802
- let pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1279
+ this.#assertCanInstallSnaps();
1280
+ let pendingApproval = this.#createApproval({
803
1281
  origin,
804
1282
  snapId,
805
1283
  type: SNAP_APPROVAL_INSTALL,
@@ -811,27 +1289,27 @@ export class SnapController extends BaseController {
811
1289
  }
812
1290
  // Existing snaps that should be re-installed should not maintain their existing permissions
813
1291
  if (existingSnap && location.shouldAlwaysReload) {
814
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_revokeAllSnapPermissions).call(this, snapId);
1292
+ this.#revokeAllSnapPermissions(snapId);
815
1293
  }
816
1294
  try {
817
- const { sourceCode } = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_add).call(this, {
1295
+ const { sourceCode } = await this.#add({
818
1296
  origin,
819
1297
  id: snapId,
820
1298
  location,
821
1299
  versionRange,
822
1300
  });
823
1301
  await this.authorize(snapId, pendingApproval);
824
- pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1302
+ pendingApproval = this.#createApproval({
825
1303
  origin,
826
1304
  snapId,
827
1305
  type: SNAP_APPROVAL_RESULT,
828
1306
  });
829
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_startSnap).call(this, {
1307
+ await this.#startSnap({
830
1308
  snapId,
831
1309
  sourceCode,
832
1310
  });
833
1311
  const truncated = this.getTruncatedExpect(snapId);
834
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1312
+ this.#updateApproval(pendingApproval.id, {
835
1313
  loading: false,
836
1314
  type: SNAP_APPROVAL_INSTALL,
837
1315
  });
@@ -840,7 +1318,7 @@ export class SnapController extends BaseController {
840
1318
  catch (error) {
841
1319
  logError(`Error when adding ${snapId}.`, error);
842
1320
  const errorString = error instanceof Error ? error.message : error.toString();
843
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1321
+ this.#updateApproval(pendingApproval.id, {
844
1322
  loading: false,
845
1323
  type: SNAP_APPROVAL_INSTALL,
846
1324
  error: errorString,
@@ -849,6 +1327,34 @@ export class SnapController extends BaseController {
849
1327
  throw error;
850
1328
  }
851
1329
  }
1330
+ #createApproval({ origin, snapId, type, }) {
1331
+ const id = nanoid();
1332
+ const promise = this.messagingSystem.call('ApprovalController:addRequest', {
1333
+ origin,
1334
+ id,
1335
+ type,
1336
+ requestData: {
1337
+ // Mirror previous installation metadata
1338
+ metadata: { id, origin: snapId, dappOrigin: origin },
1339
+ snapId,
1340
+ },
1341
+ requestState: {
1342
+ loading: true,
1343
+ },
1344
+ }, true);
1345
+ return { id, promise };
1346
+ }
1347
+ #updateApproval(id, requestState) {
1348
+ try {
1349
+ this.messagingSystem.call('ApprovalController:updateRequestState', {
1350
+ id,
1351
+ requestState,
1352
+ });
1353
+ }
1354
+ catch {
1355
+ // Do nothing
1356
+ }
1357
+ }
852
1358
  /**
853
1359
  * Updates an installed snap. The flow is similar to
854
1360
  * {@link SnapController.installSnaps}. The user will be asked if they want
@@ -869,8 +1375,8 @@ export class SnapController extends BaseController {
869
1375
  * @returns The snap metadata if updated, `null` otherwise.
870
1376
  */
871
1377
  async updateSnap(origin, snapId, location, newVersionRange = DEFAULT_REQUESTED_SNAP_VERSION, emitEvent = true) {
872
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanInstallSnaps).call(this);
873
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1378
+ this.#assertCanInstallSnaps();
1379
+ this.#assertCanUsePlatform();
874
1380
  const snap = this.getExpect(snapId);
875
1381
  if (snap.preinstalled) {
876
1382
  throw new Error('Preinstalled Snaps cannot be manually updated.');
@@ -878,7 +1384,7 @@ export class SnapController extends BaseController {
878
1384
  if (!isValidSemVerRange(newVersionRange)) {
879
1385
  throw new Error(`Received invalid snap version range: "${newVersionRange}".`);
880
1386
  }
881
- let pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1387
+ let pendingApproval = this.#createApproval({
882
1388
  origin,
883
1389
  snapId,
884
1390
  type: SNAP_APPROVAL_UPDATE,
@@ -896,17 +1402,17 @@ export class SnapController extends BaseController {
896
1402
  if (!satisfiesVersionRange(newVersion, newVersionRange)) {
897
1403
  throw new Error(`Version mismatch. Manifest for "${snapId}" specifies version "${newVersion}" which doesn't satisfy requested version range "${newVersionRange}".`);
898
1404
  }
899
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertIsInstallAllowed).call(this, snapId, {
1405
+ await this.#assertIsInstallAllowed(snapId, {
900
1406
  version: newVersion,
901
1407
  checksum: manifest.source.shasum,
902
1408
  permissions: manifest.initialPermissions,
903
1409
  platformVersion: manifest.platformVersion,
904
1410
  });
905
1411
  const processedPermissions = processSnapPermissions(manifest.initialPermissions);
906
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validateSnapPermissions).call(this, processedPermissions);
907
- const { newPermissions, unusedPermissions, approvedPermissions } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_calculatePermissionsChange).call(this, snapId, processedPermissions);
908
- const { newConnections, unusedConnections, approvedConnections } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_calculateConnectionsChange).call(this, snapId, oldManifest.initialConnections ?? {}, manifest.initialConnections ?? {});
909
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1412
+ this.#validateSnapPermissions(processedPermissions);
1413
+ const { newPermissions, unusedPermissions, approvedPermissions } = this.#calculatePermissionsChange(snapId, processedPermissions);
1414
+ const { newConnections, unusedConnections, approvedConnections } = this.#calculateConnectionsChange(snapId, oldManifest.initialConnections ?? {}, manifest.initialConnections ?? {});
1415
+ this.#updateApproval(pendingApproval.id, {
910
1416
  permissions: newPermissions,
911
1417
  newVersion: manifest.version,
912
1418
  newPermissions,
@@ -918,7 +1424,7 @@ export class SnapController extends BaseController {
918
1424
  loading: false,
919
1425
  });
920
1426
  const { permissions: approvedNewPermissions, ...requestData } = (await pendingApproval.promise);
921
- pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1427
+ pendingApproval = this.#createApproval({
922
1428
  origin,
923
1429
  snapId,
924
1430
  type: SNAP_APPROVAL_RESULT,
@@ -926,23 +1432,23 @@ export class SnapController extends BaseController {
926
1432
  if (this.isRunning(snapId)) {
927
1433
  await this.stopSnap(snapId, SnapStatusEvents.Stop);
928
1434
  }
929
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, SnapStatusEvents.Update);
930
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_set).call(this, {
1435
+ this.#transition(snapId, SnapStatusEvents.Update);
1436
+ this.#set({
931
1437
  origin,
932
1438
  id: snapId,
933
1439
  files: newSnap,
934
1440
  isUpdate: true,
935
1441
  });
936
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, {
1442
+ this.#updatePermissions({
937
1443
  snapId,
938
1444
  unusedPermissions,
939
1445
  newPermissions: approvedNewPermissions,
940
1446
  requestData,
941
1447
  });
942
1448
  if (manifest.initialConnections) {
943
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleInitialConnections).call(this, snapId, oldManifest.initialConnections ?? null, manifest.initialConnections);
1449
+ this.#handleInitialConnections(snapId, oldManifest.initialConnections ?? null, manifest.initialConnections);
944
1450
  }
945
- const rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1451
+ const rollbackSnapshot = this.#getRollbackSnapshot(snapId);
946
1452
  if (rollbackSnapshot !== undefined) {
947
1453
  rollbackSnapshot.permissions.revoked = unusedPermissions;
948
1454
  rollbackSnapshot.permissions.granted = approvedNewPermissions;
@@ -951,7 +1457,7 @@ export class SnapController extends BaseController {
951
1457
  const sourceCode = sourceCodeFile.toString();
952
1458
  assert(typeof sourceCode === 'string' && sourceCode.length > 0, `Invalid source code for snap "${snapId}".`);
953
1459
  try {
954
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_startSnap).call(this, { snapId, sourceCode });
1460
+ await this.#startSnap({ snapId, sourceCode });
955
1461
  }
956
1462
  catch {
957
1463
  throw new Error(`Snap ${snapId} crashed with updated source code.`);
@@ -960,7 +1466,7 @@ export class SnapController extends BaseController {
960
1466
  if (emitEvent) {
961
1467
  this.messagingSystem.publish('SnapController:snapUpdated', truncatedSnap, snap.version, origin, false);
962
1468
  }
963
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1469
+ this.#updateApproval(pendingApproval.id, {
964
1470
  loading: false,
965
1471
  type: SNAP_APPROVAL_UPDATE,
966
1472
  });
@@ -969,7 +1475,7 @@ export class SnapController extends BaseController {
969
1475
  catch (error) {
970
1476
  logError(`Error when updating ${snapId},`, error);
971
1477
  const errorString = error instanceof Error ? error.message : error.toString();
972
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1478
+ this.#updateApproval(pendingApproval.id, {
973
1479
  loading: false,
974
1480
  error: errorString,
975
1481
  type: SNAP_APPROVAL_UPDATE,
@@ -978,70 +1484,299 @@ export class SnapController extends BaseController {
978
1484
  throw error;
979
1485
  }
980
1486
  }
1487
+ async #resolveAllowlistVersion(snapId, versionRange) {
1488
+ return await this.messagingSystem.call('SnapsRegistry:resolveVersion', snapId, versionRange);
1489
+ }
981
1490
  /**
982
- * Initiates a request for the given snap's initial permissions.
983
- * Must be called in order. See processRequestedSnap.
1491
+ * Returns a promise representing the complete installation of the requested snap.
1492
+ * If the snap is already being installed, the previously pending promise will be returned.
984
1493
  *
985
- * This function is not hash private yet because of tests.
986
- *
987
- * @param snapId - The id of the Snap.
988
- * @param pendingApproval - Pending approval to update.
989
- * @returns The snap's approvedPermissions.
1494
+ * @param args - Object containing the snap id and either the URL of the snap's manifest,
1495
+ * or the snap's manifest and source code. The object may also optionally contain a target
1496
+ * version.
1497
+ * @returns The resulting snap object.
990
1498
  */
991
- // eslint-disable-next-line no-restricted-syntax
992
- async authorize(snapId, pendingApproval) {
993
- log(`Authorizing snap: ${snapId}`);
994
- const snapsState = this.state.snaps;
995
- const snap = snapsState[snapId];
996
- const { initialPermissions, initialConnections } = snap;
1499
+ async #add(args) {
1500
+ const { id: snapId, location, versionRange } = args;
1501
+ this.#setupRuntime(snapId);
1502
+ const runtime = this.#getRuntimeExpect(snapId);
1503
+ if (!runtime.installPromise) {
1504
+ log(`Adding snap: ${snapId}`);
1505
+ // If fetching and setting the snap succeeds, this property will be set
1506
+ // to null in the authorize() method.
1507
+ runtime.installPromise = (async () => {
1508
+ const fetchedSnap = await fetchSnap(snapId, location);
1509
+ const manifest = fetchedSnap.manifest.result;
1510
+ if (!satisfiesVersionRange(manifest.version, versionRange)) {
1511
+ throw new Error(`Version mismatch. Manifest for "${snapId}" specifies version "${manifest.version}" which doesn't satisfy requested version range "${versionRange}".`);
1512
+ }
1513
+ await this.#assertIsInstallAllowed(snapId, {
1514
+ version: manifest.version,
1515
+ checksum: manifest.source.shasum,
1516
+ permissions: manifest.initialPermissions,
1517
+ platformVersion: manifest.platformVersion,
1518
+ });
1519
+ return this.#set({
1520
+ ...args,
1521
+ files: fetchedSnap,
1522
+ id: snapId,
1523
+ });
1524
+ })();
1525
+ }
997
1526
  try {
998
- const processedPermissions = processSnapPermissions(initialPermissions);
999
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validateSnapPermissions).call(this, processedPermissions);
1000
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1001
- loading: false,
1002
- connections: initialConnections ?? {},
1003
- permissions: processedPermissions,
1004
- });
1005
- const { permissions: approvedPermissions, ...requestData } = (await pendingApproval.promise);
1006
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, {
1007
- snapId,
1008
- newPermissions: approvedPermissions,
1009
- requestData,
1010
- });
1011
- if (snap.manifest.initialConnections) {
1012
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleInitialConnections).call(this, snapId, null, snap.manifest.initialConnections);
1013
- }
1527
+ return await runtime.installPromise;
1014
1528
  }
1015
- finally {
1016
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1529
+ catch (error) {
1530
+ // Reset promise so users can retry installation in case the problem is
1531
+ // temporary.
1017
1532
  runtime.installPromise = null;
1533
+ throw error;
1018
1534
  }
1019
1535
  }
1020
- destroy() {
1021
- super.destroy();
1022
- if (__classPrivateFieldGet(this, _SnapController_timeoutForLastRequestStatus, "f")) {
1023
- clearTimeout(__classPrivateFieldGet(this, _SnapController_timeoutForLastRequestStatus, "f"));
1536
+ async #startSnap(snapData) {
1537
+ const { snapId } = snapData;
1538
+ if (this.isRunning(snapId)) {
1539
+ throw new Error(`Snap "${snapId}" is already started.`);
1540
+ }
1541
+ try {
1542
+ const runtime = this.#getRuntimeExpect(snapId);
1543
+ const result = await this.messagingSystem.call('ExecutionService:executeSnap', {
1544
+ ...snapData,
1545
+ endowments: await this.#getEndowments(snapId),
1546
+ });
1547
+ this.#transition(snapId, SnapStatusEvents.Start);
1548
+ // We treat the initialization of the snap as the first request, for idle timing purposes.
1549
+ runtime.lastRequest = Date.now();
1550
+ return result;
1551
+ }
1552
+ catch (error) {
1553
+ await this.#terminateSnap(snapId);
1554
+ throw error;
1024
1555
  }
1025
- /* eslint-disable @typescript-eslint/unbound-method */
1026
- this.messagingSystem.unsubscribe('ExecutionService:unhandledError', this._onUnhandledSnapError);
1027
- this.messagingSystem.unsubscribe('ExecutionService:outboundRequest', this._onOutboundRequest);
1028
- this.messagingSystem.unsubscribe('ExecutionService:outboundResponse', this._onOutboundResponse);
1029
- this.messagingSystem.clearEventSubscriptions('SnapController:snapInstalled');
1030
- this.messagingSystem.clearEventSubscriptions('SnapController:snapUpdated');
1031
- /* eslint-enable @typescript-eslint/unbound-method */
1032
1556
  }
1033
1557
  /**
1034
- * Passes a JSON-RPC request object to the RPC handler function of a snap.
1558
+ * Gets the names of all endowments that will be added to the Snap's
1559
+ * Compartment when it executes. These should be the names of global
1560
+ * JavaScript APIs accessible in the root realm of the execution environment.
1035
1561
  *
1036
- * @param options - A bag of options.
1037
- * @param options.snapId - The ID of the recipient snap.
1038
- * @param options.origin - The origin of the RPC request.
1039
- * @param options.handler - The handler to trigger on the snap for the request.
1040
- * @param options.request - The JSON-RPC request object.
1041
- * @returns The result of the JSON-RPC request.
1562
+ * Throws an error if the endowment getter for a permission returns a truthy
1563
+ * value that is not an array of strings.
1564
+ *
1565
+ * @param snapId - The id of the snap whose SES endowments to get.
1566
+ * @returns An array of the names of the endowments.
1042
1567
  */
1043
- async handleRequest({ snapId, origin, handler: handlerType, request: rawRequest, }) {
1044
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1568
+ async #getEndowments(snapId) {
1569
+ let allEndowments = [];
1570
+ for (const permissionName of this.#environmentEndowmentPermissions) {
1571
+ if (this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName)) {
1572
+ const endowments = await this.messagingSystem.call('PermissionController:getEndowments', snapId, permissionName);
1573
+ if (endowments) {
1574
+ // We don't have any guarantees about the type of the endowments
1575
+ // value, so we have to guard at runtime.
1576
+ if (!Array.isArray(endowments) ||
1577
+ endowments.some((value) => typeof value !== 'string')) {
1578
+ throw new Error('Expected an array of string endowment names.');
1579
+ }
1580
+ allEndowments = allEndowments.concat(endowments);
1581
+ }
1582
+ }
1583
+ }
1584
+ const dedupedEndowments = [
1585
+ ...new Set([...DEFAULT_ENDOWMENTS, ...allEndowments]),
1586
+ ];
1587
+ if (dedupedEndowments.length <
1588
+ DEFAULT_ENDOWMENTS.length + allEndowments.length) {
1589
+ logError(`Duplicate endowments found for ${snapId}. Default endowments should not be requested.`, allEndowments);
1590
+ }
1591
+ return dedupedEndowments;
1592
+ }
1593
+ /**
1594
+ * Sets a snap in state. Called when a snap is installed or updated. Performs
1595
+ * various validation checks on the received arguments, and will throw if
1596
+ * validation fails.
1597
+ *
1598
+ * The snap will be enabled and unblocked by the time this method returns,
1599
+ * regardless of its previous state.
1600
+ *
1601
+ * See {@link SnapController.add} and {@link SnapController.updateSnap} for
1602
+ * usage.
1603
+ *
1604
+ * @param args - The add snap args.
1605
+ * @returns The resulting snap object.
1606
+ */
1607
+ #set(args) {
1608
+ const { id: snapId, origin, files, isUpdate = false, removable, preinstalled, hidden, hideSnapBranding, } = args;
1609
+ const { manifest, sourceCode: sourceCodeFile, svgIcon, auxiliaryFiles: rawAuxiliaryFiles, localizationFiles, } = files;
1610
+ assertIsSnapManifest(manifest.result);
1611
+ const { version } = manifest.result;
1612
+ const sourceCode = sourceCodeFile.toString();
1613
+ assert(typeof sourceCode === 'string' && sourceCode.length > 0, `Invalid source code for snap "${snapId}".`);
1614
+ const auxiliaryFiles = rawAuxiliaryFiles.map((file) => {
1615
+ assert(typeof file.data.base64 === 'string');
1616
+ return {
1617
+ path: file.path,
1618
+ value: file.data.base64,
1619
+ };
1620
+ });
1621
+ const snapsState = this.state.snaps;
1622
+ const existingSnap = snapsState[snapId];
1623
+ const previousVersionHistory = existingSnap?.versionHistory ?? [];
1624
+ const versionHistory = [
1625
+ ...previousVersionHistory,
1626
+ {
1627
+ version,
1628
+ date: Date.now(),
1629
+ origin,
1630
+ },
1631
+ ];
1632
+ const localizedFiles = localizationFiles.map((file) => file.result);
1633
+ const snap = {
1634
+ // Restore relevant snap state if it exists
1635
+ ...existingSnap,
1636
+ // Note that the snap will be unblocked and enabled, regardless of its
1637
+ // previous state.
1638
+ blocked: false,
1639
+ enabled: true,
1640
+ removable,
1641
+ preinstalled,
1642
+ hidden,
1643
+ hideSnapBranding,
1644
+ id: snapId,
1645
+ initialConnections: manifest.result.initialConnections,
1646
+ initialPermissions: manifest.result.initialPermissions,
1647
+ manifest: manifest.result,
1648
+ status: this.#statusMachine.config.initial,
1649
+ sourceCode,
1650
+ version,
1651
+ versionHistory,
1652
+ auxiliaryFiles,
1653
+ localizationFiles: localizedFiles,
1654
+ };
1655
+ // If the snap was blocked, it isn't any longer
1656
+ delete snap.blockInformation;
1657
+ // store the snap back in state
1658
+ const { inversePatches } = this.update((state) => {
1659
+ state.snaps[snapId] = snap;
1660
+ });
1661
+ // checking for isUpdate here as this function is also used in
1662
+ // the install flow, we do not care to create snapshots for installs
1663
+ if (isUpdate) {
1664
+ const rollbackSnapshot = this.#getRollbackSnapshot(snapId);
1665
+ if (rollbackSnapshot !== undefined) {
1666
+ rollbackSnapshot.statePatches = inversePatches;
1667
+ }
1668
+ }
1669
+ // In case the Snap uses a localized manifest, we need to get the
1670
+ // proposed name from the localized manifest.
1671
+ const { proposedName } = getLocalizedSnapManifest(manifest.result, 'en', localizedFiles);
1672
+ this.messagingSystem.call('SubjectMetadataController:addSubjectMetadata', {
1673
+ subjectType: SubjectType.Snap,
1674
+ name: proposedName,
1675
+ origin: snap.id,
1676
+ version,
1677
+ svgIcon: svgIcon?.toString() ?? null,
1678
+ });
1679
+ return { ...snap, sourceCode };
1680
+ }
1681
+ #validateSnapPermissions(processedPermissions) {
1682
+ const permissionKeys = Object.keys(processedPermissions);
1683
+ const handlerPermissions = Array.from(new Set(Object.values(handlerEndowments)));
1684
+ assert(permissionKeys.some((key) => handlerPermissions.includes(key)), `A snap must request at least one of the following permissions: ${handlerPermissions
1685
+ .filter((handler) => handler !== null)
1686
+ .join(', ')}.`);
1687
+ const excludedPermissionErrors = permissionKeys.reduce((errors, permission) => {
1688
+ if (hasProperty(this.#excludedPermissions, permission)) {
1689
+ errors.push(this.#excludedPermissions[permission]);
1690
+ }
1691
+ return errors;
1692
+ }, []);
1693
+ assert(excludedPermissionErrors.length === 0, `One or more permissions are not allowed:\n${excludedPermissionErrors.join('\n')}`);
1694
+ }
1695
+ /**
1696
+ * Validate that the platform version specified in the manifest (if any) is
1697
+ * compatible with the current platform version.
1698
+ *
1699
+ * @param snapId - The ID of the Snap.
1700
+ * @param platformVersion - The platform version to validate against.
1701
+ * @throws If the platform version is greater than the current platform
1702
+ * version.
1703
+ */
1704
+ #validatePlatformVersion(snapId, platformVersion) {
1705
+ if (platformVersion === undefined) {
1706
+ return;
1707
+ }
1708
+ if (gt(platformVersion, getPlatformVersion())) {
1709
+ const message = `The Snap "${snapId}" requires platform version "${platformVersion}" which is greater than the current platform version "${getPlatformVersion()}".`;
1710
+ if (this.#featureFlags.rejectInvalidPlatformVersion) {
1711
+ throw new Error(message);
1712
+ }
1713
+ logWarning(message);
1714
+ }
1715
+ }
1716
+ /**
1717
+ * Initiates a request for the given snap's initial permissions.
1718
+ * Must be called in order. See processRequestedSnap.
1719
+ *
1720
+ * This function is not hash private yet because of tests.
1721
+ *
1722
+ * @param snapId - The id of the Snap.
1723
+ * @param pendingApproval - Pending approval to update.
1724
+ * @returns The snap's approvedPermissions.
1725
+ */
1726
+ // eslint-disable-next-line no-restricted-syntax
1727
+ async authorize(snapId, pendingApproval) {
1728
+ log(`Authorizing snap: ${snapId}`);
1729
+ const snapsState = this.state.snaps;
1730
+ const snap = snapsState[snapId];
1731
+ const { initialPermissions, initialConnections } = snap;
1732
+ try {
1733
+ const processedPermissions = processSnapPermissions(initialPermissions);
1734
+ this.#validateSnapPermissions(processedPermissions);
1735
+ this.#updateApproval(pendingApproval.id, {
1736
+ loading: false,
1737
+ connections: initialConnections ?? {},
1738
+ permissions: processedPermissions,
1739
+ });
1740
+ const { permissions: approvedPermissions, ...requestData } = (await pendingApproval.promise);
1741
+ this.#updatePermissions({
1742
+ snapId,
1743
+ newPermissions: approvedPermissions,
1744
+ requestData,
1745
+ });
1746
+ if (snap.manifest.initialConnections) {
1747
+ this.#handleInitialConnections(snapId, null, snap.manifest.initialConnections);
1748
+ }
1749
+ }
1750
+ finally {
1751
+ const runtime = this.#getRuntimeExpect(snapId);
1752
+ runtime.installPromise = null;
1753
+ }
1754
+ }
1755
+ destroy() {
1756
+ super.destroy();
1757
+ if (this.#timeoutForLastRequestStatus) {
1758
+ clearTimeout(this.#timeoutForLastRequestStatus);
1759
+ }
1760
+ /* eslint-disable @typescript-eslint/unbound-method */
1761
+ this.messagingSystem.unsubscribe('ExecutionService:unhandledError', this._onUnhandledSnapError);
1762
+ this.messagingSystem.unsubscribe('ExecutionService:outboundRequest', this._onOutboundRequest);
1763
+ this.messagingSystem.unsubscribe('ExecutionService:outboundResponse', this._onOutboundResponse);
1764
+ this.messagingSystem.clearEventSubscriptions('SnapController:snapInstalled');
1765
+ this.messagingSystem.clearEventSubscriptions('SnapController:snapUpdated');
1766
+ /* eslint-enable @typescript-eslint/unbound-method */
1767
+ }
1768
+ /**
1769
+ * Passes a JSON-RPC request object to the RPC handler function of a snap.
1770
+ *
1771
+ * @param options - A bag of options.
1772
+ * @param options.snapId - The ID of the recipient snap.
1773
+ * @param options.origin - The origin of the RPC request.
1774
+ * @param options.handler - The handler to trigger on the snap for the request.
1775
+ * @param options.request - The JSON-RPC request object.
1776
+ * @returns The result of the JSON-RPC request.
1777
+ */
1778
+ async handleRequest({ snapId, origin, handler: handlerType, request: rawRequest, }) {
1779
+ this.#assertCanUsePlatform();
1045
1780
  assert(origin === METAMASK_ORIGIN || isValidUrl(origin), "'origin' must be a valid URL or 'metamask'.");
1046
1781
  const request = {
1047
1782
  jsonrpc: '2.0',
@@ -1082,9 +1817,12 @@ export class SnapController extends BaseController {
1082
1817
  if (this.state.snaps[snapId].status === SnapStatus.Installing) {
1083
1818
  throw new Error(`Snap "${snapId}" is currently being installed. Please try again later.`);
1084
1819
  }
1085
- const timeout = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getExecutionTimeout).call(this, handlerPermissions);
1820
+ const timeout = this.#getExecutionTimeout(handlerPermissions);
1821
+ const runtime = this.#getRuntimeExpect(snapId);
1822
+ if (runtime.stopPromise) {
1823
+ await runtime.stopPromise;
1824
+ }
1086
1825
  if (!this.isRunning(snapId)) {
1087
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1088
1826
  if (!runtime.startPromise) {
1089
1827
  runtime.startPromise = this.startSnap(snapId);
1090
1828
  }
@@ -1095,9 +1833,9 @@ export class SnapController extends BaseController {
1095
1833
  runtime.startPromise = null;
1096
1834
  }
1097
1835
  }
1098
- const transformedRequest = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transformSnapRpcRequest).call(this, snapId, handlerType, request);
1836
+ const transformedRequest = this.#transformSnapRpcRequest(snapId, handlerType, request);
1099
1837
  const timer = new Timer(timeout);
1100
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_recordSnapRpcRequestStart).call(this, snapId, transformedRequest.id, timer);
1838
+ this.#recordSnapRpcRequestStart(snapId, transformedRequest.id, timer);
1101
1839
  const handleRpcRequestPromise = this.messagingSystem.call('ExecutionService:handleRpcRequest', snapId, { origin, handler: handlerType, request: transformedRequest });
1102
1840
  // This will either get the result or reject due to the timeout.
1103
1841
  try {
@@ -1105,1030 +1843,520 @@ export class SnapController extends BaseController {
1105
1843
  if (result === hasTimedOut) {
1106
1844
  throw new Error(`${snapId} failed to respond to the request in time.`);
1107
1845
  }
1108
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertSnapRpcResponse).call(this, snapId, handlerType, result);
1109
- const transformedResult = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transformSnapRpcResponse).call(this, snapId, handlerType, transformedRequest, result);
1110
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_recordSnapRpcRequestFinish).call(this, snapId, transformedRequest.id, handlerType, origin, true);
1846
+ await this.#assertSnapRpcResponse(snapId, handlerType, result);
1847
+ const transformedResult = await this.#transformSnapRpcResponse(snapId, handlerType, transformedRequest, result);
1848
+ this.#recordSnapRpcRequestFinish(snapId, transformedRequest.id, handlerType, origin, true);
1111
1849
  return transformedResult;
1112
1850
  }
1113
1851
  catch (error) {
1114
1852
  // We flag the RPC request as finished early since termination may affect pending requests
1115
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_recordSnapRpcRequestFinish).call(this, snapId, transformedRequest.id, handlerType, origin, false);
1853
+ this.#recordSnapRpcRequestFinish(snapId, transformedRequest.id, handlerType, origin, false);
1116
1854
  const [jsonRpcError, handled] = unwrapError(error);
1117
1855
  if (!handled) {
1856
+ logError(`"${snapId}" crashed due to an unhandled error:`, jsonRpcError);
1118
1857
  await this.stopSnap(snapId, SnapStatusEvents.Crash);
1119
1858
  }
1120
1859
  throw jsonRpcError;
1121
1860
  }
1122
1861
  }
1123
- }
1124
- _SnapController_closeAllConnections = new WeakMap(), _SnapController_dynamicPermissions = new WeakMap(), _SnapController_environmentEndowmentPermissions = new WeakMap(), _SnapController_excludedPermissions = new WeakMap(), _SnapController_featureFlags = new WeakMap(), _SnapController_fetchFunction = new WeakMap(), _SnapController_idleTimeCheckInterval = new WeakMap(), _SnapController_maxIdleTime = new WeakMap(), _SnapController_encryptor = new WeakMap(), _SnapController_getMnemonicSeed = new WeakMap(), _SnapController_getFeatureFlags = new WeakMap(), _SnapController_clientCryptography = new WeakMap(), _SnapController_detectSnapLocation = new WeakMap(), _SnapController_snapsRuntimeData = new WeakMap(), _SnapController_rollbackSnapshots = new WeakMap(), _SnapController_timeoutForLastRequestStatus = new WeakMap(), _SnapController_statusMachine = new WeakMap(), _SnapController_preinstalledSnaps = new WeakMap(), _SnapController_trackEvent = new WeakMap(), _SnapController_trackSnapExport = new WeakMap(), _SnapController_persistSnapState = new WeakMap(), _SnapController_instances = new WeakSet(), _SnapController_initializeStateMachine = function _SnapController_initializeStateMachine() {
1125
- const disableGuard = ({ snapId }) => {
1126
- return this.getExpect(snapId).enabled;
1127
- };
1128
- const statusConfig = {
1129
- initial: SnapStatus.Installing,
1130
- states: {
1131
- [SnapStatus.Installing]: {
1132
- on: {
1133
- [SnapStatusEvents.Start]: {
1134
- target: SnapStatus.Running,
1135
- cond: disableGuard,
1136
- },
1137
- },
1138
- },
1139
- [SnapStatus.Updating]: {
1140
- on: {
1141
- [SnapStatusEvents.Start]: {
1142
- target: SnapStatus.Running,
1143
- cond: disableGuard,
1144
- },
1145
- [SnapStatusEvents.Stop]: SnapStatus.Stopped,
1146
- },
1147
- },
1148
- [SnapStatus.Running]: {
1149
- on: {
1150
- [SnapStatusEvents.Stop]: SnapStatus.Stopped,
1151
- [SnapStatusEvents.Crash]: SnapStatus.Crashed,
1152
- },
1153
- },
1154
- [SnapStatus.Stopped]: {
1155
- on: {
1156
- [SnapStatusEvents.Start]: {
1157
- target: SnapStatus.Running,
1158
- cond: disableGuard,
1159
- },
1160
- [SnapStatusEvents.Update]: SnapStatus.Updating,
1161
- },
1162
- },
1163
- [SnapStatus.Crashed]: {
1164
- on: {
1165
- [SnapStatusEvents.Start]: {
1166
- target: SnapStatus.Running,
1167
- cond: disableGuard,
1168
- },
1169
- [SnapStatusEvents.Update]: SnapStatus.Updating,
1170
- },
1171
- },
1172
- },
1173
- };
1174
- __classPrivateFieldSet(this, _SnapController_statusMachine, createMachine(statusConfig), "f");
1175
- validateMachine(__classPrivateFieldGet(this, _SnapController_statusMachine, "f"));
1176
- }, _SnapController_registerMessageHandlers = function _SnapController_registerMessageHandlers() {
1177
- this.messagingSystem.registerActionHandler(`${controllerName}:clearSnapState`, (...args) => this.clearSnapState(...args));
1178
- this.messagingSystem.registerActionHandler(`${controllerName}:get`, (...args) => this.get(...args));
1179
- this.messagingSystem.registerActionHandler(`${controllerName}:getSnapState`, async (...args) => this.getSnapState(...args));
1180
- this.messagingSystem.registerActionHandler(`${controllerName}:handleRequest`, async (...args) => this.handleRequest(...args));
1181
- this.messagingSystem.registerActionHandler(`${controllerName}:has`, (...args) => this.has(...args));
1182
- this.messagingSystem.registerActionHandler(`${controllerName}:updateBlockedSnaps`, async () => this.updateBlockedSnaps());
1183
- this.messagingSystem.registerActionHandler(`${controllerName}:updateSnapState`, async (...args) => this.updateSnapState(...args));
1184
- this.messagingSystem.registerActionHandler(`${controllerName}:enable`, (...args) => this.enableSnap(...args));
1185
- this.messagingSystem.registerActionHandler(`${controllerName}:disable`, async (...args) => this.disableSnap(...args));
1186
- this.messagingSystem.registerActionHandler(`${controllerName}:remove`, async (...args) => this.removeSnap(...args));
1187
- this.messagingSystem.registerActionHandler(`${controllerName}:getPermitted`, (...args) => this.getPermittedSnaps(...args));
1188
- this.messagingSystem.registerActionHandler(`${controllerName}:install`, async (...args) => this.installSnaps(...args));
1189
- this.messagingSystem.registerActionHandler(`${controllerName}:getAll`, (...args) => this.getAllSnaps(...args));
1190
- this.messagingSystem.registerActionHandler(`${controllerName}:getRunnableSnaps`, (...args) => this.getRunnableSnaps(...args));
1191
- this.messagingSystem.registerActionHandler(`${controllerName}:incrementActiveReferences`, (...args) => this.incrementActiveReferences(...args));
1192
- this.messagingSystem.registerActionHandler(`${controllerName}:decrementActiveReferences`, (...args) => this.decrementActiveReferences(...args));
1193
- this.messagingSystem.registerActionHandler(`${controllerName}:disconnectOrigin`, (...args) => this.removeSnapFromSubject(...args));
1194
- this.messagingSystem.registerActionHandler(`${controllerName}:revokeDynamicPermissions`, (...args) => this.revokeDynamicSnapPermissions(...args));
1195
- this.messagingSystem.registerActionHandler(`${controllerName}:getFile`, async (...args) => this.getSnapFile(...args));
1196
- this.messagingSystem.registerActionHandler(`${controllerName}:stopAllSnaps`, async (...args) => this.stopAllSnaps(...args));
1197
- this.messagingSystem.registerActionHandler(`${controllerName}:isMinimumPlatformVersion`, (...args) => this.isMinimumPlatformVersion(...args));
1198
- }, _SnapController_handlePreinstalledSnaps = function _SnapController_handlePreinstalledSnaps(preinstalledSnaps) {
1199
- for (const { snapId, manifest, files, removable, hidden, hideSnapBranding, } of preinstalledSnaps) {
1200
- const existingSnap = this.get(snapId);
1201
- const isAlreadyInstalled = existingSnap !== undefined;
1202
- const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version);
1203
- // Disallow downgrades and overwriting non preinstalled snaps
1204
- if (isAlreadyInstalled &&
1205
- (!isUpdate || existingSnap.preinstalled !== true)) {
1206
- continue;
1207
- }
1208
- const manifestFile = new VirtualFile({
1209
- path: NpmSnapFileNames.Manifest,
1210
- value: JSON.stringify(manifest),
1211
- result: manifest,
1212
- });
1213
- const virtualFiles = files.map(({ path, value }) => new VirtualFile({ value, path }));
1214
- const { filePath, iconPath } = manifest.source.location.npm;
1215
- const sourceCode = virtualFiles.find((file) => file.path === filePath);
1216
- const svgIcon = iconPath
1217
- ? virtualFiles.find((file) => file.path === iconPath)
1218
- : undefined;
1219
- assert(sourceCode, 'Source code not provided for preinstalled snap.');
1220
- assert(!iconPath || (iconPath && svgIcon), 'Icon not provided for preinstalled snap.');
1221
- assert(manifest.source.files === undefined, 'Auxiliary files are not currently supported for preinstalled snaps.');
1222
- const localizationFiles = manifest.source.locales?.map((path) => virtualFiles.find((file) => file.path === path)) ?? [];
1223
- const validatedLocalizationFiles = getValidatedLocalizationFiles(localizationFiles.filter(Boolean));
1224
- assert(localizationFiles.length === validatedLocalizationFiles.length, 'Missing localization files for preinstalled snap.');
1225
- const filesObject = {
1226
- manifest: manifestFile,
1227
- sourceCode,
1228
- svgIcon,
1229
- auxiliaryFiles: [],
1230
- localizationFiles: validatedLocalizationFiles,
1231
- };
1232
- // Add snap to the SnapController state
1233
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_set).call(this, {
1234
- id: snapId,
1235
- origin: METAMASK_ORIGIN,
1236
- files: filesObject,
1237
- removable,
1238
- hidden,
1239
- hideSnapBranding,
1240
- preinstalled: true,
1241
- });
1242
- // Setup permissions
1243
- const processedPermissions = processSnapPermissions(manifest.initialPermissions);
1244
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validateSnapPermissions).call(this, processedPermissions);
1245
- const { newPermissions, unusedPermissions } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_calculatePermissionsChange).call(this, snapId, processedPermissions);
1246
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, { snapId, newPermissions, unusedPermissions });
1247
- if (manifest.initialConnections) {
1248
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleInitialConnections).call(this, snapId, existingSnap?.initialConnections ?? null, manifest.initialConnections);
1249
- }
1250
- // Set status
1251
- this.update((state) => {
1252
- state.snaps[snapId].status = SnapStatus.Stopped;
1253
- });
1254
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_setupRuntime).call(this, snapId);
1255
- // Emit events
1256
- if (isUpdate) {
1257
- this.messagingSystem.publish('SnapController:snapUpdated', this.getTruncatedExpect(snapId), existingSnap.version, METAMASK_ORIGIN, true);
1258
- }
1259
- else {
1260
- this.messagingSystem.publish('SnapController:snapInstalled', this.getTruncatedExpect(snapId), METAMASK_ORIGIN, true);
1261
- }
1262
- }
1263
- }, _SnapController_pollForLastRequestStatus = function _SnapController_pollForLastRequestStatus() {
1264
- __classPrivateFieldSet(this, _SnapController_timeoutForLastRequestStatus, setTimeout(() => {
1265
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_stopSnapsLastRequestPastMax).call(this).catch((error) => {
1266
- // TODO: Decide how to handle errors.
1267
- logError(error);
1268
- });
1269
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_pollForLastRequestStatus).call(this);
1270
- }, __classPrivateFieldGet(this, _SnapController_idleTimeCheckInterval, "f")), "f");
1271
- }, _SnapController_blockSnap =
1272
- /**
1273
- * Blocks an installed snap and prevents it from being started again. Emits
1274
- * {@link SnapBlocked}. Does nothing if the snap is not installed.
1275
- *
1276
- * @param snapId - The snap to block.
1277
- * @param blockedSnapInfo - Information detailing why the snap is blocked.
1278
- */
1279
- async function _SnapController_blockSnap(snapId, blockedSnapInfo) {
1280
- if (!this.has(snapId)) {
1281
- return;
1282
- }
1283
- try {
1284
- this.update((state) => {
1285
- state.snaps[snapId].blocked = true;
1286
- state.snaps[snapId].blockInformation = blockedSnapInfo;
1287
- });
1288
- await this.disableSnap(snapId);
1289
- }
1290
- catch (error) {
1291
- logError(`Encountered error when stopping blocked snap "${snapId}".`, error);
1292
- }
1293
- this.messagingSystem.publish(`${controllerName}:snapBlocked`, snapId, blockedSnapInfo);
1294
- }, _SnapController_unblockSnap = function _SnapController_unblockSnap(snapId) {
1295
- if (!this.has(snapId) || !this.state.snaps[snapId].blocked) {
1296
- return;
1297
- }
1298
- this.update((state) => {
1299
- state.snaps[snapId].blocked = false;
1300
- delete state.snaps[snapId].blockInformation;
1301
- });
1302
- this.messagingSystem.publish(`${controllerName}:snapUnblocked`, snapId);
1303
- }, _SnapController_assertIsInstallAllowed = async function _SnapController_assertIsInstallAllowed(snapId, { platformVersion, ...snapInfo }) {
1304
- const results = await this.messagingSystem.call('SnapsRegistry:get', {
1305
- [snapId]: snapInfo,
1306
- });
1307
- const result = results[snapId];
1308
- if (result.status === SnapsRegistryStatus.Blocked) {
1309
- throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": The version is blocked. ${result.reason?.explanation ?? ''}`);
1310
- }
1311
- const isAllowlistingRequired = Object.keys(snapInfo.permissions).some((permission) => !ALLOWED_PERMISSIONS.includes(permission));
1312
- if (__classPrivateFieldGet(this, _SnapController_featureFlags, "f").requireAllowlist &&
1313
- isAllowlistingRequired &&
1314
- result.status !== SnapsRegistryStatus.Verified) {
1315
- throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": ${result.status === SnapsRegistryStatus.Unavailable
1316
- ? 'The registry is temporarily unavailable.'
1317
- : 'The snap is not on the allowlist.'}`);
1318
- }
1319
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validatePlatformVersion).call(this, snapId, platformVersion);
1320
- }, _SnapController_assertCanInstallSnaps = function _SnapController_assertCanInstallSnaps() {
1321
- assert(__classPrivateFieldGet(this, _SnapController_featureFlags, "f").disableSnapInstallation !== true, 'Installing Snaps is currently disabled in this version of MetaMask.');
1322
- }, _SnapController_assertCanUsePlatform = function _SnapController_assertCanUsePlatform() {
1323
- const flags = __classPrivateFieldGet(this, _SnapController_getFeatureFlags, "f").call(this);
1324
- assert(flags.disableSnaps !== true, 'The Snaps platform requires basic functionality to be used. Enable basic functionality in the settings to use the Snaps platform.');
1325
- }, _SnapController_stopSnapsLastRequestPastMax = async function _SnapController_stopSnapsLastRequestPastMax() {
1326
- const entries = [...__classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").entries()];
1327
- return Promise.all(entries
1328
- .filter(([_snapId, runtime]) => runtime.activeReferences === 0 &&
1329
- runtime.pendingInboundRequests.length === 0 &&
1330
- runtime.lastRequest &&
1331
- __classPrivateFieldGet(this, _SnapController_maxIdleTime, "f") &&
1332
- timeSince(runtime.lastRequest) > __classPrivateFieldGet(this, _SnapController_maxIdleTime, "f"))
1333
- .map(async ([snapId]) => this.stopSnap(snapId, SnapStatusEvents.Stop)));
1334
- }, _SnapController_transition = function _SnapController_transition(snapId, event) {
1335
- const { interpreter } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1336
- interpreter.send(event);
1337
- this.update((state) => {
1338
- state.snaps[snapId].status = interpreter.state.value;
1339
- });
1340
- }, _SnapController_terminateSnap =
1341
- /**
1342
- * Terminates the specified snap and emits the `snapTerminated` event.
1343
- *
1344
- * @param snapId - The snap to terminate.
1345
- */
1346
- async function _SnapController_terminateSnap(snapId) {
1347
- await this.messagingSystem.call('ExecutionService:terminateSnap', snapId);
1348
- // Hack to give up execution for a bit to let gracefully terminating Snaps return.
1349
- await new Promise((resolve) => setTimeout(resolve, 1));
1350
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1351
- // Unresponsive requests may still be timed, time them out.
1352
- runtime.pendingInboundRequests
1353
- .filter((pendingRequest) => pendingRequest.timer.status !== 'finished')
1354
- .forEach((pendingRequest) => pendingRequest.timer.finish());
1355
- // Hack to give up execution for a bit to let timed out requests return.
1356
- await new Promise((resolve) => setTimeout(resolve, 1));
1357
- this.messagingSystem.publish('SnapController:snapTerminated', this.getTruncatedExpect(snapId));
1358
- }, _SnapController_hasCachedEncryptionKey = function _SnapController_hasCachedEncryptionKey(snapId, runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId)) {
1359
- return runtime.encryptionKey !== null && runtime.encryptionSalt !== null;
1360
- }, _SnapController_getSnapEncryptionKey =
1361
- /**
1362
- * Generate an encryption key to be used for state encryption for a given Snap.
1363
- *
1364
- * @param options - An options bag.
1365
- * @param options.snapId - The Snap ID.
1366
- * @param options.salt - A salt to be used for the encryption key.
1367
- * @param options.useCache - Whether to use caching or not.
1368
- * @param options.keyMetadata - Optional metadata about how to derive the encryption key.
1369
- * @returns An encryption key.
1370
- */
1371
- async function _SnapController_getSnapEncryptionKey({ snapId, salt: passedSalt, useCache, keyMetadata, }) {
1372
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1373
- if (__classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_hasCachedEncryptionKey).call(this, snapId, runtime) && useCache) {
1374
- return {
1375
- key: await __classPrivateFieldGet(this, _SnapController_encryptor, "f").importKey(runtime.encryptionKey),
1376
- salt: runtime.encryptionSalt,
1377
- };
1862
+ /**
1863
+ * Determine the execution timeout for a given handler permission.
1864
+ *
1865
+ * If no permission is specified or the permission itself has no execution timeout defined
1866
+ * the constructor argument `maxRequestTime` will be used.
1867
+ *
1868
+ * @param permission - An optional permission constraint for the handler being called.
1869
+ * @returns The execution timeout for the given handler.
1870
+ */
1871
+ #getExecutionTimeout(permission) {
1872
+ return getMaxRequestTimeCaveat(permission) ?? this.maxRequestTime;
1378
1873
  }
1379
- const salt = passedSalt ?? __classPrivateFieldGet(this, _SnapController_encryptor, "f").generateSalt();
1380
- const seed = await __classPrivateFieldGet(this, _SnapController_getMnemonicSeed, "f").call(this);
1381
- const entropy = await getEncryptionEntropy({
1382
- snapId,
1383
- seed,
1384
- cryptographicFunctions: __classPrivateFieldGet(this, _SnapController_clientCryptography, "f"),
1385
- });
1386
- const encryptionKey = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").keyFromPassword(entropy, salt, true, keyMetadata);
1387
- const exportedKey = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").exportKey(encryptionKey);
1388
- // Cache exported encryption key in runtime
1389
- if (useCache) {
1390
- runtime.encryptionKey = exportedKey;
1391
- runtime.encryptionSalt = salt;
1392
- }
1393
- return { key: encryptionKey, salt };
1394
- }, _SnapController_decryptSnapState =
1395
- /**
1396
- * Decrypt the encrypted state for a given Snap.
1397
- *
1398
- * @param snapId - The Snap ID.
1399
- * @param state - The encrypted state as a string.
1400
- * @returns A valid JSON object derived from the encrypted state.
1401
- * @throws If the decryption fails or the decrypted state is not valid JSON.
1402
- */
1403
- async function _SnapController_decryptSnapState(snapId, state) {
1404
- try {
1405
- // We assume that the state string here is valid JSON since we control serialization.
1406
- // This lets us skip JSON validation.
1407
- const parsed = JSON.parse(state);
1408
- const { salt, keyMetadata } = parsed;
1409
- // We only cache encryption keys if they are already cached or if the encryption key is using the latest key derivation params.
1410
- const useCache = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_hasCachedEncryptionKey).call(this, snapId) ||
1411
- __classPrivateFieldGet(this, _SnapController_encryptor, "f").isVaultUpdated(state);
1412
- const { key } = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getSnapEncryptionKey).call(this, {
1413
- snapId,
1414
- salt,
1415
- useCache,
1416
- // When decrypting state we expect key metadata to be present.
1417
- // If it isn't present, we assume that the Snap state we are decrypting is old enough to use the legacy encryption params.
1418
- keyMetadata: keyMetadata ?? LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS,
1419
- });
1420
- const decryptedState = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").decryptWithKey(key, parsed);
1421
- // We assume this to be valid JSON, since all RPC requests from a Snap are validated and sanitized.
1422
- return decryptedState;
1874
+ /**
1875
+ * Create a dynamic interface in the SnapInterfaceController.
1876
+ *
1877
+ * @param snapId - The snap ID.
1878
+ * @param content - The initial interface content.
1879
+ * @param contentType - The type of content.
1880
+ * @returns An identifier that can be used to identify the interface.
1881
+ */
1882
+ async #createInterface(snapId, content, contentType) {
1883
+ return this.messagingSystem.call('SnapInterfaceController:createInterface', snapId, content, undefined, contentType);
1423
1884
  }
1424
- catch {
1425
- throw rpcErrors.internal({
1426
- message: 'Failed to decrypt snap state, the state must be corrupted.',
1427
- });
1885
+ #assertInterfaceExists(snapId, id) {
1886
+ // This will throw if the interface isn't accessible, but we assert nevertheless.
1887
+ assert(this.messagingSystem.call('SnapInterfaceController:getInterface', snapId, id));
1428
1888
  }
1429
- }, _SnapController_encryptSnapState =
1430
- /**
1431
- * Encrypt a JSON state object for a given Snap.
1432
- *
1433
- * Note: This function does not assert the validity of the object,
1434
- * please ensure only valid JSON is passed to it.
1435
- *
1436
- * @param snapId - The Snap ID.
1437
- * @param state - The state object.
1438
- * @returns A string containing the encrypted JSON object.
1439
- */
1440
- async function _SnapController_encryptSnapState(snapId, state) {
1441
- const { key, salt } = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getSnapEncryptionKey).call(this, {
1442
- snapId,
1443
- useCache: true,
1444
- });
1445
- const encryptedState = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").encryptWithKey(key, state);
1446
- encryptedState.salt = salt;
1447
- return JSON.stringify(encryptedState);
1448
- }, _SnapController_getStateToPersist =
1449
- /**
1450
- * Get the new Snap state to persist based on the given state and encryption
1451
- * flag.
1452
- *
1453
- * - If the state is null, return null.
1454
- * - If the state should be encrypted, return the encrypted state.
1455
- * - Otherwise, if the state should not be encrypted, return the JSON-
1456
- * stringified state.
1457
- *
1458
- * @param snapId - The Snap ID.
1459
- * @param state - The state to persist.
1460
- * @param encrypted - A flag to indicate whether to use encrypted storage or
1461
- * not.
1462
- * @returns The state to persist.
1463
- */
1464
- async function _SnapController_getStateToPersist(snapId, state, encrypted) {
1465
- if (state === null) {
1466
- return null;
1467
- }
1468
- if (encrypted) {
1469
- return await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_encryptSnapState).call(this, snapId, state);
1470
- }
1471
- return JSON.stringify(state);
1472
- }, _SnapController_handleInitialConnections = function _SnapController_handleInitialConnections(snapId, previousInitialConnections, initialConnections) {
1473
- if (previousInitialConnections) {
1474
- const revokedInitialConnections = setDiff(previousInitialConnections, initialConnections);
1475
- for (const origin of Object.keys(revokedInitialConnections)) {
1476
- this.removeSnapFromSubject(origin, snapId);
1477
- }
1478
- }
1479
- for (const origin of Object.keys(initialConnections)) {
1480
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_addSnapToSubject).call(this, origin, snapId);
1481
- }
1482
- }, _SnapController_addSnapToSubject = function _SnapController_addSnapToSubject(origin, snapId) {
1483
- const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
1484
- const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
1485
- const subjectHasSnap = Boolean(existingCaveat?.value?.[snapId]);
1486
- // If the subject is already connected to the snap, this is a no-op.
1487
- if (subjectHasSnap) {
1488
- return;
1489
- }
1490
- // If an existing caveat exists, we add the snap to that.
1491
- if (existingCaveat) {
1492
- this.messagingSystem.call('PermissionController:updateCaveat', origin, WALLET_SNAP_PERMISSION_KEY, SnapCaveatType.SnapIds, { ...existingCaveat.value, [snapId]: {} });
1493
- return;
1494
- }
1495
- const approvedPermissions = {
1496
- [WALLET_SNAP_PERMISSION_KEY]: {
1497
- caveats: [
1498
- {
1499
- type: SnapCaveatType.SnapIds,
1500
- value: {
1501
- [snapId]: {},
1502
- },
1503
- },
1504
- ],
1505
- },
1506
- };
1507
- this.messagingSystem.call('PermissionController:grantPermissions', {
1508
- approvedPermissions,
1509
- subject: { origin },
1510
- });
1511
- }, _SnapController_removeSnapFromSubjects = function _SnapController_removeSnapFromSubjects(snapId) {
1512
- const subjects = this.messagingSystem.call('PermissionController:getSubjectNames');
1513
- for (const subject of subjects) {
1514
- this.removeSnapFromSubject(subject, snapId);
1515
- }
1516
- }, _SnapController_revokeAllSnapPermissions = function _SnapController_revokeAllSnapPermissions(snapId) {
1517
- if (this.messagingSystem.call('PermissionController:hasPermissions', snapId)) {
1518
- this.messagingSystem.call('PermissionController:revokeAllPermissions', snapId);
1519
- }
1520
- }, _SnapController_createApproval = function _SnapController_createApproval({ origin, snapId, type, }) {
1521
- const id = nanoid();
1522
- const promise = this.messagingSystem.call('ApprovalController:addRequest', {
1523
- origin,
1524
- id,
1525
- type,
1526
- requestData: {
1527
- // Mirror previous installation metadata
1528
- metadata: { id, origin: snapId, dappOrigin: origin },
1529
- snapId,
1530
- },
1531
- requestState: {
1532
- loading: true,
1533
- },
1534
- }, true);
1535
- return { id, promise };
1536
- }, _SnapController_updateApproval = function _SnapController_updateApproval(id, requestState) {
1537
- try {
1538
- this.messagingSystem.call('ApprovalController:updateRequestState', {
1539
- id,
1540
- requestState,
1541
- });
1889
+ /**
1890
+ * Transform a RPC response if necessary.
1891
+ *
1892
+ * @param snapId - The snap ID of the snap that produced the result.
1893
+ * @param handlerType - The handler type that produced the result.
1894
+ * @param request - The request that returned the result.
1895
+ * @param result - The response.
1896
+ * @returns The transformed result if applicable, otherwise the original result.
1897
+ */
1898
+ async #transformSnapRpcResponse(snapId, handlerType, request, result) {
1899
+ switch (handlerType) {
1900
+ case HandlerType.OnTransaction:
1901
+ case HandlerType.OnSignature:
1902
+ case HandlerType.OnHomePage:
1903
+ case HandlerType.OnSettingsPage: {
1904
+ // Since this type has been asserted earlier we can cast
1905
+ const castResult = result;
1906
+ // If a handler returns static content, we turn it into a dynamic UI
1907
+ if (castResult && hasProperty(castResult, 'content')) {
1908
+ const { content, ...rest } = castResult;
1909
+ const id = await this.#createInterface(snapId, content);
1910
+ return { ...rest, id };
1911
+ }
1912
+ return result;
1913
+ }
1914
+ case HandlerType.OnAssetsLookup:
1915
+ // We can cast since the request and result have already been validated.
1916
+ return this.#transformOnAssetsLookupResult(snapId, request, result);
1917
+ case HandlerType.OnAssetsConversion:
1918
+ // We can cast since the request and result have already been validated.
1919
+ return this.#transformOnAssetsConversionResult(request, result);
1920
+ default:
1921
+ return result;
1922
+ }
1542
1923
  }
1543
- catch {
1544
- // Do nothing
1924
+ /**
1925
+ * Transform an RPC response coming from the `onAssetsLookup` handler.
1926
+ *
1927
+ * This filters out responses that are out of scope for the Snap based on
1928
+ * its permissions and the incoming request.
1929
+ *
1930
+ * @param snapId - The snap ID of the snap that produced the result.
1931
+ * @param request - The request that returned the result.
1932
+ * @param request.params - The parameters for the request.
1933
+ * @param result - The result.
1934
+ * @param result.assets - The assets returned by the Snap.
1935
+ * @returns The transformed result.
1936
+ */
1937
+ #transformOnAssetsLookupResult(snapId, { params: requestedParams }, { assets }) {
1938
+ const permissions = this.messagingSystem.call('PermissionController:getPermissions', snapId);
1939
+ // We know the permissions are guaranteed to be set here.
1940
+ assert(permissions);
1941
+ const permission = permissions[SnapEndowments.Assets];
1942
+ const scopes = getChainIdsCaveat(permission);
1943
+ assert(scopes);
1944
+ const { assets: requestedAssets } = requestedParams;
1945
+ const filteredAssets = Object.keys(assets).reduce((accumulator, assetType) => {
1946
+ const castAssetType = assetType;
1947
+ const isValid = scopes.some((scope) => castAssetType.startsWith(scope)) &&
1948
+ requestedAssets.includes(castAssetType);
1949
+ // Filter out unrequested assets and assets for scopes the Snap hasn't registered for.
1950
+ if (isValid) {
1951
+ accumulator[castAssetType] = assets[castAssetType];
1952
+ }
1953
+ return accumulator;
1954
+ }, {});
1955
+ return { assets: filteredAssets };
1545
1956
  }
1546
- }, _SnapController_resolveAllowlistVersion = async function _SnapController_resolveAllowlistVersion(snapId, versionRange) {
1547
- return await this.messagingSystem.call('SnapsRegistry:resolveVersion', snapId, versionRange);
1548
- }, _SnapController_add =
1549
- /**
1550
- * Returns a promise representing the complete installation of the requested snap.
1551
- * If the snap is already being installed, the previously pending promise will be returned.
1552
- *
1553
- * @param args - Object containing the snap id and either the URL of the snap's manifest,
1554
- * or the snap's manifest and source code. The object may also optionally contain a target
1555
- * version.
1556
- * @returns The resulting snap object.
1557
- */
1558
- async function _SnapController_add(args) {
1559
- const { id: snapId, location, versionRange } = args;
1560
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_setupRuntime).call(this, snapId);
1561
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1562
- if (!runtime.installPromise) {
1563
- log(`Adding snap: ${snapId}`);
1564
- // If fetching and setting the snap succeeds, this property will be set
1565
- // to null in the authorize() method.
1566
- runtime.installPromise = (async () => {
1567
- const fetchedSnap = await fetchSnap(snapId, location);
1568
- const manifest = fetchedSnap.manifest.result;
1569
- if (!satisfiesVersionRange(manifest.version, versionRange)) {
1570
- throw new Error(`Version mismatch. Manifest for "${snapId}" specifies version "${manifest.version}" which doesn't satisfy requested version range "${versionRange}".`);
1957
+ /**
1958
+ * Transform an RPC response coming from the `onAssetsConversion` handler.
1959
+ *
1960
+ * This filters out responses that are out of scope for the Snap based on
1961
+ * the incoming request.
1962
+ *
1963
+ * @param request - The request that returned the result.
1964
+ * @param request.params - The parameters for the request.
1965
+ * @param result - The result.
1966
+ * @param result.conversionRates - The conversion rates returned by the Snap.
1967
+ * @returns The transformed result.
1968
+ */
1969
+ #transformOnAssetsConversionResult({ params: requestedParams }, { conversionRates }) {
1970
+ const { conversions: requestedConversions } = requestedParams;
1971
+ const filteredConversionRates = requestedConversions.reduce((accumulator, conversion) => {
1972
+ const rate = conversionRates[conversion.from]?.[conversion.to];
1973
+ // Only include rates that were actually requested.
1974
+ if (rate) {
1975
+ accumulator[conversion.from] ??= {};
1976
+ accumulator[conversion.from][conversion.to] = rate;
1571
1977
  }
1572
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertIsInstallAllowed).call(this, snapId, {
1573
- version: manifest.version,
1574
- checksum: manifest.source.shasum,
1575
- permissions: manifest.initialPermissions,
1576
- platformVersion: manifest.platformVersion,
1577
- });
1578
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_set).call(this, {
1579
- ...args,
1580
- files: fetchedSnap,
1581
- id: snapId,
1582
- });
1583
- })();
1584
- }
1585
- try {
1586
- return await runtime.installPromise;
1587
- }
1588
- catch (error) {
1589
- // Reset promise so users can retry installation in case the problem is
1590
- // temporary.
1591
- runtime.installPromise = null;
1592
- throw error;
1593
- }
1594
- }, _SnapController_startSnap = async function _SnapController_startSnap(snapData) {
1595
- const { snapId } = snapData;
1596
- if (this.isRunning(snapId)) {
1597
- throw new Error(`Snap "${snapId}" is already started.`);
1598
- }
1599
- try {
1600
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1601
- const result = await this.messagingSystem.call('ExecutionService:executeSnap', {
1602
- ...snapData,
1603
- endowments: await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getEndowments).call(this, snapId),
1604
- });
1605
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, SnapStatusEvents.Start);
1606
- // We treat the initialization of the snap as the first request, for idle timing purposes.
1607
- runtime.lastRequest = Date.now();
1608
- return result;
1978
+ return accumulator;
1979
+ }, {});
1980
+ return { conversionRates: filteredConversionRates };
1609
1981
  }
1610
- catch (error) {
1611
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_terminateSnap).call(this, snapId);
1612
- throw error;
1982
+ /**
1983
+ * Transforms a JSON-RPC request before sending it to the Snap, if required for a given handler.
1984
+ *
1985
+ * @param snapId - The Snap ID.
1986
+ * @param handlerType - The handler being called.
1987
+ * @param request - The JSON-RPC request.
1988
+ * @returns The potentially transformed JSON-RPC request.
1989
+ */
1990
+ #transformSnapRpcRequest(snapId, handlerType, request) {
1991
+ switch (handlerType) {
1992
+ // For onUserInput we inject context, so the client doesn't have to worry about keeping it in sync.
1993
+ case HandlerType.OnUserInput: {
1994
+ assert(request.params && hasProperty(request.params, 'id'));
1995
+ const interfaceId = request.params.id;
1996
+ const { context } = this.messagingSystem.call('SnapInterfaceController:getInterface', snapId, interfaceId);
1997
+ return {
1998
+ ...request,
1999
+ params: { ...request.params, context },
2000
+ };
2001
+ }
2002
+ default:
2003
+ return request;
2004
+ }
1613
2005
  }
1614
- }, _SnapController_getEndowments =
1615
- /**
1616
- * Gets the names of all endowments that will be added to the Snap's
1617
- * Compartment when it executes. These should be the names of global
1618
- * JavaScript APIs accessible in the root realm of the execution environment.
1619
- *
1620
- * Throws an error if the endowment getter for a permission returns a truthy
1621
- * value that is not an array of strings.
1622
- *
1623
- * @param snapId - The id of the snap whose SES endowments to get.
1624
- * @returns An array of the names of the endowments.
1625
- */
1626
- async function _SnapController_getEndowments(snapId) {
1627
- let allEndowments = [];
1628
- for (const permissionName of __classPrivateFieldGet(this, _SnapController_environmentEndowmentPermissions, "f")) {
1629
- if (this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName)) {
1630
- const endowments = await this.messagingSystem.call('PermissionController:getEndowments', snapId, permissionName);
1631
- if (endowments) {
1632
- // We don't have any guarantees about the type of the endowments
1633
- // value, so we have to guard at runtime.
1634
- if (!Array.isArray(endowments) ||
1635
- endowments.some((value) => typeof value !== 'string')) {
1636
- throw new Error('Expected an array of string endowment names.');
2006
+ /**
2007
+ * Assert that the returned result of a Snap RPC call is the expected shape.
2008
+ *
2009
+ * @param snapId - The snap ID.
2010
+ * @param handlerType - The handler type of the RPC Request.
2011
+ * @param result - The result of the RPC request.
2012
+ */
2013
+ async #assertSnapRpcResponse(snapId, handlerType, result) {
2014
+ switch (handlerType) {
2015
+ case HandlerType.OnTransaction: {
2016
+ assertStruct(result, OnTransactionResponseStruct);
2017
+ if (result && hasProperty(result, 'id')) {
2018
+ this.#assertInterfaceExists(snapId, result.id);
2019
+ }
2020
+ break;
2021
+ }
2022
+ case HandlerType.OnSignature: {
2023
+ assertStruct(result, OnSignatureResponseStruct);
2024
+ if (result && hasProperty(result, 'id')) {
2025
+ this.#assertInterfaceExists(snapId, result.id);
1637
2026
  }
1638
- allEndowments = allEndowments.concat(endowments);
2027
+ break;
1639
2028
  }
2029
+ case HandlerType.OnHomePage: {
2030
+ assertStruct(result, OnHomePageResponseStruct);
2031
+ if (result && hasProperty(result, 'id')) {
2032
+ this.#assertInterfaceExists(snapId, result.id);
2033
+ }
2034
+ break;
2035
+ }
2036
+ case HandlerType.OnSettingsPage: {
2037
+ assertStruct(result, OnSettingsPageResponseStruct);
2038
+ if (result && hasProperty(result, 'id')) {
2039
+ this.#assertInterfaceExists(snapId, result.id);
2040
+ }
2041
+ break;
2042
+ }
2043
+ case HandlerType.OnNameLookup:
2044
+ assertStruct(result, OnNameLookupResponseStruct);
2045
+ break;
2046
+ case HandlerType.OnAssetsLookup:
2047
+ assertStruct(result, OnAssetsLookupResponseStruct);
2048
+ break;
2049
+ case HandlerType.OnAssetsConversion:
2050
+ assertStruct(result, OnAssetsConversionResponseStruct);
2051
+ break;
2052
+ case HandlerType.OnAssetHistoricalPrice:
2053
+ assertStruct(result, OnAssetHistoricalPriceResponseStruct);
2054
+ break;
2055
+ default:
2056
+ break;
1640
2057
  }
1641
2058
  }
1642
- const dedupedEndowments = [
1643
- ...new Set([...DEFAULT_ENDOWMENTS, ...allEndowments]),
1644
- ];
1645
- if (dedupedEndowments.length <
1646
- DEFAULT_ENDOWMENTS.length + allEndowments.length) {
1647
- logError(`Duplicate endowments found for ${snapId}. Default endowments should not be requested.`, allEndowments);
1648
- }
1649
- return dedupedEndowments;
1650
- }, _SnapController_set = function _SnapController_set(args) {
1651
- const { id: snapId, origin, files, isUpdate = false, removable, preinstalled, hidden, hideSnapBranding, } = args;
1652
- const { manifest, sourceCode: sourceCodeFile, svgIcon, auxiliaryFiles: rawAuxiliaryFiles, localizationFiles, } = files;
1653
- assertIsSnapManifest(manifest.result);
1654
- const { version } = manifest.result;
1655
- const sourceCode = sourceCodeFile.toString();
1656
- assert(typeof sourceCode === 'string' && sourceCode.length > 0, `Invalid source code for snap "${snapId}".`);
1657
- const auxiliaryFiles = rawAuxiliaryFiles.map((file) => {
1658
- assert(typeof file.data.base64 === 'string');
1659
- return {
1660
- path: file.path,
1661
- value: file.data.base64,
1662
- };
1663
- });
1664
- const snapsState = this.state.snaps;
1665
- const existingSnap = snapsState[snapId];
1666
- const previousVersionHistory = existingSnap?.versionHistory ?? [];
1667
- const versionHistory = [
1668
- ...previousVersionHistory,
1669
- {
1670
- version,
1671
- date: Date.now(),
1672
- origin,
1673
- },
1674
- ];
1675
- const localizedFiles = localizationFiles.map((file) => file.result);
1676
- const snap = {
1677
- // Restore relevant snap state if it exists
1678
- ...existingSnap,
1679
- // Note that the snap will be unblocked and enabled, regardless of its
1680
- // previous state.
1681
- blocked: false,
1682
- enabled: true,
1683
- removable,
1684
- preinstalled,
1685
- hidden,
1686
- hideSnapBranding,
1687
- id: snapId,
1688
- initialConnections: manifest.result.initialConnections,
1689
- initialPermissions: manifest.result.initialPermissions,
1690
- manifest: manifest.result,
1691
- status: __classPrivateFieldGet(this, _SnapController_statusMachine, "f").config.initial,
1692
- sourceCode,
1693
- version,
1694
- versionHistory,
1695
- auxiliaryFiles,
1696
- localizationFiles: localizedFiles,
1697
- };
1698
- // If the snap was blocked, it isn't any longer
1699
- delete snap.blockInformation;
1700
- // store the snap back in state
1701
- const { inversePatches } = this.update((state) => {
1702
- state.snaps[snapId] = snap;
1703
- });
1704
- // checking for isUpdate here as this function is also used in
1705
- // the install flow, we do not care to create snapshots for installs
1706
- if (isUpdate) {
1707
- const rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1708
- if (rollbackSnapshot !== undefined) {
1709
- rollbackSnapshot.statePatches = inversePatches;
1710
- }
1711
- }
1712
- // In case the Snap uses a localized manifest, we need to get the
1713
- // proposed name from the localized manifest.
1714
- const { proposedName } = getLocalizedSnapManifest(manifest.result, 'en', localizedFiles);
1715
- this.messagingSystem.call('SubjectMetadataController:addSubjectMetadata', {
1716
- subjectType: SubjectType.Snap,
1717
- name: proposedName,
1718
- origin: snap.id,
1719
- version,
1720
- svgIcon: svgIcon?.toString() ?? null,
1721
- });
1722
- return { ...snap, sourceCode };
1723
- }, _SnapController_validateSnapPermissions = function _SnapController_validateSnapPermissions(processedPermissions) {
1724
- const permissionKeys = Object.keys(processedPermissions);
1725
- const handlerPermissions = Array.from(new Set(Object.values(handlerEndowments)));
1726
- assert(permissionKeys.some((key) => handlerPermissions.includes(key)), `A snap must request at least one of the following permissions: ${handlerPermissions
1727
- .filter((handler) => handler !== null)
1728
- .join(', ')}.`);
1729
- const excludedPermissionErrors = permissionKeys.reduce((errors, permission) => {
1730
- if (hasProperty(__classPrivateFieldGet(this, _SnapController_excludedPermissions, "f"), permission)) {
1731
- errors.push(__classPrivateFieldGet(this, _SnapController_excludedPermissions, "f")[permission]);
1732
- }
1733
- return errors;
1734
- }, []);
1735
- assert(excludedPermissionErrors.length === 0, `One or more permissions are not allowed:\n${excludedPermissionErrors.join('\n')}`);
1736
- }, _SnapController_validatePlatformVersion = function _SnapController_validatePlatformVersion(snapId, platformVersion) {
1737
- if (platformVersion === undefined) {
1738
- return;
1739
- }
1740
- if (gt(platformVersion, getPlatformVersion())) {
1741
- const message = `The Snap "${snapId}" requires platform version "${platformVersion}" which is greater than the current platform version "${getPlatformVersion()}".`;
1742
- if (__classPrivateFieldGet(this, _SnapController_featureFlags, "f").rejectInvalidPlatformVersion) {
1743
- throw new Error(message);
1744
- }
1745
- logWarning(message);
1746
- }
1747
- }, _SnapController_getExecutionTimeout = function _SnapController_getExecutionTimeout(permission) {
1748
- return getMaxRequestTimeCaveat(permission) ?? this.maxRequestTime;
1749
- }, _SnapController_createInterface =
1750
- /**
1751
- * Create a dynamic interface in the SnapInterfaceController.
1752
- *
1753
- * @param snapId - The snap ID.
1754
- * @param content - The initial interface content.
1755
- * @param contentType - The type of content.
1756
- * @returns An identifier that can be used to identify the interface.
1757
- */
1758
- async function _SnapController_createInterface(snapId, content, contentType) {
1759
- return this.messagingSystem.call('SnapInterfaceController:createInterface', snapId, content, undefined, contentType);
1760
- }, _SnapController_assertInterfaceExists = function _SnapController_assertInterfaceExists(snapId, id) {
1761
- // This will throw if the interface isn't accessible, but we assert nevertheless.
1762
- assert(this.messagingSystem.call('SnapInterfaceController:getInterface', snapId, id));
1763
- }, _SnapController_transformSnapRpcResponse =
1764
- /**
1765
- * Transform a RPC response if necessary.
1766
- *
1767
- * @param snapId - The snap ID of the snap that produced the result.
1768
- * @param handlerType - The handler type that produced the result.
1769
- * @param request - The request that returned the result.
1770
- * @param result - The response.
1771
- * @returns The transformed result if applicable, otherwise the original result.
1772
- */
1773
- async function _SnapController_transformSnapRpcResponse(snapId, handlerType, request, result) {
1774
- switch (handlerType) {
1775
- case HandlerType.OnTransaction:
1776
- case HandlerType.OnSignature:
1777
- case HandlerType.OnHomePage:
1778
- case HandlerType.OnSettingsPage: {
1779
- // Since this type has been asserted earlier we can cast
1780
- const castResult = result;
1781
- // If a handler returns static content, we turn it into a dynamic UI
1782
- if (castResult && hasProperty(castResult, 'content')) {
1783
- const { content, ...rest } = castResult;
1784
- const id = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createInterface).call(this, snapId, content);
1785
- return { ...rest, id };
2059
+ #recordSnapRpcRequestStart(snapId, requestId, timer) {
2060
+ const runtime = this.#getRuntimeExpect(snapId);
2061
+ runtime.pendingInboundRequests.push({ requestId, timer });
2062
+ runtime.lastRequest = null;
2063
+ }
2064
+ #recordSnapRpcRequestFinish(snapId, requestId, handlerType, origin, success) {
2065
+ const runtime = this.#getRuntimeExpect(snapId);
2066
+ runtime.pendingInboundRequests = runtime.pendingInboundRequests.filter((request) => request.requestId !== requestId);
2067
+ if (runtime.pendingInboundRequests.length === 0) {
2068
+ runtime.lastRequest = Date.now();
2069
+ }
2070
+ const snap = this.get(snapId);
2071
+ if (isTrackableHandler(handlerType) && !snap?.preinstalled) {
2072
+ try {
2073
+ this.#trackSnapExport(snapId, handlerType, success, origin);
2074
+ }
2075
+ catch (error) {
2076
+ logError(`Error when calling MetaMetrics hook for snap "${snap?.id}": ${getErrorMessage(error)}`);
1786
2077
  }
1787
- return result;
1788
2078
  }
1789
- case HandlerType.OnAssetsLookup:
1790
- // We can cast since the request and result have already been validated.
1791
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transformOnAssetsLookupResult).call(this, snapId, request, result);
1792
- case HandlerType.OnAssetsConversion:
1793
- // We can cast since the request and result have already been validated.
1794
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transformOnAssetsConversionResult).call(this, request, result);
1795
- default:
1796
- return result;
1797
2079
  }
1798
- }, _SnapController_transformOnAssetsLookupResult = function _SnapController_transformOnAssetsLookupResult(snapId, { params: requestedParams }, { assets }) {
1799
- const permissions = this.messagingSystem.call('PermissionController:getPermissions', snapId);
1800
- // We know the permissions are guaranteed to be set here.
1801
- assert(permissions);
1802
- const permission = permissions[SnapEndowments.Assets];
1803
- const scopes = getChainIdsCaveat(permission);
1804
- assert(scopes);
1805
- const { assets: requestedAssets } = requestedParams;
1806
- const filteredAssets = Object.keys(assets).reduce((accumulator, assetType) => {
1807
- const castAssetType = assetType;
1808
- const isValid = scopes.some((scope) => castAssetType.startsWith(scope)) &&
1809
- requestedAssets.includes(castAssetType);
1810
- // Filter out unrequested assets and assets for scopes the Snap hasn't registered for.
1811
- if (isValid) {
1812
- accumulator[castAssetType] = assets[castAssetType];
1813
- }
1814
- return accumulator;
1815
- }, {});
1816
- return { assets: filteredAssets };
1817
- }, _SnapController_transformOnAssetsConversionResult = function _SnapController_transformOnAssetsConversionResult({ params: requestedParams }, { conversionRates }) {
1818
- const { conversions: requestedConversions } = requestedParams;
1819
- const filteredConversionRates = requestedConversions.reduce((accumulator, conversion) => {
1820
- var _a;
1821
- const rate = conversionRates[conversion.from]?.[conversion.to];
1822
- // Only include rates that were actually requested.
1823
- if (rate) {
1824
- accumulator[_a = conversion.from] ?? (accumulator[_a] = {});
1825
- accumulator[conversion.from][conversion.to] = rate;
1826
- }
1827
- return accumulator;
1828
- }, {});
1829
- return { conversionRates: filteredConversionRates };
1830
- }, _SnapController_transformSnapRpcRequest = function _SnapController_transformSnapRpcRequest(snapId, handlerType, request) {
1831
- switch (handlerType) {
1832
- // For onUserInput we inject context, so the client doesn't have to worry about keeping it in sync.
1833
- case HandlerType.OnUserInput: {
1834
- assert(request.params && hasProperty(request.params, 'id'));
1835
- const interfaceId = request.params.id;
1836
- const { context } = this.messagingSystem.call('SnapInterfaceController:getInterface', snapId, interfaceId);
1837
- return {
1838
- ...request,
1839
- params: { ...request.params, context },
1840
- };
1841
- }
1842
- default:
1843
- return request;
2080
+ /**
2081
+ * Retrieves the rollback snapshot of a snap.
2082
+ *
2083
+ * @param snapId - The snap id.
2084
+ * @returns A `RollbackSnapshot` or `undefined` if one doesn't exist.
2085
+ */
2086
+ #getRollbackSnapshot(snapId) {
2087
+ return this.#rollbackSnapshots.get(snapId);
1844
2088
  }
1845
- }, _SnapController_assertSnapRpcResponse =
1846
- /**
1847
- * Assert that the returned result of a Snap RPC call is the expected shape.
1848
- *
1849
- * @param snapId - The snap ID.
1850
- * @param handlerType - The handler type of the RPC Request.
1851
- * @param result - The result of the RPC request.
1852
- */
1853
- async function _SnapController_assertSnapRpcResponse(snapId, handlerType, result) {
1854
- switch (handlerType) {
1855
- case HandlerType.OnTransaction: {
1856
- assertStruct(result, OnTransactionResponseStruct);
1857
- if (result && hasProperty(result, 'id')) {
1858
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1859
- }
1860
- break;
2089
+ /**
2090
+ * Creates a `RollbackSnapshot` that is used to help ensure
2091
+ * atomicity in multiple snap updates.
2092
+ *
2093
+ * @param snapId - The snap id.
2094
+ * @throws {@link Error}. If the snap exists before creation or if creation fails.
2095
+ * @returns A `RollbackSnapshot`.
2096
+ */
2097
+ #createRollbackSnapshot(snapId) {
2098
+ assert(this.#rollbackSnapshots.get(snapId) === undefined, new Error(`Snap "${snapId}" rollback snapshot already exists.`));
2099
+ this.#rollbackSnapshots.set(snapId, {
2100
+ statePatches: [],
2101
+ permissions: {},
2102
+ newVersion: '',
2103
+ });
2104
+ const newRollbackSnapshot = this.#rollbackSnapshots.get(snapId);
2105
+ assert(newRollbackSnapshot !== undefined, new Error(`Snapshot creation failed for ${snapId}.`));
2106
+ return newRollbackSnapshot;
2107
+ }
2108
+ /**
2109
+ * Rolls back a snap to its previous state, permissions
2110
+ * and source code based on the `RollbackSnapshot` that
2111
+ * is captured during the update process. After rolling back,
2112
+ * the function also emits an event indicating that the
2113
+ * snap has been rolled back and it clears the snapshot
2114
+ * for that snap.
2115
+ *
2116
+ * @param snapId - The snap id.
2117
+ * @throws {@link Error}. If a snapshot does not exist.
2118
+ */
2119
+ async #rollbackSnap(snapId) {
2120
+ const rollbackSnapshot = this.#getRollbackSnapshot(snapId);
2121
+ if (!rollbackSnapshot) {
2122
+ throw new Error('A snapshot does not exist for this snap.');
1861
2123
  }
1862
- case HandlerType.OnSignature: {
1863
- assertStruct(result, OnSignatureResponseStruct);
1864
- if (result && hasProperty(result, 'id')) {
1865
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1866
- }
1867
- break;
2124
+ await this.stopSnap(snapId, SnapStatusEvents.Stop);
2125
+ // Always set to stopped even if it wasn't running initially
2126
+ if (this.get(snapId)?.status !== SnapStatus.Stopped) {
2127
+ this.#transition(snapId, SnapStatusEvents.Stop);
1868
2128
  }
1869
- case HandlerType.OnHomePage: {
1870
- assertStruct(result, OnHomePageResponseStruct);
1871
- if (result && hasProperty(result, 'id')) {
1872
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1873
- }
1874
- break;
2129
+ const { statePatches, permissions } = rollbackSnapshot;
2130
+ if (statePatches?.length) {
2131
+ this.applyPatches(statePatches);
1875
2132
  }
1876
- case HandlerType.OnSettingsPage: {
1877
- assertStruct(result, OnSettingsPageResponseStruct);
1878
- if (result && hasProperty(result, 'id')) {
1879
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1880
- }
1881
- break;
1882
- }
1883
- case HandlerType.OnNameLookup:
1884
- assertStruct(result, OnNameLookupResponseStruct);
1885
- break;
1886
- case HandlerType.OnAssetsLookup:
1887
- assertStruct(result, OnAssetsLookupResponseStruct);
1888
- break;
1889
- case HandlerType.OnAssetsConversion:
1890
- assertStruct(result, OnAssetsConversionResponseStruct);
1891
- break;
1892
- case HandlerType.OnAssetHistoricalPrice:
1893
- assertStruct(result, OnAssetHistoricalPriceResponseStruct);
1894
- break;
1895
- default:
1896
- break;
1897
- }
1898
- }, _SnapController_recordSnapRpcRequestStart = function _SnapController_recordSnapRpcRequestStart(snapId, requestId, timer) {
1899
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1900
- runtime.pendingInboundRequests.push({ requestId, timer });
1901
- runtime.lastRequest = null;
1902
- }, _SnapController_recordSnapRpcRequestFinish = function _SnapController_recordSnapRpcRequestFinish(snapId, requestId, handlerType, origin, success) {
1903
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1904
- runtime.pendingInboundRequests = runtime.pendingInboundRequests.filter((request) => request.requestId !== requestId);
1905
- if (runtime.pendingInboundRequests.length === 0) {
1906
- runtime.lastRequest = Date.now();
1907
- }
1908
- const snap = this.get(snapId);
1909
- if (isTrackableHandler(handlerType) && !snap?.preinstalled) {
1910
- try {
1911
- __classPrivateFieldGet(this, _SnapController_trackSnapExport, "f").call(this, snapId, handlerType, success, origin);
2133
+ // Reset snap status, as we may have been in another state when we stored state patches
2134
+ // But now we are 100% in a stopped state
2135
+ if (this.get(snapId)?.status !== SnapStatus.Stopped) {
2136
+ this.update((state) => {
2137
+ state.snaps[snapId].status = SnapStatus.Stopped;
2138
+ });
1912
2139
  }
1913
- catch (error) {
1914
- logError(`Error when calling MetaMetrics hook for snap "${snap?.id}": ${getErrorMessage(error)}`);
1915
- }
1916
- }
1917
- }, _SnapController_getRollbackSnapshot = function _SnapController_getRollbackSnapshot(snapId) {
1918
- return __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").get(snapId);
1919
- }, _SnapController_createRollbackSnapshot = function _SnapController_createRollbackSnapshot(snapId) {
1920
- assert(__classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").get(snapId) === undefined, new Error(`Snap "${snapId}" rollback snapshot already exists.`));
1921
- __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").set(snapId, {
1922
- statePatches: [],
1923
- permissions: {},
1924
- newVersion: '',
1925
- });
1926
- const newRollbackSnapshot = __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").get(snapId);
1927
- assert(newRollbackSnapshot !== undefined, new Error(`Snapshot creation failed for ${snapId}.`));
1928
- return newRollbackSnapshot;
1929
- }, _SnapController_rollbackSnap =
1930
- /**
1931
- * Rolls back a snap to its previous state, permissions
1932
- * and source code based on the `RollbackSnapshot` that
1933
- * is captured during the update process. After rolling back,
1934
- * the function also emits an event indicating that the
1935
- * snap has been rolled back and it clears the snapshot
1936
- * for that snap.
1937
- *
1938
- * @param snapId - The snap id.
1939
- * @throws {@link Error}. If a snapshot does not exist.
1940
- */
1941
- async function _SnapController_rollbackSnap(snapId) {
1942
- const rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1943
- if (!rollbackSnapshot) {
1944
- throw new Error('A snapshot does not exist for this snap.');
1945
- }
1946
- await this.stopSnap(snapId, SnapStatusEvents.Stop);
1947
- // Always set to stopped even if it wasn't running initially
1948
- if (this.get(snapId)?.status !== SnapStatus.Stopped) {
1949
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, SnapStatusEvents.Stop);
1950
- }
1951
- const { statePatches, permissions } = rollbackSnapshot;
1952
- if (statePatches?.length) {
1953
- this.applyPatches(statePatches);
1954
- }
1955
- // Reset snap status, as we may have been in another state when we stored state patches
1956
- // But now we are 100% in a stopped state
1957
- if (this.get(snapId)?.status !== SnapStatus.Stopped) {
1958
- this.update((state) => {
1959
- state.snaps[snapId].status = SnapStatus.Stopped;
2140
+ this.#updatePermissions({
2141
+ snapId,
2142
+ unusedPermissions: permissions.granted,
2143
+ newPermissions: permissions.revoked,
2144
+ requestData: permissions.requestData,
1960
2145
  });
2146
+ const truncatedSnap = this.getTruncatedExpect(snapId);
2147
+ this.messagingSystem.publish('SnapController:snapRolledback', truncatedSnap, rollbackSnapshot.newVersion);
2148
+ this.#rollbackSnapshots.delete(snapId);
1961
2149
  }
1962
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, {
1963
- snapId,
1964
- unusedPermissions: permissions.granted,
1965
- newPermissions: permissions.revoked,
1966
- requestData: permissions.requestData,
1967
- });
1968
- const truncatedSnap = this.getTruncatedExpect(snapId);
1969
- this.messagingSystem.publish('SnapController:snapRolledback', truncatedSnap, rollbackSnapshot.newVersion);
1970
- __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").delete(snapId);
1971
- }, _SnapController_rollbackSnaps =
1972
- /**
1973
- * Iterates through an array of snap ids
1974
- * and calls `rollbackSnap` on them.
1975
- *
1976
- * @param snapIds - An array of snap ids.
1977
- */
1978
- async function _SnapController_rollbackSnaps(snapIds) {
1979
- for (const snapId of snapIds) {
1980
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_rollbackSnap).call(this, snapId);
1981
- }
1982
- }, _SnapController_getRuntime = function _SnapController_getRuntime(snapId) {
1983
- return __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").get(snapId);
1984
- }, _SnapController_getRuntimeExpect = function _SnapController_getRuntimeExpect(snapId) {
1985
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntime).call(this, snapId);
1986
- assert(runtime !== undefined, new Error(`Snap "${snapId}" runtime data not found`));
1987
- return runtime;
1988
- }, _SnapController_setupRuntime = function _SnapController_setupRuntime(snapId) {
1989
- if (__classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").has(snapId)) {
1990
- return;
1991
- }
1992
- const snap = this.get(snapId);
1993
- const interpreter = interpret(__classPrivateFieldGet(this, _SnapController_statusMachine, "f"));
1994
- interpreter.start({
1995
- context: { snapId },
1996
- value: snap?.status ??
1997
- __classPrivateFieldGet(this, _SnapController_statusMachine, "f").config.initial,
1998
- });
1999
- forceStrict(interpreter);
2000
- __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").set(snapId, {
2001
- lastRequest: null,
2002
- startPromise: null,
2003
- installPromise: null,
2004
- encryptionKey: null,
2005
- encryptionSalt: null,
2006
- activeReferences: 0,
2007
- pendingInboundRequests: [],
2008
- pendingOutboundRequests: 0,
2009
- interpreter,
2010
- stopping: false,
2011
- stateMutex: new Mutex(),
2012
- getStateMutex: new Mutex(),
2013
- });
2014
- }, _SnapController_calculatePermissionsChange = function _SnapController_calculatePermissionsChange(snapId, desiredPermissionsSet) {
2015
- const oldPermissions = this.messagingSystem.call('PermissionController:getPermissions', snapId) ?? {};
2016
- const newPermissions = permissionsDiff(desiredPermissionsSet, oldPermissions);
2017
- // TODO(ritave): The assumption that these are unused only holds so long as we do not
2018
- // permit dynamic permission requests.
2019
- const unusedPermissions = permissionsDiff(oldPermissions, desiredPermissionsSet);
2020
- // It's a Set Intersection of oldPermissions and desiredPermissionsSet
2021
- // oldPermissions (oldPermissions ∖ desiredPermissionsSet) oldPermissions desiredPermissionsSet
2022
- const approvedPermissions = permissionsDiff(oldPermissions, unusedPermissions);
2023
- return { newPermissions, unusedPermissions, approvedPermissions };
2024
- }, _SnapController_isSubjectConnectedToSnap = function _SnapController_isSubjectConnectedToSnap(snapId, origin) {
2025
- const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
2026
- const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
2027
- return Boolean(existingCaveat?.value?.[snapId]);
2028
- }, _SnapController_calculateConnectionsChange = function _SnapController_calculateConnectionsChange(snapId, oldConnectionsSet, desiredConnectionsSet) {
2029
- // Filter out any origins that have been revoked since last install/update.
2030
- // That way they will be represented as new.
2031
- const filteredOldConnections = Object.keys(oldConnectionsSet)
2032
- .filter((origin) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_isSubjectConnectedToSnap).call(this, snapId, origin))
2033
- .reduce((accumulator, origin) => {
2034
- accumulator[origin] = oldConnectionsSet[origin];
2035
- return accumulator;
2036
- }, {});
2037
- const newConnections = setDiff(desiredConnectionsSet, filteredOldConnections);
2038
- const unusedConnections = setDiff(filteredOldConnections, desiredConnectionsSet);
2039
- // It's a Set Intersection of oldConnections and desiredConnectionsSet
2040
- // oldConnections ∖ (oldConnections ∖ desiredConnectionsSet) ⟺ oldConnections ∩ desiredConnectionsSet
2041
- const approvedConnections = setDiff(filteredOldConnections, unusedConnections);
2042
- return { newConnections, unusedConnections, approvedConnections };
2043
- }, _SnapController_getPermissionsToGrant = function _SnapController_getPermissionsToGrant(snapId, newPermissions) {
2044
- if (__classPrivateFieldGet(this, _SnapController_featureFlags, "f").useCaip25Permission &&
2045
- Object.keys(newPermissions).includes(SnapEndowments.EthereumProvider)) {
2046
- // This will return the globally selected network if the Snap doesn't have
2047
- // one set.
2048
- const networkClientId = this.messagingSystem.call('SelectedNetworkController:getNetworkClientIdForDomain', snapId);
2049
- const { configuration } = this.messagingSystem.call('NetworkController:getNetworkClientById', networkClientId);
2050
- const chainId = hexToNumber(configuration.chainId);
2051
- // This needs to be assigned to have proper type inference.
2052
- const modifiedPermissions = {
2053
- ...newPermissions,
2054
- 'endowment:caip25': {
2055
- caveats: [
2056
- {
2057
- type: 'authorizedScopes',
2058
- value: {
2059
- requiredScopes: {},
2060
- optionalScopes: {
2061
- [`eip155:${chainId}`]: {
2062
- accounts: [],
2150
+ /**
2151
+ * Iterates through an array of snap ids
2152
+ * and calls `rollbackSnap` on them.
2153
+ *
2154
+ * @param snapIds - An array of snap ids.
2155
+ */
2156
+ async #rollbackSnaps(snapIds) {
2157
+ for (const snapId of snapIds) {
2158
+ await this.#rollbackSnap(snapId);
2159
+ }
2160
+ }
2161
+ #getRuntime(snapId) {
2162
+ return this.#snapsRuntimeData.get(snapId);
2163
+ }
2164
+ #getRuntimeExpect(snapId) {
2165
+ const runtime = this.#getRuntime(snapId);
2166
+ assert(runtime !== undefined, new Error(`Snap "${snapId}" runtime data not found`));
2167
+ return runtime;
2168
+ }
2169
+ #setupRuntime(snapId) {
2170
+ if (this.#snapsRuntimeData.has(snapId)) {
2171
+ return;
2172
+ }
2173
+ const snap = this.get(snapId);
2174
+ const interpreter = interpret(this.#statusMachine);
2175
+ interpreter.start({
2176
+ context: { snapId },
2177
+ value: snap?.status ??
2178
+ this.#statusMachine.config.initial,
2179
+ });
2180
+ forceStrict(interpreter);
2181
+ this.#snapsRuntimeData.set(snapId, {
2182
+ lastRequest: null,
2183
+ startPromise: null,
2184
+ stopPromise: null,
2185
+ installPromise: null,
2186
+ encryptionKey: null,
2187
+ encryptionSalt: null,
2188
+ activeReferences: 0,
2189
+ pendingInboundRequests: [],
2190
+ pendingOutboundRequests: 0,
2191
+ interpreter,
2192
+ stateMutex: new Mutex(),
2193
+ getStateMutex: new Mutex(),
2194
+ });
2195
+ }
2196
+ #calculatePermissionsChange(snapId, desiredPermissionsSet) {
2197
+ const oldPermissions = this.messagingSystem.call('PermissionController:getPermissions', snapId) ?? {};
2198
+ const newPermissions = permissionsDiff(desiredPermissionsSet, oldPermissions);
2199
+ // TODO(ritave): The assumption that these are unused only holds so long as we do not
2200
+ // permit dynamic permission requests.
2201
+ const unusedPermissions = permissionsDiff(oldPermissions, desiredPermissionsSet);
2202
+ // It's a Set Intersection of oldPermissions and desiredPermissionsSet
2203
+ // oldPermissions (oldPermissions ∖ desiredPermissionsSet) oldPermissions ∩ desiredPermissionsSet
2204
+ const approvedPermissions = permissionsDiff(oldPermissions, unusedPermissions);
2205
+ return { newPermissions, unusedPermissions, approvedPermissions };
2206
+ }
2207
+ #isSubjectConnectedToSnap(snapId, origin) {
2208
+ const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
2209
+ const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
2210
+ return Boolean(existingCaveat?.value?.[snapId]);
2211
+ }
2212
+ #calculateConnectionsChange(snapId, oldConnectionsSet, desiredConnectionsSet) {
2213
+ // Filter out any origins that have been revoked since last install/update.
2214
+ // That way they will be represented as new.
2215
+ const filteredOldConnections = Object.keys(oldConnectionsSet)
2216
+ .filter((origin) => this.#isSubjectConnectedToSnap(snapId, origin))
2217
+ .reduce((accumulator, origin) => {
2218
+ accumulator[origin] = oldConnectionsSet[origin];
2219
+ return accumulator;
2220
+ }, {});
2221
+ const newConnections = setDiff(desiredConnectionsSet, filteredOldConnections);
2222
+ const unusedConnections = setDiff(filteredOldConnections, desiredConnectionsSet);
2223
+ // It's a Set Intersection of oldConnections and desiredConnectionsSet
2224
+ // oldConnections ∖ (oldConnections ∖ desiredConnectionsSet) ⟺ oldConnections ∩ desiredConnectionsSet
2225
+ const approvedConnections = setDiff(filteredOldConnections, unusedConnections);
2226
+ return { newConnections, unusedConnections, approvedConnections };
2227
+ }
2228
+ /**
2229
+ * Get the permissions to grant to a Snap following an install, update or
2230
+ * rollback.
2231
+ *
2232
+ * @param snapId - The snap ID.
2233
+ * @param newPermissions - The new permissions to be granted.
2234
+ * @returns The permissions to grant to the Snap.
2235
+ */
2236
+ #getPermissionsToGrant(snapId, newPermissions) {
2237
+ if (this.#featureFlags.useCaip25Permission &&
2238
+ Object.keys(newPermissions).includes(SnapEndowments.EthereumProvider)) {
2239
+ // This will return the globally selected network if the Snap doesn't have
2240
+ // one set.
2241
+ const networkClientId = this.messagingSystem.call('SelectedNetworkController:getNetworkClientIdForDomain', snapId);
2242
+ const { configuration } = this.messagingSystem.call('NetworkController:getNetworkClientById', networkClientId);
2243
+ const chainId = hexToNumber(configuration.chainId);
2244
+ // This needs to be assigned to have proper type inference.
2245
+ const modifiedPermissions = {
2246
+ ...newPermissions,
2247
+ 'endowment:caip25': {
2248
+ caveats: [
2249
+ {
2250
+ type: 'authorizedScopes',
2251
+ value: {
2252
+ requiredScopes: {},
2253
+ optionalScopes: {
2254
+ [`eip155:${chainId}`]: {
2255
+ accounts: [],
2256
+ },
2063
2257
  },
2258
+ sessionProperties: {},
2259
+ isMultichainOrigin: false,
2064
2260
  },
2065
- sessionProperties: {},
2066
- isMultichainOrigin: false,
2067
2261
  },
2068
- },
2069
- ],
2070
- },
2071
- };
2072
- return modifiedPermissions;
2073
- }
2074
- return newPermissions;
2075
- }, _SnapController_updatePermissions = function _SnapController_updatePermissions({ snapId, unusedPermissions = {}, newPermissions = {}, requestData, }) {
2076
- const unusedPermissionsKeys = Object.keys(unusedPermissions);
2077
- if (isNonEmptyArray(unusedPermissionsKeys)) {
2078
- this.messagingSystem.call('PermissionController:revokePermissions', {
2079
- [snapId]: unusedPermissionsKeys,
2080
- });
2262
+ ],
2263
+ },
2264
+ };
2265
+ return modifiedPermissions;
2266
+ }
2267
+ return newPermissions;
2081
2268
  }
2082
- if (isNonEmptyArray(Object.keys(newPermissions))) {
2083
- const approvedPermissions = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getPermissionsToGrant).call(this, snapId, newPermissions);
2084
- this.messagingSystem.call('PermissionController:grantPermissions', {
2085
- approvedPermissions,
2086
- subject: { origin: snapId },
2087
- requestData,
2088
- });
2269
+ /**
2270
+ * Update the permissions for a snap following an install, update or rollback.
2271
+ *
2272
+ * Grants newly requested permissions and revokes unused/revoked permissions.
2273
+ *
2274
+ * @param args - An options bag.
2275
+ * @param args.snapId - The snap ID.
2276
+ * @param args.newPermissions - New permissions to be granted.
2277
+ * @param args.unusedPermissions - Unused permissions to be revoked.
2278
+ * @param args.requestData - Optional request data from an approval.
2279
+ */
2280
+ #updatePermissions({ snapId, unusedPermissions = {}, newPermissions = {}, requestData, }) {
2281
+ const unusedPermissionsKeys = Object.keys(unusedPermissions);
2282
+ if (isNonEmptyArray(unusedPermissionsKeys)) {
2283
+ this.messagingSystem.call('PermissionController:revokePermissions', {
2284
+ [snapId]: unusedPermissionsKeys,
2285
+ });
2286
+ }
2287
+ if (isNonEmptyArray(Object.keys(newPermissions))) {
2288
+ const approvedPermissions = this.#getPermissionsToGrant(snapId, newPermissions);
2289
+ this.messagingSystem.call('PermissionController:grantPermissions', {
2290
+ approvedPermissions,
2291
+ subject: { origin: snapId },
2292
+ requestData,
2293
+ });
2294
+ }
2089
2295
  }
2090
- }, _SnapController_isValidUpdate = function _SnapController_isValidUpdate(snapId, newVersionRange) {
2091
- const existingSnap = this.getExpect(snapId);
2092
- if (satisfiesVersionRange(existingSnap.version, newVersionRange)) {
2093
- return false;
2296
+ /**
2297
+ * Checks if a snap will pass version validation checks
2298
+ * with the new version range that is requested. The first
2299
+ * check that is done is to check if the existing snap version
2300
+ * falls inside the requested range. If it does, we want to return
2301
+ * false because we do not care to create a rollback snapshot in
2302
+ * that scenario. The second check is to ensure that the current
2303
+ * snap version is not greater than all possible versions in
2304
+ * the requested version range. If it is, then we also want
2305
+ * to return false in that scenario.
2306
+ *
2307
+ * @param snapId - The snap id.
2308
+ * @param newVersionRange - The new version range being requested.
2309
+ * @returns `true` if validation checks pass and `false` if they do not.
2310
+ */
2311
+ #isValidUpdate(snapId, newVersionRange) {
2312
+ const existingSnap = this.getExpect(snapId);
2313
+ if (satisfiesVersionRange(existingSnap.version, newVersionRange)) {
2314
+ return false;
2315
+ }
2316
+ if (gtRange(existingSnap.version, newVersionRange)) {
2317
+ return false;
2318
+ }
2319
+ return true;
2094
2320
  }
2095
- if (gtRange(existingSnap.version, newVersionRange)) {
2096
- return false;
2321
+ /**
2322
+ * Call a lifecycle hook on a snap, if the snap has the
2323
+ * `endowment:lifecycle-hooks` permission. If the snap does not have the
2324
+ * permission, nothing happens.
2325
+ *
2326
+ * @param origin - The origin.
2327
+ * @param snapId - The snap ID.
2328
+ * @param handler - The lifecycle hook to call. This should be one of the
2329
+ * supported lifecycle hooks.
2330
+ * @private
2331
+ */
2332
+ async #callLifecycleHook(origin, snapId, handler) {
2333
+ const permissionName = handlerEndowments[handler];
2334
+ assert(permissionName, 'Lifecycle hook must have an endowment.');
2335
+ const hasPermission = this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName);
2336
+ if (!hasPermission) {
2337
+ return;
2338
+ }
2339
+ await this.handleRequest({
2340
+ snapId,
2341
+ handler,
2342
+ origin,
2343
+ request: {
2344
+ jsonrpc: '2.0',
2345
+ method: handler,
2346
+ },
2347
+ });
2097
2348
  }
2098
- return true;
2099
- }, _SnapController_callLifecycleHook =
2100
- /**
2101
- * Call a lifecycle hook on a snap, if the snap has the
2102
- * `endowment:lifecycle-hooks` permission. If the snap does not have the
2103
- * permission, nothing happens.
2104
- *
2105
- * @param origin - The origin.
2106
- * @param snapId - The snap ID.
2107
- * @param handler - The lifecycle hook to call. This should be one of the
2108
- * supported lifecycle hooks.
2109
- * @private
2110
- */
2111
- async function _SnapController_callLifecycleHook(origin, snapId, handler) {
2112
- const permissionName = handlerEndowments[handler];
2113
- assert(permissionName, 'Lifecycle hook must have an endowment.');
2114
- const hasPermission = this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName);
2115
- if (!hasPermission) {
2116
- return;
2117
- }
2118
- await this.handleRequest({
2119
- snapId,
2120
- handler,
2121
- origin,
2122
- request: {
2123
- jsonrpc: '2.0',
2124
- method: handler,
2125
- },
2126
- });
2127
- }, _SnapController_handleLock = function _SnapController_handleLock() {
2128
- for (const runtime of __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").values()) {
2129
- runtime.encryptionKey = null;
2130
- runtime.encryptionSalt = null;
2131
- runtime.state = undefined;
2349
+ /**
2350
+ * Handle the `KeyringController:lock` event.
2351
+ *
2352
+ * Currently this clears the cached encrypted state (if any) for all Snaps.
2353
+ */
2354
+ #handleLock() {
2355
+ for (const runtime of this.#snapsRuntimeData.values()) {
2356
+ runtime.encryptionKey = null;
2357
+ runtime.encryptionSalt = null;
2358
+ runtime.state = undefined;
2359
+ }
2132
2360
  }
2133
- };
2361
+ }
2134
2362
  //# sourceMappingURL=SnapController.mjs.map