@onekeyfe/hd-core 1.2.0-alpha.34 → 1.2.0-alpha.36

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 (91) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +74 -0
  2. package/__tests__/DeviceCommands.test.ts +84 -0
  3. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  4. package/__tests__/device-settings.test.ts +3 -0
  5. package/__tests__/device-wallet-session-store.test.ts +80 -7
  6. package/__tests__/evmSignTransaction.test.ts +69 -0
  7. package/__tests__/get-device-state.test.ts +180 -40
  8. package/__tests__/open-wallet-session.test.ts +314 -55
  9. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  10. package/__tests__/protocol-v2.test.ts +768 -139
  11. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  14. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  15. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  16. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  17. package/dist/api/evm/protocol.d.ts +3 -0
  18. package/dist/api/evm/protocol.d.ts.map +1 -0
  19. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
  20. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  21. package/dist/core/deviceEventRegistration.d.ts +2 -0
  22. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/device/Device.d.ts +25 -6
  25. package/dist/device/Device.d.ts.map +1 -1
  26. package/dist/device/DeviceCommands.d.ts.map +1 -1
  27. package/dist/device/DeviceWalletSessionStore.d.ts +0 -1
  28. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
  29. package/dist/events/device.d.ts +4 -0
  30. package/dist/events/device.d.ts.map +1 -1
  31. package/dist/events/ui-request.d.ts +27 -2
  32. package/dist/events/ui-request.d.ts.map +1 -1
  33. package/dist/index.d.ts +63 -12
  34. package/dist/index.js +1164 -904
  35. package/dist/protocols/protocol-v2/features.d.ts +17 -6
  36. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  37. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  38. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  39. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts +1 -0
  40. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  41. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  42. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  43. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/walletSession.d.ts +5 -1
  45. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  46. package/dist/types/api/openWalletSession.d.ts +1 -1
  47. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  48. package/dist/types/api/protocolV2.d.ts +2 -2
  49. package/dist/types/api/protocolV2.d.ts.map +1 -1
  50. package/dist/types/device.d.ts +2 -1
  51. package/dist/types/device.d.ts.map +1 -1
  52. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  53. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  54. package/dist/utils/patch.d.ts +1 -1
  55. package/dist/utils/patch.d.ts.map +1 -1
  56. package/package.json +4 -4
  57. package/src/api/BaseMethod.ts +1 -1
  58. package/src/api/FirmwareUpdateV4.ts +39 -14
  59. package/src/api/GetPassphraseState.ts +17 -0
  60. package/src/api/OpenWalletSession.ts +31 -8
  61. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +24 -2
  62. package/src/api/device/DeviceUnlock.ts +1 -1
  63. package/src/api/evm/EVMGetAddress.ts +2 -2
  64. package/src/api/evm/EVMGetPublicKey.ts +2 -2
  65. package/src/api/evm/EVMSignMessage.ts +2 -2
  66. package/src/api/evm/EVMSignTransaction.ts +2 -2
  67. package/src/api/evm/EVMSignTypedData.ts +35 -45
  68. package/src/api/evm/EVMVerifyMessage.ts +2 -2
  69. package/src/api/evm/protocol.ts +6 -0
  70. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
  71. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  72. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  73. package/src/core/deviceEventRegistration.ts +4 -0
  74. package/src/core/index.ts +46 -4
  75. package/src/data/messages/messages-protocol-v2.json +408 -485
  76. package/src/device/Device.ts +266 -43
  77. package/src/device/DeviceCommands.ts +12 -2
  78. package/src/device/DeviceWalletSessionStore.ts +0 -14
  79. package/src/deviceProfile/buildDeviceFeatures.ts +1 -1
  80. package/src/events/device.ts +4 -0
  81. package/src/events/ui-request.ts +32 -2
  82. package/src/protocols/protocol-v2/features.ts +65 -16
  83. package/src/protocols/protocol-v2/index.ts +5 -0
  84. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
  85. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  86. package/src/protocols/protocol-v2/unlockRetry.ts +51 -18
  87. package/src/protocols/protocol-v2/walletSession.ts +172 -64
  88. package/src/types/api/openWalletSession.ts +1 -1
  89. package/src/types/api/protocolV2.ts +2 -2
  90. package/src/types/device.ts +2 -0
  91. package/src/utils/deviceFeaturesUtils.ts +4 -0
