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