@onekeyfe/hd-core 1.2.0-alpha.21 → 1.2.0-alpha.22

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 (175) hide show
  1. package/__tests__/DeviceCommands.test.ts +87 -2
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +1 -1
  5. package/__tests__/device-wallet-session-store.test.ts +111 -7
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  7. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  8. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  9. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  10. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +200 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  12. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  13. package/__tests__/open-wallet-session.test.ts +253 -13
  14. package/__tests__/pro2Wallpaper.test.ts +7 -18
  15. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  16. package/__tests__/protocol-v2.test.ts +415 -145
  17. package/__tests__/public-device-state-api.test.ts +69 -62
  18. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  19. package/dist/api/BaseMethod.d.ts +2 -1
  20. package/dist/api/BaseMethod.d.ts.map +1 -1
  21. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  25. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  31. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  32. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  33. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  34. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  35. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  36. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  37. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  38. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  39. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  40. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  41. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  42. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  43. package/dist/api/firmware/FirmwareArtifactSource.d.ts +26 -0
  44. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  45. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  46. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  47. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  48. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  49. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  50. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +24 -0
  52. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  53. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +25 -0
  54. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  55. package/dist/api/firmware/getBinary.d.ts +7 -0
  56. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  57. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  58. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  59. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  60. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  61. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  62. package/dist/api/index.d.ts +0 -17
  63. package/dist/api/index.d.ts.map +1 -1
  64. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  65. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  66. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  67. package/dist/data-manager/DataManager.d.ts +1 -0
  68. package/dist/data-manager/DataManager.d.ts.map +1 -1
  69. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  70. package/dist/device/Device.d.ts.map +1 -1
  71. package/dist/device/DeviceCommands.d.ts.map +1 -1
  72. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  73. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  74. package/dist/events/logBlockEvent.d.ts.map +1 -1
  75. package/dist/index.d.ts +265 -226
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3045 -1550
  78. package/dist/inject.d.ts.map +1 -1
  79. package/dist/lowLevelInject.d.ts.map +1 -1
  80. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  81. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  82. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  83. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  84. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  85. package/dist/topLevelInject.d.ts.map +1 -1
  86. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  87. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  88. package/dist/types/api/deviceUnlock.d.ts +2 -2
  89. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  90. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  91. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  92. package/dist/types/api/export.d.ts +4 -1
  93. package/dist/types/api/export.d.ts.map +1 -1
  94. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  95. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  96. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  97. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  98. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  99. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  100. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  101. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  102. package/dist/types/api/index.d.ts +10 -19
  103. package/dist/types/api/index.d.ts.map +1 -1
  104. package/dist/types/api/openWalletSession.d.ts +1 -0
  105. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  106. package/dist/types/api/protocolV2.d.ts +3 -76
  107. package/dist/types/api/protocolV2.d.ts.map +1 -1
  108. package/dist/types/api/sessionCache.d.ts +4 -1
  109. package/dist/types/api/sessionCache.d.ts.map +1 -1
  110. package/dist/types/settings.d.ts +13 -0
  111. package/dist/types/settings.d.ts.map +1 -1
  112. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  113. package/dist/utils/patch.d.ts +1 -1
  114. package/dist/utils/patch.d.ts.map +1 -1
  115. package/package.json +4 -4
  116. package/src/api/BaseMethod.ts +8 -3
  117. package/src/api/CheckAllFirmwareRelease.ts +16 -3
  118. package/src/api/ClearSessionCache.ts +4 -0
  119. package/src/api/FirmwareUpdate.ts +8 -1
  120. package/src/api/FirmwareUpdateV2.ts +284 -119
  121. package/src/api/FirmwareUpdateV3.ts +248 -55
  122. package/src/api/FirmwareUpdateV4.ts +816 -328
  123. package/src/api/OpenWalletSession.ts +74 -25
  124. package/src/api/UploadPortfolio.ts +1 -1
  125. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  126. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  127. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  128. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  129. package/src/api/device/DeviceChangePin.ts +1 -1
  130. package/src/api/device/DeviceUnlock.ts +1 -2
  131. package/src/api/device/DeviceUpdateBootloader.ts +114 -6
  132. package/src/api/device/DeviceWipe.ts +1 -1
  133. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  134. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  135. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  136. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  137. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  138. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +378 -0
  139. package/src/api/firmware/updateBootloader.ts +19 -4
  140. package/src/api/firmware/uploadFirmware.ts +140 -22
  141. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  142. package/src/api/index.ts +0 -18
  143. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  144. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  145. package/src/data/messages/messages-protocol-v2.json +77 -4
  146. package/src/data-manager/DataManager.ts +33 -33
  147. package/src/data-manager/connectSettings.ts +11 -0
  148. package/src/device/Device.ts +50 -60
  149. package/src/device/DeviceCommands.ts +58 -14
  150. package/src/device/DeviceStateMapper.ts +18 -1
  151. package/src/device/DeviceStateProjector.ts +7 -1
  152. package/src/events/logBlockEvent.ts +1 -0
  153. package/src/index.ts +5 -0
  154. package/src/inject.ts +22 -25
  155. package/src/lowLevelInject.ts +5 -1
  156. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  157. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -2
  158. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  159. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  160. package/src/protocols/protocol-v2/walletSession.ts +68 -12
  161. package/src/topLevelInject.ts +5 -1
  162. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  163. package/src/types/api/deviceUnlock.ts +2 -5
  164. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  165. package/src/types/api/export.ts +18 -0
  166. package/src/types/api/firmwareUpdate.ts +76 -0
  167. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  168. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  169. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  170. package/src/types/api/index.ts +14 -34
  171. package/src/types/api/openWalletSession.ts +2 -0
  172. package/src/types/api/protocolV2.ts +3 -156
  173. package/src/types/api/sessionCache.ts +9 -4
  174. package/src/types/settings.ts +13 -0
  175. package/src/utils/deviceFeaturesUtils.ts +5 -1