@@ -1,10 +1,11 @@
1
1
  import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
+ import { DeviceSessionPinType, DeviceSessionSeedDomain } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import GetPassphraseState from '../src/api/GetPassphraseState';
5
5
  import OpenWalletSession from '../src/api/OpenWalletSession';
6
6
  import { Device } from '../src/device/Device';
7
7
  import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
8
+ import { ensureProtocolV2WalletSessionUnlocked } from '../src/protocols/protocol-v2/walletSession';
8
9
 
9
10
  jest.mock('../src/data/config', () => ({
10
11
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -43,6 +44,11 @@ const createDevice = ({
43
44
  }),
44
45
  promptPassphrase,
45
46
  },
47
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
48
+ device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
49
+ eventless_wallet_session: true,
50
+ })
51
+ ),
46
52
  getDeviceState: jest.fn().mockResolvedValue({
47
53
  identity: { deviceId: 'device-1' },
48
54
  status: { passphraseProtection },
@@ -82,6 +88,50 @@ describe('openWalletSession', () => {
82
88
  deviceWalletSessionStore.clear();
83
89
  });
84
90
 
91
+ test('unlocks a locked Protocol V2 device before restoring a wallet session', async () => {
92
+ const device = {
93
+ commands: {
94
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
95
+ },
96
+ isBootloader: jest.fn().mockReturnValue(false),
97
+ isProtocolV2: jest.fn().mockReturnValue(true),
98
+ isRomloader: jest.fn().mockReturnValue(false),
99
+ state: { status: { unlocked: false } },
100
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
101
+ updateProtocolV2Status: jest.fn(function updateProtocolV2Status(
102
+ this: { state: { status: { unlocked: boolean } } },
103
+ status: { unlocked?: boolean }
104
+ ) {
105
+ this.state.status.unlocked = status.unlocked ?? this.state.status.unlocked;
106
+ }),
107
+ };
108
+
109
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(true);
110
+ expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
111
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
112
+ source: 'unlock-coordinator',
113
+ reason: 'device-locked',
114
+ deviceOnly: true,
115
+ });
116
+ });
117
+
118
+ test('does not request PIN when wallet-session recovery finds the device unlocked', async () => {
119
+ const device = {
120
+ commands: {
121
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
122
+ },
123
+ isBootloader: jest.fn().mockReturnValue(false),
124
+ isProtocolV2: jest.fn().mockReturnValue(true),
125
+ isRomloader: jest.fn().mockReturnValue(false),
126
+ state: { status: { unlocked: true } },
127
+ unlockDevice: jest.fn(),
128
+ updateProtocolV2Status: jest.fn(),
129
+ };
130
+
131
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(false);
132
+ expect(device.unlockDevice).not.toHaveBeenCalled();
133
+ });
134
+
85
135
  test.each([{ useEmptyPassphrase: true }, { initSession: true }, {}])(
86
136
  'requires the explicit mode in the new public API: %p',
87
137
  legacyParams => {
@@ -108,7 +158,7 @@ describe('openWalletSession', () => {
108
158
  session_id: 'pro2-wallet-session',
109
159
  },
110
160
  });
111
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
161
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
112
162
  const method = new GetPassphraseState({
113
163
  payload: {
114
164
  method: 'getPassphraseState',
@@ -130,7 +180,8 @@ describe('openWalletSession', () => {
130
180
  { cancelDeviceOnReject: false }
131
181
  );
132
182
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
133
- on_device: true,
183
+ passphrase: 'host hidden wallet',
184
+ on_device: false,
134
185
  });
135
186
  expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
136
187
  });
@@ -208,7 +259,7 @@ describe('openWalletSession', () => {
208
259
  protocol: 'V1',
209
260
  walletType: 'standard',
210
261
  deviceId: 'device-1',
211
- passphraseState: 'main-wallet-state',
262
+ passphraseState: null,
212
263
  resumed: false,
213
264
  });
214
265
  expect(method.payload.useEmptyPassphrase).toBe(true);
@@ -229,7 +280,7 @@ describe('openWalletSession', () => {
229
280
  session_id: 'hidden-session',
230
281
  },
231
282
  });
