@@ -52,6 +52,7 @@ import {
52
52
MOCK_SNAP_NAME ,
53
53
DEFAULT_SOURCE_PATH ,
54
54
DEFAULT_ICON_PATH ,
55
+ TEST_SECRET_RECOVERY_PHRASE_BYTES ,
55
56
} from '@metamask/snaps-utils/test-utils' ;
56
57
import type { SemVerRange , SemVerVersion , Json } from '@metamask/utils' ;
57
58
import {
@@ -60,6 +61,7 @@ import {
60
61
AssertionError ,
61
62
base64ToBytes ,
62
63
stringToBytes ,
64
+ createDeferredPromise ,
63
65
} from '@metamask/utils' ;
64
66
import { File } from 'buffer' ;
65
67
import { webcrypto } from 'crypto' ;
@@ -78,6 +80,7 @@ import {
78
80
getNodeEESMessenger ,
79
81
getPersistedSnapsState ,
80
82
getSnapController ,
83
+ getSnapControllerEncryptor ,
81
84
getSnapControllerMessenger ,
82
85
getSnapControllerOptions ,
83
86
getSnapControllerWithEES ,
@@ -97,6 +100,7 @@ import {
97
100
MOCK_WALLET_SNAP_PERMISSION ,
98
101
MockSnapsRegistry ,
99
102
sleep ,
103
+ waitForStateChange ,
100
104
} from '../test-utils' ;
101
105
import { delay } from '../utils' ;
102
106
import { LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS } from './constants' ;
@@ -2117,6 +2121,59 @@ describe('SnapController', () => {
2117
2121
await service . terminateAllSnaps ( ) ;
2118
2122
} ) ;
2119
2123
2124
+ it ( 'clears encrypted state of Snaps when the client is locked' , async ( ) => {
2125
+ const rootMessenger = getControllerMessenger ( ) ;
2126
+ const messenger = getSnapControllerMessenger ( rootMessenger ) ;
2127
+
2128
+ const state = { myVariable : 1 } ;
2129
+
2130
+ const mockEncryptedState = await encrypt (
2131
+ ENCRYPTION_KEY ,
2132
+ state ,
2133
+ undefined ,
2134
+ undefined ,
2135
+ DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS ,
2136
+ ) ;
2137
+
2138
+ const getMnemonic = jest
2139
+ . fn ( )
2140
+ . mockReturnValue ( TEST_SECRET_RECOVERY_PHRASE_BYTES ) ;
2141
+
2142
+ const snapController = getSnapController (
2143
+ getSnapControllerOptions ( {
2144
+ messenger,
2145
+ state : {
2146
+ snaps : {
2147
+ [ MOCK_SNAP_ID ] : getPersistedSnapObject ( ) ,
2148
+ } ,
2149
+ snapStates : {
2150
+ [ MOCK_SNAP_ID ] : mockEncryptedState ,
2151
+ } ,
2152
+ } ,
2153
+ getMnemonic,
2154
+ } ) ,
2155
+ ) ;
2156
+
2157
+ expect (
2158
+ await messenger . call ( 'SnapController:getSnapState' , MOCK_SNAP_ID , true ) ,
2159
+ ) . toStrictEqual ( state ) ;
2160
+ expect ( getMnemonic ) . toHaveBeenCalledTimes ( 1 ) ;
2161
+
2162
+ rootMessenger . publish ( 'KeyringController:lock' ) ;
2163
+
2164
+ expect (
2165
+ await messenger . call ( 'SnapController:getSnapState' , MOCK_SNAP_ID , true ) ,
2166
+ ) . toStrictEqual ( state ) ;
2167
+
2168
+ // We assume `getMnemonic` is called again because the controller needs to
2169
+ // decrypt the state again. This is not an ideal way to test this, but it
2170
+ // is the easiest to test this without exposing the internal state of the
2171
+ // `SnapController`.
2172
+ expect ( getMnemonic ) . toHaveBeenCalledTimes ( 2 ) ;
2173
+
2174
+ snapController . destroy ( ) ;
2175
+ } ) ;
2176
+
2120
2177
describe ( 'handleRequest' , ( ) => {
2121
2178
it . each (
2122
2179
Object . keys ( handlerEndowments ) . filter (
@@ -8801,6 +8858,7 @@ describe('SnapController', () => {
8801
8858
) ;
8802
8859
8803
8860
const newState = { myVariable : 2 } ;
8861
+ const promise = waitForStateChange ( messenger ) ;
8804
8862
8805
8863
await messenger . call (
8806
8864
'SnapController:updateSnapState' ,
@@ -8817,6 +8875,8 @@ describe('SnapController', () => {
8817
8875
DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS ,
8818
8876
) ;
8819
8877
8878
+ await promise ;
8879
+
8820
8880
const result = await messenger . call (
8821
8881
'SnapController:getSnapState' ,
8822
8882
MOCK_SNAP_ID ,
@@ -8831,7 +8891,7 @@ describe('SnapController', () => {
8831
8891
snapController . destroy ( ) ;
8832
8892
} ) ;
8833
8893
8834
- it ( 'different snaps use different encryption keys' , async ( ) => {
8894
+ it ( 'uses different encryption keys for different snaps ' , async ( ) => {
8835
8895
const messenger = getSnapControllerMessenger ( ) ;
8836
8896
8837
8897
const state = { foo : 'bar' } ;
@@ -8857,13 +8917,17 @@ describe('SnapController', () => {
8857
8917
true ,
8858
8918
) ;
8859
8919
8920
+ const promise = waitForStateChange ( messenger ) ;
8921
+
8860
8922
await messenger . call (
8861
8923
'SnapController:updateSnapState' ,
8862
8924
MOCK_LOCAL_SNAP_ID ,
8863
8925
state ,
8864
8926
true ,
8865
8927
) ;
8866
8928
8929
+ await promise ;
8930
+
8867
8931
const encryptedState1 = await encrypt (
8868
8932
ENCRYPTION_KEY ,
8869
8933
state ,
@@ -9073,13 +9137,17 @@ describe('SnapController', () => {
9073
9137
undefined ,
9074
9138
DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS ,
9075
9139
) ;
9140
+
9141
+ const promise = waitForStateChange ( messenger ) ;
9076
9142
await messenger . call (
9077
9143
'SnapController:updateSnapState' ,
9078
9144
MOCK_SNAP_ID ,
9079
9145
state ,
9080
9146
true ,
9081
9147
) ;
9082
9148
9149
+ await promise ;
9150
+
9083
9151
expect ( updateSnapStateSpy ) . toHaveBeenCalledTimes ( 1 ) ;
9084
9152
expect ( snapController . state . snapStates [ MOCK_SNAP_ID ] ) . toStrictEqual (
9085
9153
mockEncryptedState ,
@@ -9137,17 +9205,126 @@ describe('SnapController', () => {
9137
9205
) ;
9138
9206
9139
9207
const state = { foo : 'bar' } ;
9208
+
9209
+ const promise = waitForStateChange ( messenger ) ;
9140
9210
await messenger . call (
9141
9211
'SnapController:updateSnapState' ,
9142
9212
MOCK_SNAP_ID ,
9143
9213
state ,
9144
9214
true ,
9145
9215
) ;
9146
9216
9217
+ await promise ;
9218
+
9147
9219
expect ( pbkdf2Sha512 ) . toHaveBeenCalledTimes ( 1 ) ;
9148
9220
9149
9221
snapController . destroy ( ) ;
9150
9222
} ) ;
9223
+
9224
+ it ( 'queues multiple state updates' , async ( ) => {
9225
+ const messenger = getSnapControllerMessenger ( ) ;
9226
+
9227
+ jest . useFakeTimers ( ) ;
9228
+
9229
+ const encryptor = getSnapControllerEncryptor ( ) ;
9230
+ const { promise, resolve } = createDeferredPromise ( ) ;
9231
+ const encryptWithKey = jest
9232
+ . fn <
9233
+ ReturnType < typeof encryptor . encryptWithKey > ,
9234
+ Parameters < typeof encryptor . encryptWithKey >
9235
+ > ( )
9236
+ . mockImplementation ( async ( ...args ) => {
9237
+ resolve ( ) ;
9238
+ await sleep ( 1 ) ;
9239
+ return await encryptor . encryptWithKey ( ...args ) ;
9240
+ } ) ;
9241
+
9242
+ const snapController = getSnapController (
9243
+ getSnapControllerOptions ( {
9244
+ messenger,
9245
+ state : {
9246
+ snaps : getPersistedSnapsState ( ) ,
9247
+ } ,
9248
+ encryptor : {
9249
+ ...getSnapControllerEncryptor ( ) ,
9250
+ // @ts -expect-error - Missing required properties.
9251
+ encryptWithKey,
9252
+ } ,
9253
+ } ) ,
9254
+ ) ;
9255
+
9256
+ const firstStateChange = waitForStateChange ( messenger ) ;
9257
+ await messenger . call (
9258
+ 'SnapController:updateSnapState' ,
9259
+ MOCK_SNAP_ID ,
9260
+ { foo : 'bar' } ,
9261
+ true ,
9262
+ ) ;
9263
+
9264
+ await messenger . call (
9265
+ 'SnapController:updateSnapState' ,
9266
+ MOCK_SNAP_ID ,
9267
+ { bar : 'baz' } ,
9268
+ true ,
9269
+ ) ;
9270
+
9271
+ // We await this promise to ensure the timer is queued.
9272
+ await promise ;
9273
+ jest . advanceTimersByTime ( 1 ) ;
9274
+
9275
+ // After this point the second update should be queued.
9276
+ await firstStateChange ;
9277
+ const secondStateChange = waitForStateChange ( messenger ) ;
9278
+
9279
+ expect ( encryptWithKey ) . toHaveBeenCalledTimes ( 1 ) ;
9280
+
9281
+ // This is a bit hacky, but we can't simply advance the timer by 1ms
9282
+ // because the second timer is not running yet.
9283
+ jest . useRealTimers ( ) ;
9284
+ await secondStateChange ;
9285
+
9286
+ expect ( encryptWithKey ) . toHaveBeenCalledTimes ( 2 ) ;
9287
+
9288
+ expect (
9289
+ await messenger . call ( 'SnapController:getSnapState' , MOCK_SNAP_ID , true ) ,
9290
+ ) . toStrictEqual ( { bar : 'baz' } ) ;
9291
+
9292
+ snapController . destroy ( ) ;
9293
+ } ) ;
9294
+
9295
+ it ( 'logs an error message if the state fails to persist' , async ( ) => {
9296
+ const messenger = getSnapControllerMessenger ( ) ;
9297
+
9298
+ const errorValue = new Error ( 'Failed to persist state.' ) ;
9299
+ const snapController = getSnapController (
9300
+ getSnapControllerOptions ( {
9301
+ messenger,
9302
+ state : {
9303
+ snaps : getPersistedSnapsState ( ) ,
9304
+ } ,
9305
+ // @ts -expect-error - Missing required properties.
9306
+ encryptor : {
9307
+ ...getSnapControllerEncryptor ( ) ,
9308
+ encryptWithKey : jest . fn ( ) . mockRejectedValue ( errorValue ) ,
9309
+ } ,
9310
+ } ) ,
9311
+ ) ;
9312
+
9313
+ const { promise, resolve } = createDeferredPromise ( ) ;
9314
+ const error = jest . spyOn ( console , 'error' ) . mockImplementation ( resolve ) ;
9315
+
9316
+ await messenger . call (
9317
+ 'SnapController:updateSnapState' ,
9318
+ MOCK_SNAP_ID ,
9319
+ { foo : 'bar' } ,
9320
+ true ,
9321
+ ) ;
9322
+
9323
+ await promise ;
9324
+ expect ( error ) . toHaveBeenCalledWith ( errorValue ) ;
9325
+
9326
+ snapController . destroy ( ) ;
9327
+ } ) ;
9151
9328
} ) ;
9152
9329
9153
9330
describe ( 'SnapController:clearSnapState' , ( ) => {
@@ -9206,6 +9383,41 @@ describe('SnapController', () => {
9206
9383
9207
9384
snapController . destroy ( ) ;
9208
9385
} ) ;
9386
+
9387
+ it ( 'logs an error message if the state fails to persist' , async ( ) => {
9388
+ const messenger = getSnapControllerMessenger ( ) ;
9389
+
9390
+ const errorValue = new Error ( 'Failed to persist state.' ) ;
9391
+ const snapController = getSnapController (
9392
+ getSnapControllerOptions ( {
9393
+ messenger,
9394
+ state : {
9395
+ snaps : getPersistedSnapsState ( ) ,
9396
+ } ,
9397
+ // @ts -expect-error - Missing required properties.
9398
+ encryptor : {
9399
+ ...getSnapControllerEncryptor ( ) ,
9400
+ encryptWithKey : jest . fn ( ) . mockRejectedValue ( errorValue ) ,
9401
+ } ,
9402
+ } ) ,
9403
+ ) ;
9404
+
9405
+ const { promise, resolve } = createDeferredPromise ( ) ;
9406
+ const error = jest . spyOn ( console , 'error' ) . mockImplementation ( resolve ) ;
9407
+
9408
+ // @ts -expect-error - Property `update` is protected.
9409
+ // eslint-disable-next-line jest/prefer-spy-on
9410
+ snapController . update = jest . fn ( ) . mockImplementation ( ( ) => {
9411
+ throw errorValue ;
9412
+ } ) ;
9413
+
9414
+ await messenger . call ( 'SnapController:clearSnapState' , MOCK_SNAP_ID , true ) ;
9415
+
9416
+ await promise ;
9417
+ expect ( error ) . toHaveBeenCalledWith ( errorValue ) ;
9418
+
9419
+ snapController . destroy ( ) ;
9420
+ } ) ;
9209
9421
} ) ;
9210
9422
9211
9423
describe ( 'SnapController:updateBlockedSnaps' , ( ) => {
0 commit comments