@@ -0,0 +1,70 @@
1
+ import { EDeviceType } from '@onekeyfe/hd-shared';
2
+
3
+ import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
4
+
5
+ import type { Device } from '../../src/device/Device';
6
+ import type { FirmwareByteSource } from '../../src/api/firmware/FirmwareArtifactSource';
7
+
8
+ jest.mock('../../src/data/config', () => ({
9
+ getSDKVersion: jest.fn(() => '1.0.0'),
10
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
11
+ }));
12
+
13
+ describe('uploadFirmwareFromSource', () => {
14
+ it('serves legacy Touch firmware requests without materializing the complete artifact', async () => {
15
+ const size = 700 * 1024;
16
+ const requestedRanges: Array<{ offset: number; length: number }> = [];
17
+ const source: FirmwareByteSource = {
18
+ size,
19
+ readAt: jest.fn((offset: number, length: number) => {
20
+ requestedRanges.push({ offset, length });
21
+ return Promise.resolve(new ArrayBuffer(length));
22
+ }),
23
+ close: jest.fn(() => Promise.resolve()),
24
+ };
25
+ let nextOffset = 0;
26
+ const requestedLength = 128 * 1024;
27
+ const typedCall = jest.fn((type: string) => {
28
+ if (type === 'FirmwareErase') {
29
+ return Promise.resolve({
30
+ type: 'FirmwareRequest',
31
+ message: {
32
+ offset: nextOffset,
33
+ length: Math.min(requestedLength, size - nextOffset),
34
+ },
35
+ });
36
+ }
37
+ if (type === 'FirmwareUpload') {
38
+ nextOffset += Math.min(requestedLength, size - nextOffset);
39
+ if (nextOffset < size) {
40
+ return Promise.resolve({
41
+ type: 'FirmwareRequest',
42
+ message: {
43
+ offset: nextOffset,
44
+ length: Math.min(requestedLength, size - nextOffset),
45
+ },
46
+ });
47
+ }
48
+ return Promise.resolve({
49
+ type: 'Success',
50
+ message: { message: 'installed' },
51
+ });
52
+ }
53
+ return Promise.reject(new Error(`Unexpected command: ${type}`));
54
+ });
55
+ const device = {
56
+ features: undefined,
57
+ emit: jest.fn(),
58
+ getCurrentDeviceType: () => EDeviceType.Touch,
59
+ toMessageObject: () => ({}),
60
+ } as unknown as Device;
61
+
62
+ await expect(
63
+ uploadFirmwareFromSource('firmware', typedCall as never, jest.fn(), device, source)
64
+ ).resolves.toEqual({ message: 'installed' });
65
+
66
+ expect(requestedRanges).toHaveLength(Math.ceil(size / requestedLength));
67
+ expect(requestedRanges.every(({ length }) => length <= 256 * 1024)).toBe(true);
68
+ expect(requestedRanges.some(({ length }) => length === size)).toBe(false);
69
+ });
70
+ });
@@ -28,6 +28,7 @@ const createDevice = ({
28
28
  commands: { typedCall, promptPassphrase },
29
29
  getDeviceState: jest.fn().mockResolvedValue({
30
30
  identity: { deviceId: 'device-1' },
31
+ status: { passphraseProtection },
31
32
  }),
32
33
  getCurrentDeviceId: () => 'device-1',
33
34
  getCurrentPassphraseProtection: () => passphraseProtection,
@@ -145,6 +146,7 @@ describe('openWalletSession', () => {
145
146
  walletType: 'standard',
146
147
  deviceId: 'device-1',
147
148
  passphraseState: null,
149
+ sessionId: 'main-wallet-session',
148
150
  resumed: false,
149
151
  });
150
152
  expect(method.payload.useEmptyPassphrase).toBe(true);
@@ -175,6 +177,43 @@ describe('openWalletSession', () => {
175
177
  });
176
178
  });