232
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
283
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
233
284
  const method = new OpenWalletSession({
234
285
  payload: {
235
286
  method: 'openWalletSession',
@@ -247,11 +298,47 @@ describe('openWalletSession', () => {
247
298
  });
248
299
  expect(promptPassphrase).toHaveBeenCalled();
249
300
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
250
- on_device: true,
301
+ passphrase: 'host hidden wallet',
302
+ on_device: false,
251
303
  });
252
304
  expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
253
305
  });
254
306
 
307
+ test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
308
+ const typedCall = jest
309
+ .fn()
310
+ .mockResolvedValueOnce({ message: { version: 2 } })
311
+ .mockResolvedValueOnce({ message: {} })
312
+ .mockResolvedValueOnce({
313
+ message: {
314
+ btc_test_address: 'hidden-state-after-preflight',
315
+ session_id: 'hidden-session-after-preflight',
316
+ },
317
+ });
318
+ const method = new OpenWalletSession({
319
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
320
+ });
321
+ method.init();
322
+ const device = createDevice({
323
+ typedCall,
324
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
325
+ });
326
+ device.getDeviceState = jest.fn().mockResolvedValue({
327
+ identity: { deviceId: 'device-1' },
328
+ status: { unlocked: false, passphraseProtection: true },
329
+ });
330
+ method.device = device as any;
331
+
332
+ await expect(method.run()).resolves.toMatchObject({
333
+ walletType: 'hidden',
334
+ passphraseState: 'hidden-state-after-preflight',
335
+ });
336
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
337
+ expect(device.unlockDevice.mock.invocationCallOrder[0]).toBeLessThan(
338
+ typedCall.mock.invocationCallOrder[0]
339
+ );
340
+ });
341
+
255
342
  test('classifies the selected Pro2 wallet from the post-unlock device state', async () => {
256
343
  const typedCall = jest
257
344
  .fn()
@@ -263,7 +350,7 @@ describe('openWalletSession', () => {
263
350
  session_id: 'hidden-session-after-unlock',
264
351
  },
265
352
  });
266
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
353
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
267
354
  const method = new OpenWalletSession({
268
355
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
269
356
  });
@@ -275,10 +362,23 @@ describe('openWalletSession', () => {
275
362
  });
276
363
  device.features.unlocked = false;
277
364
  device.getCurrentPassphraseProtection = () => device.features.passphraseProtection;
278
- device.unlockDevice = jest.fn().mockImplementation(() => {
279
- device.features.unlocked = true;
280
- device.features.passphraseProtection = true;
281
- return Promise.resolve(device.features);
365
+ device.commands.typedCall = jest.fn((request: string, ...args: unknown[]) => {
366
+ if (request === 'DeviceStatusGet') {
367
+ return {
368
+ message: {
369
+ device_id: 'device-1',
370
+ unlocked: true,
371
+ passphrase_enabled: true,
372
+ },
373
+ };
374
+ }
375
+ return typedCall(request, ...args);
376
+ });
377
+ device.updateProtocolV2Status = jest.fn((status: Record<string, unknown>) => {
378
+ device.features.unlocked = status.unlocked ?? device.features.unlocked;
379
+ device.features.passphraseProtection =
380
+ status.passphrase_enabled ?? device.features.passphraseProtection;
381
+ return device.features;
282
382
  });
283
383
  method.device = device as any;
284
384
 
@@ -293,6 +393,35 @@ describe('openWalletSession', () => {
293
393
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
294
394
  });
295
395
 
396
+ test('rejects a Pro2 standard-wallet fallback after selecting a hidden wallet', async () => {
397
+ const typedCall = jest
398
+ .fn()
399
+ .mockResolvedValueOnce({ message: { version: 2 } })
400
+ .mockResolvedValueOnce({ message: {} })
401
+ .mockResolvedValueOnce({
402
+ message: {
403
+ btc_test_address: 'standard-wallet-state',
404
+ session_id: 'standard-wallet-session',
405
+ },
406
+ });
407
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
408
+ const method = new OpenWalletSession({
409
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
410
+ });
411
+ method.init();
412
+ const device = createDevice({
413
+ passphraseProtection: false,
414
+ typedCall,
415
+ promptPassphrase,
416
+ });
417
+ method.device = device as any;
418
+
419
+ await expect(method.run()).rejects.toMatchObject({
420
+ errorCode: HardwareErrorCode.DeviceNotOpenedPassphrase,
421
+ });
422
+ expect(device.clearInternalState).toHaveBeenCalled();
423
+ });
424
+
296
425
  test('select-hidden starts a fresh hidden-wallet session on Protocol V2', async () => {
297
426
  const typedCall = jest
298
427
  .fn()
@@ -304,7 +433,7 @@ describe('openWalletSession', () => {
304
433
  session_id: 'new-hidden-session',
305
434
  },
306
435
  });
307
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
436
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
308
437
  const method = new OpenWalletSession({
309
438
  payload: {
310
439
  method: 'openWalletSession',
@@ -326,7 +455,8 @@ describe('openWalletSession', () => {
326
455
  eventless_wallet_session: true,
327
456
  });
328
457
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
329
- on_device: true,
458
+ passphrase: 'host hidden wallet',
459
+ on_device: false,
330
460
  });
331
461
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
332
462
  expect(promptPassphrase).toHaveBeenCalled();
@@ -344,7 +474,7 @@ describe('openWalletSession', () => {
344
474
  session_id: 'current-device-session',
345
475
  },
346
476
  });
347
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
477
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
348
478
  const method = new OpenWalletSession({
349
479
  payload: {
350
480
  method: 'openWalletSession',
@@ -399,6 +529,7 @@ describe('openWalletSession', () => {
399
529
  });
400
530
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
401
531
  session_id: 'known-session',
532
+ btc_test_address: 'hidden-state',
402
533
  });
403
534
  expect(promptPassphrase).not.toHaveBeenCalled();
404
535
  });
@@ -569,14 +700,18 @@ describe('openWalletSession', () => {
569
700
  protocol: 'V2',
570
701
  walletType: 'standard',
571
702
  deviceId: 'device-1',
572
- passphraseState: 'main-wallet-state',
703
+ passphraseState: null,
573
704
  resumed: false,
574
705
  });
575
706
  expect(typedCall).toHaveBeenCalledTimes(2);
576
707
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
577
708
  eventless_wallet_session: true,
578
709
  });
579
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
710
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
711
+ source: 'wallet-session-coordinator',
712
+ reason: 'open-wallet',
713
+ deviceOnly: true,
714
+ });
580
715
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
581
716
  expect(promptPassphrase).not.toHaveBeenCalled();
