@onekeyfe/hd-core 1.2.0-alpha.33 → 1.2.0-alpha.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/__tests__/DeviceCommands.test.ts +84 -0
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +31 -0
  3. package/__tests__/core-dispose.test.ts +3 -3
  4. package/__tests__/device-lifecycle-events.test.ts +52 -0
  5. package/__tests__/device-pool-state.test.ts +14 -2
  6. package/__tests__/device-settings.test.ts +25 -0
  7. package/__tests__/device-state-mapper.test.ts +13 -2
  8. package/__tests__/device-state-projector.test.ts +2 -2
  9. package/__tests__/device-utils.test.ts +1 -0
  10. package/__tests__/device-wallet-session-store.test.ts +137 -17
  11. package/__tests__/evmSignTransaction.test.ts +69 -0
  12. package/__tests__/get-device-state.test.ts +78 -30
  13. package/__tests__/kaspaSignTransaction.test.ts +9 -0
  14. package/__tests__/known-device-public-types.type-test.ts +3 -0
  15. package/__tests__/logBlockEvent.test.ts +89 -5
  16. package/__tests__/open-wallet-session.test.ts +499 -60
  17. package/__tests__/pro2Wallpaper.test.ts +1 -1
  18. package/__tests__/protocol-v2.test.ts +1180 -169
  19. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  20. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  23. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  24. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  25. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  26. package/dist/api/evm/protocol.d.ts +3 -0
  27. package/dist/api/evm/protocol.d.ts.map +1 -0
  28. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/device/Device.d.ts +10 -4
  32. package/dist/device/Device.d.ts.map +1 -1
  33. package/dist/device/DeviceCommands.d.ts.map +1 -1
  34. package/dist/device/DeviceConnector.d.ts +1 -1
  35. package/dist/device/DeviceConnector.d.ts.map +1 -1
  36. package/dist/device/DevicePool.d.ts +1 -0
  37. package/dist/device/DevicePool.d.ts.map +1 -1
  38. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  39. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  40. package/dist/device/DeviceWalletSessionStore.d.ts +7 -0
  41. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
  42. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  43. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
  44. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
  45. package/dist/events/logBlockEvent.d.ts +2 -0
  46. package/dist/events/logBlockEvent.d.ts.map +1 -1
  47. package/dist/index.d.ts +25 -11
  48. package/dist/index.js +1037 -838
  49. package/dist/protocols/protocol-v2/features.d.ts +17 -6
  50. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  51. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  52. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  53. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts +1 -0
  54. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  55. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  56. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -10
  57. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  58. package/dist/types/api/protocolV2.d.ts +2 -2
  59. package/dist/types/api/protocolV2.d.ts.map +1 -1
  60. package/dist/types/device.d.ts +6 -3
  61. package/dist/types/device.d.ts.map +1 -1
  62. package/dist/types/params.d.ts.map +1 -1
  63. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  64. package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
  65. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  66. package/dist/utils/patch.d.ts +1 -1
  67. package/dist/utils/patch.d.ts.map +1 -1
  68. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +11 -81
  71. package/src/api/FirmwareUpdateV4.ts +59 -25
  72. package/src/api/GetPassphraseState.ts +11 -2
  73. package/src/api/OpenWalletSession.ts +16 -8
  74. package/src/api/device/DeviceSettings.ts +8 -1
  75. package/src/api/evm/EVMGetAddress.ts +2 -2
  76. package/src/api/evm/EVMGetPublicKey.ts +2 -2
  77. package/src/api/evm/EVMSignMessage.ts +2 -2
  78. package/src/api/evm/EVMSignTransaction.ts +2 -2
  79. package/src/api/evm/EVMSignTypedData.ts +35 -45
  80. package/src/api/evm/EVMVerifyMessage.ts +2 -2
  81. package/src/api/evm/protocol.ts +6 -0
  82. package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
  83. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
  84. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  85. package/src/core/index.ts +7 -5
  86. package/src/data/messages/messages-protocol-v2.json +393 -485
  87. package/src/device/Device.ts +98 -44
  88. package/src/device/DeviceCommands.ts +12 -2
  89. package/src/device/DeviceConnector.ts +10 -7
  90. package/src/device/DevicePool.ts +5 -3
  91. package/src/device/DeviceStateMapper.ts +5 -3
  92. package/src/device/DeviceStateProjector.ts +0 -7
  93. package/src/device/DeviceWalletSessionStore.ts +43 -4
  94. package/src/deviceProfile/buildDeviceFeatures.ts +11 -4
  95. package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
  96. package/src/events/logBlockEvent.ts +84 -1
  97. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  98. package/src/protocols/protocol-v2/features.ts +58 -16
  99. package/src/protocols/protocol-v2/index.ts +5 -0
  100. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
  101. package/src/protocols/protocol-v2/unlockRetry.ts +18 -7
  102. package/src/protocols/protocol-v2/walletSession.ts +144 -62
  103. package/src/types/api/protocolV2.ts +2 -2
  104. package/src/types/device.ts +8 -2
  105. package/src/types/params.ts +2 -1
  106. package/src/utils/deviceFeaturesCompat.ts +8 -1
  107. package/src/utils/pro2Wallpaper.ts +3 -9
  108. package/tsconfig.type-tests.json +5 -1