177
179
 
180
+ test('classifies the selected Pro2 wallet from the post-unlock device state', async () => {
181
+ const typedCall = jest
182
+ .fn()
183
+ .mockResolvedValueOnce({ message: { version: 2 } })
184
+ .mockResolvedValueOnce({
185
+ message: {
186
+ btc_test_address: 'hidden-state-after-unlock',
187
+ session_id: 'hidden-session-after-unlock',
188
+ },
189
+ });
190
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden secret' });
191
+ const method = new OpenWalletSession({
192
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
193
+ });
194
+ method.init();
195
+ const device = createDevice({ passphraseProtection: false, typedCall, promptPassphrase });
196
+ device.features.unlocked = false;
197
+ device.getCurrentPassphraseProtection = () => device.features.passphraseProtection;
198
+ device.unlockDevice = jest.fn().mockImplementation(() => {
199
+ device.features.unlocked = true;
200
+ device.features.passphraseProtection = true;
201
+ return Promise.resolve(device.features);
202
+ });
203
+ method.device = device as any;
204
+
205
+ await expect(method.run()).resolves.toEqual({
206
+ protocol: 'V2',
207
+ walletType: 'hidden',
208
+ deviceId: 'device-1',
209
+ passphraseState: 'hidden-state-after-unlock',
210
+ sessionId: 'hidden-session-after-unlock',
211
+ resumed: false,
212
+ });
213
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
214
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
215
+ });
216
+
178
217
  test('uses legacy initSession to replace a hidden-wallet session on Protocol V2', async () => {
179
218
  const typedCall = jest.fn().mockResolvedValue({
180
219
  message: {
@@ -201,10 +240,16 @@ describe('openWalletSession', () => {
201
240
  walletType: 'hidden',
202
241
  deviceId: 'device-1',
203
242
  passphraseState: 'new-hidden-state',
243
+ sessionId: 'new-hidden-session',
204
244
  resumed: false,
205
245
  });
206
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
207
- expect(promptPassphrase).not.toHaveBeenCalled();
246
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
247
+ eventless_wallet_session: true,
248
+ });
249
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
250
+ select: { host_passphrase: { passphrase: 'new hidden secret' } },
251
+ });
252
+ expect(promptPassphrase).toHaveBeenCalled();
208
253
  expect(deviceWalletSessionStore.get('device-1', 'stale-hidden-state')).toBeUndefined();
209
254
  expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
210
255
  });