582
717
  expect(device.passphraseState).toBeUndefined();
@@ -602,7 +737,7 @@ describe('openWalletSession', () => {
602
737
  .fn()
603
738
  .mockResolvedValueOnce({
604
739
  identity: { deviceId: undefined },
605
- status: { passphraseProtection: true },
740
+ status: { unlocked: false, passphraseProtection: true },
606
741
  })
607
742
  .mockResolvedValueOnce({
608
743
  identity: { deviceId: 'device-1' },
@@ -618,10 +753,14 @@ describe('openWalletSession', () => {
618
753
  protocol: 'V2',
619
754
  walletType: 'standard',
620
755
  deviceId: 'device-1',
621
- passphraseState: 'main-wallet-state',
756
+ passphraseState: null,
622
757
  resumed: false,
623
758
  });
624
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
759
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
760
+ source: 'wallet-session-coordinator',
761
+ reason: 'open-wallet',
762
+ deviceOnly: true,
763
+ });
625
764
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
626
765
  });
627
766
 
@@ -641,6 +780,14 @@ describe('openWalletSession', () => {
641
780
  method.init();
642
781
  const device = createDevice({ typedCall });
643
782
  device.features.unlockedAttachPin = true;
783
+ device.getDeviceState = jest.fn().mockResolvedValue({
784
+ identity: { deviceId: 'device-1' },
785
+ status: {
786
+ unlocked: true,
787
+ unlockedAttachPin: true,
788
+ passphraseProtection: true,
789
+ },
790
+ });
644
791
  device.unlockDevice = jest.fn().mockImplementation(() => {
645
792
  device.features.unlockedAttachPin = false;
646
793
  return Promise.resolve(device.features);
@@ -649,9 +796,13 @@ describe('openWalletSession', () => {
649
796
 
650
797
  await expect(method.run()).resolves.toMatchObject({
651
798
  walletType: 'standard',
652
- passphraseState: 'main-wallet-state',
799
+ passphraseState: null,
800
+ });
801
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
802
+ source: 'wallet-session-coordinator',
803
+ reason: 'open-wallet',
804
+ deviceOnly: true,
653
805
  });
654
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
655
806
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
656
807
  });
657
808
 
@@ -666,7 +817,7 @@ describe('openWalletSession', () => {
666
817
  session_id: 'hidden-session',
667
818
  },
668
819
  });
669
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
820
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
670
821
  const method = new OpenWalletSession({
671
822
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
672
823
  });
@@ -683,7 +834,8 @@ describe('openWalletSession', () => {
683
834
  resumed: false,
684
835
  });
685
836
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
686
- on_device: true,
837
+ passphrase: 'host hidden wallet',
838
+ on_device: false,
687
839
  });