@@ -3,6 +3,7 @@ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import GetPassphraseState from '../src/api/GetPassphraseState';
5
5
  import OpenWalletSession from '../src/api/OpenWalletSession';
6
+ import { Device } from '../src/device/Device';
6
7
  import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
7
8
 
8
9
  jest.mock('../src/data/config', () => ({
@@ -26,7 +27,22 @@ const createDevice = ({
26
27
  passphraseProtection,
27
28
  attachToPinEnabled: false,
28
29
  },
29
- commands: { typedCall, promptPassphrase },
30
+ commands: {
31
+ typedCall: jest.fn((request: string, ...args: unknown[]) => {
32
+ if (request === 'DeviceStatusGet') {
33
+ return {
34
+ message: {
35
+ device_id: 'device-1',
36
+ unlocked: true,
37
+ unlocked_attach_pin: false,
38
+ passphrase_enabled: passphraseProtection,
39
+ },
40
+ };
41
+ }
42
+ return typedCall(request, ...args);
43
+ }),
44
+ promptPassphrase,
45
+ },
30
46
  getDeviceState: jest.fn().mockResolvedValue({
31
47
  identity: { deviceId: 'device-1' },
32
48
  status: { passphraseProtection },
@@ -47,6 +63,12 @@ const createDevice = ({
47
63
  }),
48
64
  lockDevice: jest.fn(),
49
65
  unlockDevice: jest.fn(),
66
+ updateProtocolV2Status: jest.fn((status: Record<string, unknown>) => {
67
+ device.features.unlocked = status.unlocked ?? device.features.unlocked;
68
+ device.features.unlockedAttachPin =
69
+ status.unlocked_attach_pin ?? device.features.unlockedAttachPin;
70
+ return device.features;
71
+ }),
50
72
  initialize: jest.fn(),
51
73
  emit: jest.fn(),
52
74
  isProtocolV2: () => true,
@@ -79,14 +101,14 @@ describe('openWalletSession', () => {
79
101
  const typedCall = jest
80
102
  .fn()
81
103
  .mockResolvedValueOnce({ message: { version: 2 } })
82
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
104
+ .mockResolvedValueOnce({ message: {} })
83
105
  .mockResolvedValueOnce({
84
106
  message: {
85
107
  btc_test_address: 'pro2-wallet-state',
86
108
  session_id: 'pro2-wallet-session',
87
109
  },
88
110
  });
89
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
111
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
90
112
  const method = new GetPassphraseState({
91
113
  payload: {
92
114
  method: 'getPassphraseState',
@@ -108,7 +130,8 @@ describe('openWalletSession', () => {
108
130
  { cancelDeviceOnReject: false }
109
131
  );
110
132
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
111
- on_device: true,
133
+ passphrase: 'host hidden wallet',
134
+ on_device: false,
112
135
  });
113
136
  expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
114
137
  });
@@ -200,14 +223,14 @@ describe('openWalletSession', () => {
200
223
  const typedCall = jest
201
224
  .fn()
202
225
  .mockResolvedValueOnce({ message: { version: 2 } })
203
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
226
+ .mockResolvedValueOnce({ message: {} })
204
227
  .mockResolvedValueOnce({
205
228
  message: {
206
229
  btc_test_address: 'hidden-state',
207
230
  session_id: 'hidden-session',
208
231
  },
209
232
  });
210
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
233
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
211
234
  const method = new OpenWalletSession({
212
235
  payload: {
213
236
  method: 'openWalletSession',
@@ -225,23 +248,59 @@ describe('openWalletSession', () => {
225
248
  });
226
249
  expect(promptPassphrase).toHaveBeenCalled();
227
250
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
228
- on_device: true,
251
+ passphrase: 'host hidden wallet',
252
+ on_device: false,
229
253
  });
230
254
  expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
231
255
  });
232
256
 
257
+ test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
258
+ const typedCall = jest
259
+ .fn()
260
+ .mockResolvedValueOnce({ message: { version: 2 } })
261
+ .mockResolvedValueOnce({ message: {} })
262
+ .mockResolvedValueOnce({
263
+ message: {
264
+ btc_test_address: 'hidden-state-after-preflight',
265
+ session_id: 'hidden-session-after-preflight',
266
+ },
267
+ });
268
+ const method = new OpenWalletSession({
269
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
270
+ });
271
+ method.init();
272
+ const device = createDevice({
273
+ typedCall,
274
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
275
+ });
276
+ device.getDeviceState = jest.fn().mockResolvedValue({
277
+ identity: { deviceId: 'device-1' },
278
+ status: { unlocked: false, passphraseProtection: true },
279
+ });
280
+ method.device = device as any;
281
+
282
+ await expect(method.run()).resolves.toMatchObject({
283
+ walletType: 'hidden',
284
+ passphraseState: 'hidden-state-after-preflight',
285
+ });
286
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
287
+ expect(device.unlockDevice.mock.invocationCallOrder[0]).toBeLessThan(
288
+ typedCall.mock.invocationCallOrder[0]
289
+ );
290
+ });
291
+
233
292
  test('classifies the selected Pro2 wallet from the post-unlock device state', async () => {
234
293
  const typedCall = jest
235
294
  .fn()
236
295
  .mockResolvedValueOnce({ message: { version: 2 } })
237
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
296
+ .mockResolvedValueOnce({ message: {} })
238
297
  .mockResolvedValueOnce({
239
298
  message: {
240
299
  btc_test_address: 'hidden-state-after-unlock',
241
300
  session_id: 'hidden-session-after-unlock',
242
301
  },
243
302
  });
244
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
303
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
245
304
  const method = new OpenWalletSession({
246
305
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
247
306
  });
@@ -253,10 +312,23 @@ describe('openWalletSession', () => {
253
312
  });
254
313
  device.features.unlocked = false;
255
314
  device.getCurrentPassphraseProtection = () => device.features.passphraseProtection;
256
- device.unlockDevice = jest.fn().mockImplementation(() => {
257
- device.features.unlocked = true;
258
- device.features.passphraseProtection = true;
259
- return Promise.resolve(device.features);
315
+ device.commands.typedCall = jest.fn((request: string, ...args: unknown[]) => {
316
+ if (request === 'DeviceStatusGet') {
317
+ return {
318
+ message: {
319
+ device_id: 'device-1',
320
+ unlocked: true,
321
+ passphrase_enabled: true,
322
+ },
323
+ };
324
+ }
325
+ return typedCall(request, ...args);
326
+ });
327
+ device.updateProtocolV2Status = jest.fn((status: Record<string, unknown>) => {
328
+ device.features.unlocked = status.unlocked ?? device.features.unlocked;
329
+ device.features.passphraseProtection =
330
+ status.passphrase_enabled ?? device.features.passphraseProtection;
331
+ return device.features;
260
332
  });
261
333
  method.device = device as any;
262
334
 
@@ -267,22 +339,51 @@ describe('openWalletSession', () => {
267
339
  passphraseState: 'hidden-state-after-unlock',
268
340
  resumed: false,
269
341
  });
270
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
342
+ expect(device.unlockDevice).not.toHaveBeenCalled();
271
343
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
272
344
  });
273
345
 
346
+ test('rejects a Pro2 standard-wallet fallback after selecting a hidden wallet', async () => {
347
+ const typedCall = jest
348
+ .fn()
349
+ .mockResolvedValueOnce({ message: { version: 2 } })
350
+ .mockResolvedValueOnce({ message: {} })
351
+ .mockResolvedValueOnce({
352
+ message: {
353
+ btc_test_address: 'standard-wallet-state',
354
+ session_id: 'standard-wallet-session',
355
+ },
356
+ });
357
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
358
+ const method = new OpenWalletSession({
359
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
360
+ });
361
+ method.init();
362
+ const device = createDevice({
363
+ passphraseProtection: false,
364
+ typedCall,
365
+ promptPassphrase,
366
+ });
367
+ method.device = device as any;
368
+
369
+ await expect(method.run()).rejects.toMatchObject({
370
+ errorCode: HardwareErrorCode.DeviceNotOpenedPassphrase,
371
+ });
372
+ expect(device.clearInternalState).toHaveBeenCalled();
373
+ });
374
+
274
375
  test('select-hidden starts a fresh hidden-wallet session on Protocol V2', async () => {
275
376
  const typedCall = jest
276
377
  .fn()
277
378
  .mockResolvedValueOnce({ message: { version: 2 } })
278
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
379
+ .mockResolvedValueOnce({ message: {} })
279
380
  .mockResolvedValueOnce({
280
381
  message: {
281
382
  btc_test_address: 'new-hidden-state',
282
383
  session_id: 'new-hidden-session',
283
384
  },
284
385
  });
285
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
386
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
286
387
  const method = new OpenWalletSession({
287
388
  payload: {
288
389
  method: 'openWalletSession',
@@ -304,7 +405,8 @@ describe('openWalletSession', () => {
304
405
  eventless_wallet_session: true,
305
406
  });
306
407
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
307
- on_device: true,
408
+ passphrase: 'host hidden wallet',
409
+ on_device: false,
308
410
  });
309
411
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
310
412
  expect(promptPassphrase).toHaveBeenCalled();
@@ -315,14 +417,14 @@ describe('openWalletSession', () => {
315
417
  const typedCall = jest
316
418
  .fn()
317
419
  .mockResolvedValueOnce({ message: { version: 2 } })
318
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
420
+ .mockResolvedValueOnce({ message: {} })
319
421
  .mockResolvedValueOnce({
320
422
  message: {
321
423
  btc_test_address: 'current-device-state',
322
424
  session_id: 'current-device-session',
323
425
  },
324
426
  });
325
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
427
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
326
428
  const method = new OpenWalletSession({
327
429
  payload: {
328
430
  method: 'openWalletSession',
@@ -484,8 +586,56 @@ describe('openWalletSession', () => {
484
586
  });
485
587
  });
486
588
 
589
+ test('rejects a Protocol V1 resume when the device returns another hidden wallet', async () => {
590
+ const typedCall = jest.fn().mockResolvedValue({
591
+ type: 'PassphraseState',
592
+ message: {
593
+ passphrase_state: 'other-hidden-state',
594
+ session_id: 'other-session',
595
+ unlocked_attach_pin: false,
596
+ },
597
+ });
598
+ const method = new OpenWalletSession({
599
+ payload: {
600
+ method: 'openWalletSession',
601
+ connectId: 'connect-id',
602
+ mode: 'resume-hidden',
603
+ deviceId: 'device-1',
604
+ passphraseState: 'hidden-state',
605
+ },
606
+ });
607
+ method.init();
608
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
609
+ const device = createDevice({ typedCall });
610
+ device.isProtocolV2 = () => false;
611
+ device.features = {
612
+ ...device.features,
613
+ deviceId: 'device-1',
614
+ unlocked: true,
615
+ sessionId: null,
616
+ };
617
+ device.getCurrentFirmwareVersionString = () => '4.15.0';
618
+ device.getCurrentDeviceType = () => EDeviceType.Pro;
619
+ device.getFeatures = jest.fn();
620
+ method.device = device as any;
621
+
622
+ await expect(method.run()).rejects.toMatchObject({
623
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
624
+ });
625
+ expect(device.clearInternalState).toHaveBeenCalledTimes(1);
626
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
627
+ });
628
+
487
629
  test('opens the standard wallet without selecting a hidden wallet', async () => {
488
- const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
630
+ const typedCall = jest
631
+ .fn()
632
+ .mockResolvedValueOnce({ message: { version: 2 } })
633
+ .mockResolvedValueOnce({
634
+ message: {
635
+ btc_test_address: 'main-wallet-state',
636
+ session_id: 'main-wallet-session',
637
+ },
638
+ });
489
639
  const promptPassphrase = jest.fn();
490
640
  const method = new OpenWalletSession({
491
641
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
@@ -502,16 +652,26 @@ describe('openWalletSession', () => {
502
652
  passphraseState: null,
503
653
  resumed: false,
504
654
  });
505
- expect(typedCall).toHaveBeenCalledTimes(1);
655
+ expect(typedCall).toHaveBeenCalledTimes(2);
506
656
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
507
657
  eventless_wallet_session: true,
508
658
  });
659
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
660
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
509
661
  expect(promptPassphrase).not.toHaveBeenCalled();
510
662
  expect(device.passphraseState).toBeUndefined();
511
663
  });
512
664
 
513
665
  test('opens a locked Protocol V2 standard wallet after deviceId becomes available', async () => {
514
- const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
666
+ const typedCall = jest
667
+ .fn()
668
+ .mockResolvedValueOnce({ message: { version: 2 } })
669
+ .mockResolvedValueOnce({
670
+ message: {
671
+ btc_test_address: 'main-wallet-state',
672
+ session_id: 'main-wallet-session',
673
+ },
674
+ });
515
675
  const method = new OpenWalletSession({
516
676
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
517
677
  });
@@ -522,7 +682,7 @@ describe('openWalletSession', () => {
522
682
  .fn()
523
683
  .mockResolvedValueOnce({
524
684
  identity: { deviceId: undefined },
525
- status: { passphraseProtection: true },
685
+ status: { unlocked: false, passphraseProtection: true },
526
686
  })
527
687
  .mockResolvedValueOnce({
528
688
  identity: { deviceId: 'device-1' },
@@ -541,18 +701,18 @@ describe('openWalletSession', () => {
541
701
  passphraseState: null,
542
702
  resumed: false,
543
703
  });
544
- expect(device.unlockDevice).toHaveBeenCalledTimes(1);
704
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
545
705
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
546
706
  });
547
707
 
548
- test('rejects a standard-wallet request when Protocol V2 is unlocked by Attach PIN', async () => {
708
+ test('switches from Attach PIN to Main PIN before opening the standard wallet', async () => {
549
709
  const typedCall = jest
550
710
  .fn()
551
711
  .mockResolvedValueOnce({ message: { version: 2 } })
552
712
  .mockResolvedValueOnce({
553
713
  message: {
554
- btc_test_address: 'attach-wallet-state',
555
- session_id: 'attach-wallet-session',
714
+ btc_test_address: 'main-wallet-state',
715
+ session_id: 'main-wallet-session',
556
716
  },
557
717
  });
558
718
  const method = new OpenWalletSession({
@@ -561,27 +721,32 @@ describe('openWalletSession', () => {
561
721
  method.init();
562
722
  const device = createDevice({ typedCall });
563
723
  device.features.unlockedAttachPin = true;
724
+ device.unlockDevice = jest.fn().mockImplementation(() => {
725
+ device.features.unlockedAttachPin = false;
726
+ return Promise.resolve(device.features);
727
+ });
564
728
  method.device = device as any;
565
729
 
566
- await expect(method.run()).rejects.toMatchObject({
567
- errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
730
+ await expect(method.run()).resolves.toMatchObject({
731
+ walletType: 'standard',
732
+ passphraseState: null,
568
733
  });
569
- expect(device.lockDevice).toHaveBeenCalled();
570
- expect(device.clearInternalState).toHaveBeenCalled();
734
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
735
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
571
736
  });
572
737
 
573
738
  test('selects a hidden wallet without exposing the internal device session', async () => {
574
739
  const typedCall = jest
575
740
  .fn()
576
741
  .mockResolvedValueOnce({ message: { version: 2 } })
577
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
742
+ .mockResolvedValueOnce({ message: {} })
578
743
  .mockResolvedValueOnce({
579
744
  message: {
580
745
  btc_test_address: 'hidden-state',
581
746
  session_id: 'hidden-session',
582
747
  },
583
748
  });
584
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
749
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
585
750
  const method = new OpenWalletSession({
586
751
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
587
752
  });
@@ -598,7 +763,8 @@ describe('openWalletSession', () => {
598
763
  resumed: false,
599
764
  });
600
765
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
601
- on_device: true,
766
+ passphrase: 'host hidden wallet',
767
+ on_device: false,
602
768
  });
603
769
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
604
770
  expect(promptPassphrase).toHaveBeenCalled();
@@ -606,15 +772,15 @@ describe('openWalletSession', () => {
606
772
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
607
773
  });
608
774
 
609
- test('selects on-device passphrase entry before getting the prepared session', async () => {
775
+ test('gets the prepared session after host passphrase entry', async () => {
610
776
  const typedCall = jest
611
777
  .fn()
612
778
  .mockResolvedValueOnce({ message: { version: 2 } })
613
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
779
+ .mockResolvedValueOnce({ message: {} })
614
780
  .mockResolvedValueOnce({
615
781
  message: { btc_test_address: 'device-state', session_id: 'device-session' },
616
782
  });
617
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
783
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
618
784
  const method = new OpenWalletSession({
619
785
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
620
786
  });
@@ -626,16 +792,42 @@ describe('openWalletSession', () => {
626
792
  passphraseState: 'device-state',
627
793
  });
628
794
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
629
- on_device: true,
795
+ passphrase: 'host hidden wallet',
796
+ on_device: false,
630
797
  });
631
798
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
632
799
  });
633
800
 
801
+ test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
802
+ const typedCall = jest
803
+ .fn()
804
+ .mockResolvedValueOnce({ message: { version: 2 } })
805
+ .mockResolvedValueOnce({ message: {} })
806
+ .mockResolvedValueOnce({
807
+ message: { btc_test_address: 'device-state', session_id: 'device-session' },
808
+ });
809
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
810
+ const method = new OpenWalletSession({
811
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
812
+ });
813
+ method.init();
814
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
815
+
816
+ await expect(method.run()).resolves.toMatchObject({
817
+ walletType: 'hidden',
818
+ passphraseState: 'device-state',
819
+ });
820
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
821
+ on_device: true,
822
+ });
823
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
824
+ });
825
+
634
826
  test('forwards a host passphrase to Pro2 firmware', async () => {
635
827
  const typedCall = jest
636
828
  .fn()
637
829
  .mockResolvedValueOnce({ message: { version: 2 } })
638
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
830
+ .mockResolvedValueOnce({ message: {} })
639
831
  .mockResolvedValueOnce({
640
832
  message: { btc_test_address: 'host-state', session_id: 'host-session' },
641
833
  });
@@ -670,6 +862,52 @@ describe('openWalletSession', () => {
670
862
  passphrase: 'host hidden wallet',
671
863
  on_device: false,
672
864
  });
865
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
866
+ });
867
+
868
+ test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
869
+ const typedCall = jest
870
+ .fn()
871
+ .mockResolvedValueOnce({ message: { version: 2 } })
872
+ .mockResolvedValueOnce({ message: {} })
873
+ .mockResolvedValueOnce({
874
+ message: { btc_test_address: 'host-state', session_id: 'host-session' },
875
+ });
876
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'caf\u00e9' });
877
+ const method = new OpenWalletSession({
878
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
879
+ });
880
+ method.init();
881
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
882
+
883
+ await expect(method.run()).resolves.toMatchObject({ passphraseState: 'host-state' });
884
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
885
+ passphrase: 'cafe\u0301',
886
+ on_device: false,
887
+ });
888
+ });
889
+
890
+ test('accepts a Host passphrase at the 50-byte firmware boundary', async () => {
891
+ const passphrase = 'a'.repeat(50);
892
+ const typedCall = jest
893
+ .fn()
894
+ .mockResolvedValueOnce({ message: { version: 2 } })
895
+ .mockResolvedValueOnce({ message: {} })
896
+ .mockResolvedValueOnce({
897
+ message: { btc_test_address: 'host-state', session_id: 'host-session' },
898
+ });
899
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase });
900
+ const method = new OpenWalletSession({
901
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
902
+ });
903
+ method.init();
904
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
905
+
906
+ await expect(method.run()).resolves.toMatchObject({ passphraseState: 'host-state' });
907
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
908
+ passphrase,
909
+ on_device: false,
910
+ });
673
911
  });
674
912
 
675
913
  test('selects Attach PIN only when the device reports an existing binding', async () => {
@@ -705,6 +943,176 @@ describe('openWalletSession', () => {
705
943
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
706
944
  });
707
945
 
946
+ test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
947
+ const typedCall = jest
948
+ .fn()
949
+ .mockResolvedValueOnce({ message: { version: 2 } })
950
+ .mockResolvedValueOnce({ message: { message: 'PIN verified' } })
951
+ .mockResolvedValueOnce({
952
+ message: { btc_test_address: 'attach-state', session_id: 'attach-session' },
953
+ });
954
+ const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
955
+ const method = new OpenWalletSession({
956
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
957
+ });
958
+ method.init();
959
+ const device = createDevice({ typedCall, promptPassphrase });
960
+ device.features.attachToPinEnabled = true;
961
+ device.unlockDevice = Device.prototype.unlockDevice.bind(device);
962
+ method.device = device as any;
963
+
964
+ await expect(method.run()).resolves.toMatchObject({
965
+ walletType: 'hidden',
966
+ passphraseState: 'attach-state',
967
+ });
968
+ expect(device.commands.typedCall.mock.calls).toEqual([
969
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
970
+ ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
971
+ ['DeviceStatusGet', 'DeviceStatus', {}],
972
+ ['DeviceSessionGet', 'DeviceSession', {}],
973
+ ]);
974
+ expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
975
+ type: DeviceSessionPinType.Main,
976
+ });
977
+ });
978
+
979
+ test.each([
980
+ ['no selection', {}],
981
+ ['an empty Host passphrase', { passphrase: '' }],
982
+ ['more than one selection', { passphrase: 'host hidden wallet', passphraseOnDevice: true }],
983
+ ])('rejects %s before sending a wallet selection command', async (_name, response) => {
984
+ const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
985
+ const promptPassphrase = jest.fn().mockResolvedValue(response);
986
+ const method = new OpenWalletSession({
987
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
988
+ });
989
+ method.init();
990
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
991
+
992
+ await expect(method.run()).rejects.toMatchObject({
993
+ errorCode: HardwareErrorCode.RuntimeError,
994
+ message: 'Wallet selection must contain exactly one passphrase access mode.',
995
+ });
996
+ expect(typedCall).toHaveBeenCalledTimes(1);
997
+ });
998
+
999
+ test('stops before sending a wallet command when the App cancels selection', async () => {
1000
+ const cancellation = new Error('Wallet selection cancelled');
1001
+ const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
1002
+ const promptPassphrase = jest.fn().mockRejectedValue(cancellation);
1003
+ const method = new OpenWalletSession({
1004
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1005
+ });
1006
+ method.init();
1007
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
1008
+
1009
+ await expect(method.run()).rejects.toBe(cancellation);
1010
+ expect(typedCall).toHaveBeenCalledTimes(1);
1011
+ });
1012
+
1013
+ test.each(['User cancelled', 'Device disconnected'])(
1014
+ 'does not issue a resume request after the wallet selection reports %s',
1015
+ async message => {
1016
+ const typedCall = jest
1017
+ .fn()
1018
+ .mockResolvedValueOnce({ message: { version: 2 } })
1019
+ .mockRejectedValueOnce(new Error(message));
1020
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1021
+ const method = new OpenWalletSession({
1022
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1023
+ });
1024
+ method.init();
1025
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
1026
+
1027
+ await expect(method.run()).rejects.toThrow(message);
1028
+ expect(typedCall).toHaveBeenCalledTimes(2);
1029
+ expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1030
+ }
1031
+ );
1032
+
1033
+ test.each([
1034
+ ['a NUL byte', 'hidden\0wallet'],
1035
+ ['more than 50 UTF-8 bytes', 'a'.repeat(51)],
1036
+ ['more than 50 UTF-8 bytes after NFKD normalization', '\u00e9'.repeat(17)],
1037
+ ['an unpaired UTF-16 surrogate', '\ud800'],
1038
+ ])('rejects a Host passphrase containing %s', async (_name, passphrase) => {
1039
+ const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
1040
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase });
1041
+ const method = new OpenWalletSession({
1042
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1043
+ });
1044
+ method.init();
1045
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
1046
+
1047
+ await expect(method.run()).rejects.toMatchObject({
1048
+ errorCode: HardwareErrorCode.RuntimeError,
1049
+ message: 'Host passphrase must contain 1 to 50 valid UTF-8 bytes without NUL.',
1050
+ });
1051
+ expect(typedCall).toHaveBeenCalledTimes(1);
1052
+ });
1053
+
1054
+ test('rejects an unavailable Attach PIN selection before asking the device for a PIN', async () => {
1055
+ const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
1056
+ const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
1057
+ const method = new OpenWalletSession({
1058
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1059
+ });
1060
+ method.init();
1061
+ const device = createDevice({ typedCall, promptPassphrase });
1062
+ device.features.attachToPinEnabled = false;
1063
+ method.device = device as any;
1064
+
1065
+ await expect(method.run()).rejects.toMatchObject({
1066
+ errorCode: HardwareErrorCode.RuntimeError,
1067
+ message: 'Attach PIN wallet selection is unavailable on this device.',
1068
+ });
1069
+ expect(device.unlockDevice).not.toHaveBeenCalled();
1070
+ expect(typedCall).toHaveBeenCalledTimes(1);
1071
+ });
1072
+
1073
+ test('does not unlock or retry when wallet preparation reports a late DeviceLocked', async () => {
1074
+ const lockedError = { errorCode: HardwareErrorCode.DeviceLocked };
1075
+ const typedCall = jest
1076
+ .fn()
1077
+ .mockResolvedValueOnce({ message: { version: 2 } })
1078
+ .mockRejectedValueOnce(lockedError)
1079
+ .mockResolvedValueOnce({ message: {} })
1080
+ .mockResolvedValueOnce({
1081
+ message: { btc_test_address: 'hidden-state', session_id: 'hidden-session' },
1082
+ });
1083
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1084
+ const method = new OpenWalletSession({
1085
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1086
+ });
1087
+ method.init();
1088
+ const device = createDevice({ typedCall, promptPassphrase });
1089
+ method.device = device as any;
1090
+
1091
+ await expect(method.run()).rejects.toBe(lockedError);
1092
+ expect(device.unlockDevice).not.toHaveBeenCalled();
1093
+ expect(typedCall).toHaveBeenCalledTimes(2);
1094
+ });
1095
+
1096
+ test('does not fall back when wallet preparation is rejected', async () => {
1097
+ const invalidSessionError = { errorCode: HardwareErrorCode.WalletSessionInvalid };
1098
+ const typedCall = jest
1099
+ .fn()
1100
+ .mockResolvedValueOnce({ message: { version: 2 } })
1101
+ .mockRejectedValueOnce(invalidSessionError);
1102
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1103
+ const method = new OpenWalletSession({
1104
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1105
+ });
1106
+ method.init();
1107
+ const device = createDevice({ typedCall, promptPassphrase });
1108
+ method.device = device as any;
1109
+
1110
+ await expect(method.run()).rejects.toBe(invalidSessionError);
1111
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
1112
+ expect(typedCall).toHaveBeenCalledTimes(2);
1113
+ expect(device.updateInternalState).not.toHaveBeenCalled();
1114
+ });
1115
+
708
1116
  test.each([
709
1117
  {
710
1118
  missingField: 'session_id',
@@ -720,9 +1128,9 @@ describe('openWalletSession', () => {
720
1128
  const typedCall = jest
721
1129
  .fn()
722
1130
  .mockResolvedValueOnce({ message: { version: 2 } })
723
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1131
+ .mockResolvedValueOnce({ message: {} })
724
1132
  .mockResolvedValueOnce({ message });
725
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
1133
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
726
1134
  const method = new OpenWalletSession({
727
1135
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
728
1136
  });
@@ -732,7 +1140,7 @@ describe('openWalletSession', () => {
732
1140
 
733
1141
  await expect(method.run()).rejects.toMatchObject({
734
1142
  errorCode: HardwareErrorCode.RuntimeError,
735
- message: 'DeviceSessionGet returned an incomplete DeviceSession response.',
1143
+ message: 'Device returned an incomplete DeviceSession response.',
736
1144
  });
737
1145
  expect(device.clearInternalState).toHaveBeenCalled();
738
1146
  expect(device.updateInternalState).not.toHaveBeenCalled();
@@ -803,7 +1211,7 @@ describe('openWalletSession', () => {
803
1211
  .fn()
804
1212
  .mockResolvedValueOnce({
805
1213
  identity: { deviceId: undefined },
806
- status: { passphraseProtection: true },
1214
+ status: { unlocked: false, passphraseProtection: true },
807
1215
  })
808
1216
  .mockResolvedValueOnce({
809
1217
  identity: { deviceId: 'device-1' },
@@ -879,13 +1287,23 @@ describe('openWalletSession', () => {
879
1287
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
880
1288
  });
881
1289
 
882
- test('does not fall back to wallet selection when a cached resume is invalid', async () => {
883
- const typedCall = jest.fn().mockRejectedValue(
884
- Object.assign(new Error('Invalid session'), {
885
- errorCode: HardwareErrorCode.WalletSessionInvalid,
886
- })
887
- );
888
- const promptPassphrase = jest.fn();
1290
+ test('reselects the expected hidden wallet when a cached Protocol V2 session is invalid', async () => {
1291
+ const typedCall = jest
1292
+ .fn()
1293
+ .mockResolvedValueOnce({ message: { version: 2 } })
1294
+ .mockRejectedValueOnce(
1295
+ Object.assign(new Error('Invalid session'), {
1296
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
1297
+ })
1298
+ )
1299
+ .mockResolvedValueOnce({ message: {} })
1300
+ .mockResolvedValueOnce({
1301
+ message: {
1302
+ btc_test_address: 'hidden-state',
1303
+ session_id: 'renewed-session',
1304
+ },
1305
+ });
1306
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
889
1307
  const method = new OpenWalletSession({
890
1308
  payload: {
891
1309
  method: 'openWalletSession',
@@ -899,15 +1317,32 @@ describe('openWalletSession', () => {
899
1317
  deviceWalletSessionStore.set('device-1', 'hidden-state', 'expired-session');
900
1318
  method.device = createDevice({ typedCall, promptPassphrase }) as any;
901
1319
 
902
- await expect(method.run()).rejects.toMatchObject({
903
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1320
+ await expect(method.run()).resolves.toEqual({
1321
+ protocol: 'V2',
1322
+ walletType: 'hidden',
1323
+ deviceId: 'device-1',
1324
+ passphraseState: 'hidden-state',
1325
+ resumed: false,
904
1326
  });
905
- expect(promptPassphrase).not.toHaveBeenCalled();
1327
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1328
+ session_id: 'expired-session',
1329
+ });
1330
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
1331
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
906
1332
  });
907
1333
 
908
- test('rejects resume-hidden when the SDK store has no matching session', async () => {
909
- const typedCall = jest.fn();
910
- const promptPassphrase = jest.fn();
1334
+ test('selects the expected hidden wallet when Protocol V2 has no cached session', async () => {
1335
+ const typedCall = jest
1336
+ .fn()
1337
+ .mockResolvedValueOnce({ message: { version: 2 } })
1338
+ .mockResolvedValueOnce({ message: {} })
1339
+ .mockResolvedValueOnce({
1340
+ message: {
1341
+ btc_test_address: 'hidden-state',
1342
+ session_id: 'new-session',
1343
+ },
1344
+ });
1345
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
911
1346
  const method = new OpenWalletSession({
912
1347
  payload: {
913
1348
  method: 'openWalletSession',
@@ -920,10 +1355,14 @@ describe('openWalletSession', () => {
920
1355
  method.init();
921
1356
  method.device = createDevice({ typedCall, promptPassphrase }) as any;
922
1357
 
923
- await expect(method.run()).rejects.toMatchObject({
924
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1358
+ await expect(method.run()).resolves.toEqual({
1359
+ protocol: 'V2',
1360
+ walletType: 'hidden',
1361
+ deviceId: 'device-1',
1362
+ passphraseState: 'hidden-state',
1363
+ resumed: false,
925
1364
  });
926
- expect(typedCall).not.toHaveBeenCalled();
927
- expect(promptPassphrase).not.toHaveBeenCalled();
1365
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
1366
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
928
1367
  });
929
1368
  });