@@ -266,10 +311,11 @@ describe('openWalletSession', () => {
266
311
  walletType: 'hidden',
267
312
  deviceId: 'device-1',
268
313
  passphraseState: 'hidden-state',
314
+ sessionId: 'renewed-session',
269
315
  resumed: true,
270
316
  });
271
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
272
- session_id: 'known-session',
317
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
318
+ resume: { session_id: 'known-session' },
273
319
  });
274
320
  expect(promptPassphrase).not.toHaveBeenCalled();
275
321
  });
@@ -378,7 +424,7 @@ describe('openWalletSession', () => {
378
424
  });
379
425
 
380
426
  test('opens the standard wallet without selecting a hidden wallet', async () => {
381
- const typedCall = jest.fn();
427
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
382
428
  const promptPassphrase = jest.fn();
383
429
  const method = new OpenWalletSession({
384
430
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
@@ -395,17 +441,61 @@ describe('openWalletSession', () => {
395
441
  passphraseState: null,
396
442
  resumed: false,
397
443
  });
398
- expect(typedCall).not.toHaveBeenCalled();
444
+ expect(typedCall).toHaveBeenCalledTimes(1);
445
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
446
+ eventless_wallet_session: true,
447
+ });
399
448
  expect(promptPassphrase).not.toHaveBeenCalled();
400
449
  expect(device.passphraseState).toBeUndefined();
401
450
  });
402
451
 