688
840
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
689
841
  expect(promptPassphrase).toHaveBeenCalled();
@@ -691,7 +843,7 @@ describe('openWalletSession', () => {
691
843
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
692
844
  });
693
845
 
694
- test('gets the prepared session after on-device passphrase entry', async () => {
846
+ test('gets the prepared session after host passphrase entry', async () => {
695
847
  const typedCall = jest
696
848
  .fn()
697
849
  .mockResolvedValueOnce({ message: { version: 2 } })
@@ -699,7 +851,7 @@ describe('openWalletSession', () => {
699
851
  .mockResolvedValueOnce({
700
852
  message: { btc_test_address: 'device-state', session_id: 'device-session' },
701
853
  });
702
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
854
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
703
855
  const method = new OpenWalletSession({
704
856
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
705
857
  });
@@ -711,11 +863,37 @@ describe('openWalletSession', () => {
711
863
  passphraseState: 'device-state',
712
864
  });
713
865
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
714
- on_device: true,
866
+ passphrase: 'host hidden wallet',
867
+ on_device: false,
715
868
  });
716
869
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
717
870
  });
718
871
 
872
+ test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
873
+ const typedCall = jest
874
+ .fn()
875
+ .mockResolvedValueOnce({ message: { version: 2 } })
876
+ .mockResolvedValueOnce({ message: {} })
877
+ .mockResolvedValueOnce({
878
+ message: { btc_test_address: 'device-state', session_id: 'device-session' },
879
+ });
880
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
881
+ const method = new OpenWalletSession({
882
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
883
+ });
884
+ method.init();
885
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
886
+
887
+ await expect(method.run()).resolves.toMatchObject({
888
+ walletType: 'hidden',
889
+ passphraseState: 'device-state',
890
+ });
891
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
892
+ on_device: true,
893
+ });
894
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
895
+ });
896
+
719
897
  test('forwards a host passphrase to Pro2 firmware', async () => {
720
898
  const typedCall = jest
721
899
  .fn()
@@ -832,7 +1010,9 @@ describe('openWalletSession', () => {
832
1010
  },
833
1011
  { cancelDeviceOnReject: false }
834
1012
  );
835
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
1013
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
1014
+ emitUiEvent: false,
1015
+ });
836
1016
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
837
1017
  });
838
1018
 
@@ -910,7 +1090,7 @@ describe('openWalletSession', () => {
910
1090
  .fn()
911
1091
  .mockResolvedValueOnce({ message: { version: 2 } })
912
1092
  .mockRejectedValueOnce(new Error(message));
913
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
1093
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
914
1094
  const method = new OpenWalletSession({
915
1095
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
916
1096
  });
@@ -963,7 +1143,7 @@ describe('openWalletSession', () => {
963
1143
  expect(typedCall).toHaveBeenCalledTimes(1);
964
1144
  });
965
1145
 
966
- test('unlocks and retries hidden-wallet preparation when the device is locked', async () => {
1146
+ test('does not unlock or retry when wallet preparation reports a late DeviceLocked', async () => {
967
1147
  const lockedError = { errorCode: HardwareErrorCode.DeviceLocked };
968
1148
  const typedCall = jest
969
1149
  .fn()
@@ -973,7 +1153,7 @@ describe('openWalletSession', () => {
973
1153
  .mockResolvedValueOnce({
974
1154
  message: { btc_test_address: 'hidden-state', session_id: 'hidden-session' },
975
1155
  });
976
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
1156
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
977
1157
  const method = new OpenWalletSession({
978
1158
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
979
1159
  });
@@ -981,12 +1161,9 @@ describe('openWalletSession', () => {
981
1161
  const device = createDevice({ typedCall, promptPassphrase });
982
1162
  method.device = device as any;
983
1163
 
984
- await expect(method.run()).resolves.toMatchObject({
985
- walletType: 'hidden',
986
- passphraseState: 'hidden-state',
987
- });
988
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
989
- expect(typedCall).toHaveBeenCalledTimes(4);
1164
+ await expect(method.run()).rejects.toBe(lockedError);
1165
+ expect(device.unlockDevice).not.toHaveBeenCalled();
1166
+ expect(typedCall).toHaveBeenCalledTimes(2);
990
1167
  });
991
1168
 
992
1169
  test('does not fall back when wallet preparation is rejected', async () => {
@@ -995,7 +1172,7 @@ describe('openWalletSession', () => {
995
1172
  .fn()
996
1173
  .mockResolvedValueOnce({ message: { version: 2 } })
997
1174
  .mockRejectedValueOnce(invalidSessionError);
998
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
1175
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
999
1176
  const method = new OpenWalletSession({
1000
1177
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1001
1178
  });
@@ -1026,7 +1203,7 @@ describe('openWalletSession', () => {
1026
1203
  .mockResolvedValueOnce({ message: { version: 2 } })
1027
1204
  .mockResolvedValueOnce({ message: {} })
1028
1205
  .mockResolvedValueOnce({ message });
1029
- const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
1206
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1030
1207
  const method = new OpenWalletSession({
1031
1208
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1032
1209
  });
@@ -1076,6 +1253,7 @@ describe('openWalletSession', () => {
1076
1253
  });
1077
1254
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1078
1255
  session_id: 'known-session',
1256
+ btc_test_address: 'hidden-state',
1079
1257
  });
1080
1258
  expect(promptPassphrase).not.toHaveBeenCalled();
1081
1259
  });
@@ -1107,7 +1285,7 @@ describe('openWalletSession', () => {
1107
1285
  .fn()
1108
1286
  .mockResolvedValueOnce({
1109
1287
  identity: { deviceId: undefined },
1110
- status: { passphraseProtection: true },
1288
+ status: { unlocked: false, passphraseProtection: true },
1111
1289
  })
1112
1290
  .mockResolvedValueOnce({
1113
1291
  identity: { deviceId: 'device-1' },
@@ -1130,6 +1308,7 @@ describe('openWalletSession', () => {
1130
1308
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
1131
1309
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1132
1310
  session_id: 'known-session',
1311
+ btc_test_address: 'hidden-state',
1133
1312
  });
1134
1313
  const sessionGetCall = typedCall.mock.calls.findIndex(
1135
1314
  ([requestName]) => requestName === 'DeviceSessionGet'
@@ -1183,13 +1362,23 @@ describe('openWalletSession', () => {
1183
1362
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
1184
1363
  });
1185
1364
 
1186
- test('does not fall back to wallet selection when a cached resume is invalid', async () => {
1187
- const typedCall = jest.fn().mockRejectedValue(
1188
- Object.assign(new Error('Invalid session'), {
1189
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1190
- })
1191
- );
1192
- const promptPassphrase = jest.fn();
1365
+ test('reselects the expected hidden wallet when a cached Protocol V2 session is invalid', async () => {
1366
+ const typedCall = jest
1367
+ .fn()
1368
+ .mockResolvedValueOnce({ message: { version: 2 } })
1369
+ .mockRejectedValueOnce(
1370
+ Object.assign(new Error('Invalid session'), {
1371
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
1372
+ })
1373
+ )
1374
+ .mockResolvedValueOnce({ message: {} })
1375
+ .mockResolvedValueOnce({
1376
+ message: {
1377
+ btc_test_address: 'hidden-state',
1378
+ session_id: 'renewed-session',
1379
+ },
1380
+ });
1381
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1193
1382
  const method = new OpenWalletSession({
1194
1383
  payload: {
1195
1384
  method: 'openWalletSession',
@@ -1203,15 +1392,32 @@ describe('openWalletSession', () => {
1203
1392
  deviceWalletSessionStore.set('device-1', 'hidden-state', 'expired-session');
1204
1393
  method.device = createDevice({ typedCall, promptPassphrase }) as any;
1205
1394
 
1206
- await expect(method.run()).rejects.toMatchObject({
1207
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1395
+ await expect(method.run()).resolves.toEqual({
1396
+ protocol: 'V2',
1397
+ walletType: 'hidden',
1398
+ deviceId: 'device-1',
1399
+ passphraseState: 'hidden-state',
1400
+ resumed: false,
1208
1401
  });
1209
- expect(promptPassphrase).not.toHaveBeenCalled();
1402
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1403
+ session_id: 'expired-session',
1404
+ btc_test_address: 'hidden-state',
1405
+ });
1406
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
1407
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
1210
1408
  });
1211
1409
 
1212
- test('rejects resume-hidden when the SDK store has no matching session', async () => {
1213
- const typedCall = jest.fn();
1214
- const promptPassphrase = jest.fn();
1410
+ test('selects the expected hidden wallet when Protocol V2 has no cached session', async () => {
1411
+ const typedCall = jest
1412
+ .fn()
1413
+ .mockResolvedValueOnce({ message: { version: 2 } })
1414
+ .mockResolvedValueOnce({
1415
+ message: {
1416
+ btc_test_address: 'hidden-state',
1417
+ session_id: 'new-session',
1418
+ },
1419
+ });
1420
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1215
1421
  const method = new OpenWalletSession({
1216
1422
  payload: {
1217
1423
  method: 'openWalletSession',
@@ -1224,10 +1430,63 @@ describe('openWalletSession', () => {
1224
1430
  method.init();
1225
1431
  method.device = createDevice({ typedCall, promptPassphrase }) as any;
1226
1432
 
1227
- await expect(method.run()).rejects.toMatchObject({
1228
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1433
+ await expect(method.run()).resolves.toEqual({
1434
+ protocol: 'V2',
1435
+ walletType: 'hidden',
1436
+ deviceId: 'device-1',
1437
+ passphraseState: 'hidden-state',
1438
+ resumed: false,
1439
+ });
1440
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1441
+ btc_test_address: 'hidden-state',
1229
1442
  });
1230
- expect(typedCall).not.toHaveBeenCalled();
1231
1443
  expect(promptPassphrase).not.toHaveBeenCalled();
1444
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
1445
+ });
1446
+
1447
+ test.each([
1448
+ {
1449
+ deriveCardano: false,
1450
+ seedDomains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1451
+ },
1452
+ {
1453
+ deriveCardano: true,
1454
+ seedDomains: [
1455
+ DeviceSessionSeedDomain.SeedDomain_Standard,
1456
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
1457
+ ],
1458
+ },
1459
+ ])('requests the selected seed domains: $seedDomains', async ({ deriveCardano, seedDomains }) => {
1460
+ const typedCall = jest
1461
+ .fn()
1462
+ .mockResolvedValueOnce({ message: { version: 2 } })
1463
+ .mockResolvedValueOnce({ message: {} })
1464
+ .mockResolvedValueOnce({
1465
+ message: {
1466
+ btc_test_address: 'hidden-state',
1467
+ session_id: 'new-session',
1468
+ },
1469
+ });
1470
+ const method = new OpenWalletSession({
1471
+ payload: {
1472
+ method: 'openWalletSession',
1473
+ connectId: 'connect-id',
1474
+ mode: 'select-hidden',
1475
+ deriveCardano,
1476
+ },
1477
+ });
1478
+ method.init();
1479
+ method.device = createDevice({
1480
+ typedCall,
1481
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
1482
+ }) as any;
1483
+
1484
+ await expect(method.run()).resolves.toMatchObject({
1485
+ walletType: 'hidden',
1486
+ passphraseState: 'hidden-state',
1487
+ });
1488
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1489
+ seed_domains: seedDomains,
1490
+ });
1232
1491
  });
1233
1492
  });