452
+ test('opens a locked Protocol V2 standard wallet after deviceId becomes available', async () => {
453
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
454
+ const method = new OpenWalletSession({
455
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
456
+ });
457
+ method.init();
458
+ const device = createDevice({ typedCall });
459
+ device.features.unlocked = false;
460
+ device.getDeviceState = jest
461
+ .fn()
462
+ .mockResolvedValueOnce({
463
+ identity: { deviceId: undefined },
464
+ status: { passphraseProtection: true },
465
+ })
466
+ .mockResolvedValueOnce({
467
+ identity: { deviceId: 'device-1' },
468
+ status: { passphraseProtection: true },
469
+ });
470
+ device.unlockDevice = jest.fn().mockImplementation(() => {
471
+ device.features.unlocked = true;
472
+ return Promise.resolve(device.features);
473
+ });
474
+ method.device = device as any;
475
+
476
+ await expect(method.run()).resolves.toEqual({
477
+ protocol: 'V2',
478
+ walletType: 'standard',
479
+ deviceId: 'device-1',
480
+ passphraseState: null,
481
+ resumed: false,
482
+ });
483
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
484
+ expect(device.getDeviceState).toHaveBeenCalledTimes(2);
485
+ });
486
+
403
487
  test('rejects a standard-wallet request when Protocol V2 is unlocked by Attach PIN', async () => {
488
+ const typedCall = jest.fn().mockResolvedValue({
489
+ message: {
490
+ btc_test_address: 'attach-wallet-state',
491
+ session_id: 'attach-wallet-session',
492
+ },
493
+ });
404
494
  const method = new OpenWalletSession({
405
495
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
406
496
  });
407
497
  method.init();
408
- const device = createDevice();
498
+ const device = createDevice({ typedCall });
409
499
  device.features.unlockedAttachPin = true;
410
500
  method.device = device as any;
411
501
 
@@ -416,7 +506,7 @@ describe('openWalletSession', () => {
416
506
  expect(device.clearInternalState).toHaveBeenCalled();
417
507
  });
418
508
 
419
- test('selects a hidden wallet and keeps the session inside the SDK store', async () => {
509
+ test('selects a hidden wallet and returns the CLI compatibility session', async () => {
420
510
  const typedCall = jest.fn().mockResolvedValue({
421
511
  message: {
422
512
  btc_test_address: 'hidden-state',
@@ -437,14 +527,63 @@ describe('openWalletSession', () => {
437
527
  walletType: 'hidden',
438
528
  deviceId: 'device-1',
439
529
  passphraseState: 'hidden-state',
530
+ sessionId: 'hidden-session',
440
531
  resumed: false,
441
532
  });
442
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
443
- expect(promptPassphrase).not.toHaveBeenCalled();
533
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
534
+ select: { host_passphrase: { passphrase: 'hidden secret' } },
535
+ });
536
+ expect(promptPassphrase).toHaveBeenCalled();
444
537
  expect(device.passphraseState).toBeUndefined();
445
538
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
446
539
  });
447
540
 
541
+ test('selects on-device passphrase entry through DeviceSessionOpen', async () => {
542
+ const typedCall = jest.fn().mockResolvedValue({
543
+ message: { btc_test_address: 'device-state', session_id: 'device-session' },
544
+ });
545
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
546
+ const method = new OpenWalletSession({
547
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
548
+ });
549
+ method.init();
550
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
551
+
552
+ await expect(method.run()).resolves.toMatchObject({
553
+ walletType: 'hidden',
554
+ passphraseState: 'device-state',
555
+ });
556
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
557
+ select: { passphrase_on_device: {} },
558
+ });
559
+ });
560
+
561
+ test('selects Attach PIN only when the device reports an existing binding', async () => {
562
+ const typedCall = jest.fn().mockResolvedValue({
563
+ message: { btc_test_address: 'attach-state', session_id: 'attach-session' },
564
+ });
565
+ const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
566
+ const method = new OpenWalletSession({
567
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
568
+ });
569
+ method.init();
570
+ const device = createDevice({ typedCall, promptPassphrase });
571
+ device.features.attachToPinEnabled = true;
572
+ method.device = device as any;
573
+
574
+ await expect(method.run()).resolves.toMatchObject({
575
+ walletType: 'hidden',
576
+ passphraseState: 'attach-state',
577
+ });
578
+ expect(promptPassphrase).toHaveBeenCalledWith(
579
+ expect.objectContaining({ existsAttachPinUser: true }),
580
+ { cancelDeviceOnReject: false }
581
+ );
582
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
583
+ select: { attach_pin_on_device: {} },
584
+ });
585
+ });
586
+
448
587
  test.each([
449
588
  {
450
589
  missingField: 'session_id',
@@ -468,7 +607,7 @@ describe('openWalletSession', () => {
468
607
 
469
608
  await expect(method.run()).rejects.toMatchObject({
470
609
  errorCode: HardwareErrorCode.RuntimeError,
471
- message: 'DeviceSessionGet returned an incomplete DeviceSession response.',
610
+ message: 'DeviceSessionOpen returned an incomplete DeviceSession response.',
472
611
  });
473
612
  expect(device.clearInternalState).toHaveBeenCalled();
474
613
  expect(device.updateInternalState).not.toHaveBeenCalled();
@@ -501,14 +640,115 @@ describe('openWalletSession', () => {
501
640
  walletType: 'hidden',
502
641
  deviceId: 'device-1',
503
642
  passphraseState: 'hidden-state',
643
+ sessionId: 'renewed-session',
504
644
  resumed: true,
505
645
  });
506
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
507
- session_id: 'known-session',
646
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
647
+ resume: { session_id: 'known-session' },
508
648
  });
509
649
  expect(promptPassphrase).not.toHaveBeenCalled();
510
650
  });
511
651
 
652
+ test('validates a locked Protocol V2 resume binding after deviceId refresh', async () => {
653
+ const typedCall = jest.fn().mockResolvedValue({
654
+ message: {
655
+ btc_test_address: 'hidden-state',
656
+ session_id: 'renewed-session',
657
+ },
658
+ });
659
+ const method = new OpenWalletSession({
660
+ payload: {
661
+ method: 'openWalletSession',
662
+ connectId: 'connect-id',
663
+ mode: 'resume-hidden',
664
+ deviceId: 'device-1',
665
+ passphraseState: 'hidden-state',
666
+ },
667
+ });
668
+ method.init();
669
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
670
+ const device = createDevice({ typedCall });
671
+ device.features.unlocked = false;
672
+ device.getDeviceState = jest
673
+ .fn()
674
+ .mockResolvedValueOnce({
675
+ identity: { deviceId: undefined },
676
+ status: { passphraseProtection: true },
677
+ })
678
+ .mockResolvedValueOnce({
679
+ identity: { deviceId: 'device-1' },
680
+ status: { passphraseProtection: true },
681
+ });
682
+ device.unlockDevice = jest.fn().mockImplementation(() => {
683
+ device.features.unlocked = true;
684
+ return Promise.resolve(device.features);
685
+ });
686
+ method.device = device as any;
687
+
688
+ await expect(method.run()).resolves.toMatchObject({
689
+ protocol: 'V2',
690
+ walletType: 'hidden',
691
+ deviceId: 'device-1',
692
+ passphraseState: 'hidden-state',
693
+ resumed: true,
694
+ });
695
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
696
+ expect(device.getDeviceState).toHaveBeenCalledTimes(2);
697
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
698
+ resume: { session_id: 'known-session' },
699
+ });
700
+ const sessionOpenCall = typedCall.mock.calls.findIndex(
701
+ ([requestName]) => requestName === 'DeviceSessionOpen'
702
+ );
703
+ expect(device.getDeviceState.mock.invocationCallOrder[1]).toBeLessThan(
704
+ typedCall.mock.invocationCallOrder[sessionOpenCall]
705
+ );
706
+ });
707
+
708
+ test('rejects a Protocol V2 resume when the refreshed deviceId does not match', async () => {
709
+ const typedCall = jest.fn().mockResolvedValue({
710
+ message: {
711
+ btc_test_address: 'hidden-state',
712
+ session_id: 'renewed-session',
713
+ },
714
+ });
715
+ const method = new OpenWalletSession({
716
+ payload: {
717
+ method: 'openWalletSession',
718
+ connectId: 'connect-id',
719
+ mode: 'resume-hidden',
720
+ deviceId: 'device-1',
721
+ passphraseState: 'hidden-state',
722
+ },
723
+ });
724
+ method.init();
725
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
726
+ const device = createDevice({ typedCall });
727
+ device.features.unlocked = false;
728
+ device.getDeviceState = jest
729
+ .fn()
730
+ .mockResolvedValueOnce({
731
+ identity: { deviceId: undefined },
732
+ status: { passphraseProtection: true },
733
+ })
734
+ .mockResolvedValueOnce({
735
+ identity: { deviceId: 'other-device' },
736
+ status: { passphraseProtection: true },
737
+ });
738
+ device.unlockDevice = jest.fn().mockImplementation(() => {
739
+ device.features.unlocked = true;
740
+ return Promise.resolve(device.features);
741
+ });
742
+ method.device = device as any;
743
+
744
+ await expect(method.run()).rejects.toMatchObject({
745
+ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError,
746
+ });
747
+ expect(typedCall).not.toHaveBeenCalled();
748
+ expect(device.clearInternalState).not.toHaveBeenCalled();
749
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
750
+ });
751
+
512
752
  test('does not fall back to wallet selection when a cached resume is invalid', async () => {
513
753
  const typedCall = jest.fn().mockRejectedValue(
514
754
  Object.assign(new Error('Invalid session'), {
@@ -10,18 +10,7 @@ describe('encodePro2Wallpaper', () => {
10
10
 
11
11
  expect(result.colorFormat).toBe('RGB565');
12
12
  expect(Array.from(result.data.slice(0, 12))).toEqual([
13
- 0x19,
14
- 0x12,
15
- 0,
16
- 0,
17
- 2,
18
- 0,
19
- 1,
20
- 0,
21
- 4,
22
- 0,
23
- 0,
24
- 0,
13
+ 0x19, 0x12, 0, 0, 2, 0, 1, 0, 4, 0, 0, 0,
25
14
  ]);
26
15
  expect(Array.from(result.data.slice(12))).toEqual([0x00, 0xf0, 0xc0, 0x07]);
27
16
  });
@@ -40,11 +29,11 @@ describe('encodePro2Wallpaper', () => {
40
29
  });
41
30
 
42
31
  test('rejects invalid dimensions and RGBA byte length', () => {
43
- expect(() =>
44
- encodePro2Wallpaper({ width: 0, height: 1, rgba: new Uint8Array() })
45
- ).toThrow('width');
46
- expect(() =>
47
- encodePro2Wallpaper({ width: 2, height: 1, rgba: new Uint8Array(4) })
48
- ).toThrow('8');
32
+ expect(() => encodePro2Wallpaper({ width: 0, height: 1, rgba: new Uint8Array() })).toThrow(
33
+ 'width'
34
+ );
35
+ expect(() => encodePro2Wallpaper({ width: 2, height: 1, rgba: new Uint8Array(4) })).toThrow(
36
+ '8'
37
+ );
49
38
  });
50
39
  });
@@ -0,0 +1,45 @@
1
+ import * as publicMethods from '../src/api';
2
+ import {
3
+ PROTOCOL_V2_RETRY_ON_LOCKED_METHODS,
4
+ getProtocolV2UnlockPolicy,
5
+ } from '../src/protocols/protocol-v2/unlockPolicy';
6
+
7
+ jest.mock('../src/data/config', () => ({
8
+ DEFAULT_DOMAIN: 'https://example.com/',
9
+ getSDKVersion: () => '0.0.0-test',
10
+ }));
11
+
12
+ describe('Protocol V2 public method unlock policy', () => {
13
+ test('classifies the complete public method table with an explicit replay allowlist', () => {
14
+ const publicMethodNames = Object.keys(publicMethods);
15
+ const retryMethodNames = new Set<string>(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS);
16
+
17
+ expect(publicMethodNames.length).toBeGreaterThan(100);
18
+ expect(new Set(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS).size).toBe(
19
+ PROTOCOL_V2_RETRY_ON_LOCKED_METHODS.length
20
+ );
21
+
22
+ for (const methodName of PROTOCOL_V2_RETRY_ON_LOCKED_METHODS) {
23
+ expect(publicMethods).toHaveProperty(methodName);
24
+ }
25
+
26
+ for (const methodName of publicMethodNames) {
27
+ expect(getProtocolV2UnlockPolicy(methodName)).toBe(
28
+ retryMethodNames.has(methodName) ? 'retry-on-locked' : 'none'
29
+ );
30
+ }
31
+ });
32
+
33
+ test.each([
34
+ 'deviceUnlock',
35
+ 'deviceLock',
36
+ 'deviceWipe',
37
+ 'deviceChangePin',
38
+ 'deviceSettings',
39
+ 'firmwareUpdateV4',
40
+ 'deviceUploadWallpaper',
41
+ 'uploadPortfolio',
42
+ ])('does not make the state-changing method %s replayable by default', methodName => {
43
+ expect(getProtocolV2UnlockPolicy(methodName)).toBe('none');
44
+ });
45
+ });