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

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 (192) hide show
  1. package/__tests__/DeviceCommands.test.ts +85 -0
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +2 -2
  5. package/__tests__/device-wallet-session-store.test.ts +152 -7
  6. package/__tests__/firmware-update/check-all-firmware-release.test.ts +86 -0
  7. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  8. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
  12. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  13. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  14. package/__tests__/logBlockEvent.test.ts +17 -0
  15. package/__tests__/open-wallet-session.test.ts +311 -12
  16. package/__tests__/pro2Wallpaper.test.ts +7 -18
  17. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  18. package/__tests__/protocol-v2.test.ts +685 -177
  19. package/__tests__/public-device-state-api.test.ts +69 -62
  20. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  21. package/dist/api/BaseMethod.d.ts +2 -1
  22. package/dist/api/BaseMethod.d.ts.map +1 -1
  23. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  24. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  27. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  29. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  30. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  31. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  32. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  33. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  34. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  35. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  36. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  37. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  38. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  39. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  40. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  41. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  42. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  43. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  44. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  45. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  46. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  47. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  48. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  49. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  50. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  52. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  53. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  54. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  55. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +23 -0
  56. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  57. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +30 -0
  58. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  59. package/dist/api/firmware/getBinary.d.ts +7 -0
  60. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  61. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  62. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  63. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  64. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  65. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  66. package/dist/api/index.d.ts +0 -17
  67. package/dist/api/index.d.ts.map +1 -1
  68. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  69. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  70. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  71. package/dist/core/index.d.ts.map +1 -1
  72. package/dist/data-manager/DataManager.d.ts +1 -0
  73. package/dist/data-manager/DataManager.d.ts.map +1 -1
  74. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  75. package/dist/device/Device.d.ts +3 -2
  76. package/dist/device/Device.d.ts.map +1 -1
  77. package/dist/device/DeviceCommands.d.ts.map +1 -1
  78. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  79. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  80. package/dist/events/device.d.ts +1 -0
  81. package/dist/events/device.d.ts.map +1 -1
  82. package/dist/events/logBlockEvent.d.ts.map +1 -1
  83. package/dist/events/ui-request.d.ts +1 -0
  84. package/dist/events/ui-request.d.ts.map +1 -1
  85. package/dist/index.d.ts +273 -230
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3060 -1676
  88. package/dist/inject.d.ts.map +1 -1
  89. package/dist/lowLevelInject.d.ts.map +1 -1
  90. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  91. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  92. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  93. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  94. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  95. package/dist/topLevelInject.d.ts.map +1 -1
  96. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  97. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  98. package/dist/types/api/deviceUnlock.d.ts +2 -2
  99. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  100. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  101. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  102. package/dist/types/api/export.d.ts +4 -1
  103. package/dist/types/api/export.d.ts.map +1 -1
  104. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  105. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  106. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  107. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  108. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  109. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  110. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  111. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  112. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  113. package/dist/types/api/index.d.ts +10 -19
  114. package/dist/types/api/index.d.ts.map +1 -1
  115. package/dist/types/api/openWalletSession.d.ts +1 -0
  116. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  117. package/dist/types/api/protocolV2.d.ts +3 -76
  118. package/dist/types/api/protocolV2.d.ts.map +1 -1
  119. package/dist/types/api/sessionCache.d.ts +4 -1
  120. package/dist/types/api/sessionCache.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +13 -0
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  124. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  125. package/dist/utils/patch.d.ts +1 -1
  126. package/dist/utils/patch.d.ts.map +1 -1
  127. package/package.json +4 -4
  128. package/src/api/BaseMethod.ts +8 -3
  129. package/src/api/CheckAllFirmwareRelease.ts +23 -3
  130. package/src/api/ClearSessionCache.ts +4 -0
  131. package/src/api/FirmwareUpdate.ts +8 -1
  132. package/src/api/FirmwareUpdateV2.ts +292 -120
  133. package/src/api/FirmwareUpdateV3.ts +256 -55
  134. package/src/api/FirmwareUpdateV4.ts +821 -331
  135. package/src/api/GetPassphraseState.ts +11 -5
  136. package/src/api/OpenWalletSession.ts +74 -25
  137. package/src/api/UploadPortfolio.ts +1 -1
  138. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  139. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  140. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  141. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  142. package/src/api/device/DeviceChangePin.ts +1 -1
  143. package/src/api/device/DeviceUnlock.ts +1 -2
  144. package/src/api/device/DeviceUpdateBootloader.ts +122 -6
  145. package/src/api/device/DeviceWipe.ts +3 -1
  146. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  147. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  148. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  149. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  150. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  151. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +392 -0
  152. package/src/api/firmware/updateBootloader.ts +19 -4
  153. package/src/api/firmware/uploadFirmware.ts +140 -22
  154. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  155. package/src/api/index.ts +0 -18
  156. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  157. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  158. package/src/core/index.ts +1 -0
  159. package/src/data/messages/messages-protocol-v2.json +35 -1
  160. package/src/data-manager/DataManager.ts +33 -33
  161. package/src/data-manager/connectSettings.ts +11 -0
  162. package/src/device/Device.ts +80 -67
  163. package/src/device/DeviceCommands.ts +76 -159
  164. package/src/device/DeviceStateMapper.ts +18 -1
  165. package/src/device/DeviceStateProjector.ts +7 -1
  166. package/src/events/device.ts +1 -0
  167. package/src/events/logBlockEvent.ts +1 -14
  168. package/src/events/ui-request.ts +1 -0
  169. package/src/index.ts +5 -0
  170. package/src/inject.ts +22 -25
  171. package/src/lowLevelInject.ts +5 -1
  172. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -3
  173. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  174. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  175. package/src/protocols/protocol-v2/walletSession.ts +86 -9
  176. package/src/topLevelInject.ts +5 -1
  177. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  178. package/src/types/api/deviceUnlock.ts +2 -5
  179. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  180. package/src/types/api/export.ts +18 -0
  181. package/src/types/api/firmwareUpdate.ts +76 -0
  182. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  183. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  184. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  185. package/src/types/api/getPassphraseState.ts +4 -3
  186. package/src/types/api/index.ts +14 -34
  187. package/src/types/api/openWalletSession.ts +2 -0
  188. package/src/types/api/protocolV2.ts +3 -156
  189. package/src/types/api/sessionCache.ts +9 -4
  190. package/src/types/settings.ts +13 -0
  191. package/src/utils/deviceFeaturesUtils.ts +5 -1
  192. package/src/utils/deviceInfoUtils.ts +3 -16
@@ -1,4 +1,5 @@
1
1
  import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
3
 
3
4
  import GetPassphraseState from '../src/api/GetPassphraseState';
4
5
  import OpenWalletSession from '../src/api/OpenWalletSession';
@@ -28,6 +29,7 @@ const createDevice = ({
28
29
  commands: { typedCall, promptPassphrase },
29
30
  getDeviceState: jest.fn().mockResolvedValue({
30
31
  identity: { deviceId: 'device-1' },
32
+ status: { passphraseProtection },
31
33
  }),
32
34
  getCurrentDeviceId: () => 'device-1',
33
35
  getCurrentPassphraseProtection: () => passphraseProtection,
@@ -58,15 +60,42 @@ describe('openWalletSession', () => {
58
60
  deviceWalletSessionStore.clear();
59
61
  });
60
62
 
61
- test('keeps getPassphraseState unsupported on Protocol V2', async () => {
63
+ test('keeps the legacy getPassphraseState App flow working on Protocol V2', async () => {
64
+ const typedCall = jest
65
+ .fn()
66
+ .mockResolvedValueOnce({ message: { eventless_wallet_session: true } })
67
+ .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
68
+ .mockResolvedValueOnce({
69
+ message: {
70
+ btc_test_address: 'pro2-wallet-state',
71
+ session_id: 'pro2-wallet-session',
72
+ },
73
+ });
74
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'legacy app secret' });
62
75
  const method = new GetPassphraseState({
63
- payload: { method: 'getPassphraseState', connectId: 'connect-id' },
76
+ payload: {
77
+ method: 'getPassphraseState',
78
+ connectId: 'connect-id',
79
+ initSession: true,
80
+ useEmptyPassphrase: false,
81
+ },
64
82
  });
65
- method.device = createDevice() as any;
83
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
66
84
 
67
- await expect(method.run()).rejects.toMatchObject({
68
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
69
- });
85
+ await expect(method.run()).resolves.toBe('pro2-wallet-state');
86
+ expect(promptPassphrase).toHaveBeenCalledWith(
87
+ expect.objectContaining({ deviceOnly: true }),
88
+ expect.anything()
89
+ );
90
+ expect(typedCall).toHaveBeenNthCalledWith(
91
+ 2,
92
+ 'DeviceSessionAskPassphrase',
93
+ 'Success',
94
+ {},
95
+ { timeoutMs: 120_000 }
96
+ );
97
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
98
+ expect(JSON.stringify(typedCall.mock.calls)).not.toContain('legacy app secret');
70
99
  });
71
100
 
72
101
  test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => {
@@ -145,6 +174,7 @@ describe('openWalletSession', () => {
145
174
  walletType: 'standard',
146
175
  deviceId: 'device-1',
147
176
  passphraseState: null,
177
+ sessionId: 'main-wallet-session',
148
178
  resumed: false,
149
179
  });
150
180
  expect(method.payload.useEmptyPassphrase).toBe(true);
@@ -173,6 +203,57 @@ describe('openWalletSession', () => {
173
203
  walletType: 'hidden',
174
204
  passphraseState: 'hidden-state',
175
205
  });
206
+ expect(promptPassphrase).toHaveBeenCalledWith(
207
+ expect.objectContaining({ deviceOnly: true }),
208
+ expect.anything()
209
+ );
210
+ expect(typedCall).toHaveBeenNthCalledWith(
211
+ 2,
212
+ 'DeviceSessionAskPassphrase',
213
+ 'Success',
214
+ {},
215
+ { timeoutMs: 120_000 }
216
+ );
217
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
218
+ expect(JSON.stringify(typedCall.mock.calls)).not.toContain('hidden secret');
219
+ });
220
+
221
+ test('classifies the selected Pro2 wallet from the post-unlock device state', async () => {
222
+ const typedCall = jest
223
+ .fn()
224
+ .mockResolvedValueOnce({ message: { version: 2 } })
225
+ .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
226
+ .mockResolvedValueOnce({
227
+ message: {
228
+ btc_test_address: 'hidden-state-after-unlock',
229
+ session_id: 'hidden-session-after-unlock',
230
+ },
231
+ });
232
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden secret' });
233
+ const method = new OpenWalletSession({
234
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
235
+ });
236
+ method.init();
237
+ const device = createDevice({ passphraseProtection: false, typedCall, promptPassphrase });
238
+ device.features.unlocked = false;
239
+ device.getCurrentPassphraseProtection = () => device.features.passphraseProtection;
240
+ device.unlockDevice = jest.fn().mockImplementation(() => {
241
+ device.features.unlocked = true;
242
+ device.features.passphraseProtection = true;
243
+ return Promise.resolve(device.features);
244
+ });
245
+ method.device = device as any;
246
+
247
+ await expect(method.run()).resolves.toEqual({
248
+ protocol: 'V2',
249
+ walletType: 'hidden',
250
+ deviceId: 'device-1',
251
+ passphraseState: 'hidden-state-after-unlock',
252
+ sessionId: 'hidden-session-after-unlock',
253
+ resumed: false,
254
+ });
255
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
256
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
176
257
  });
177
258
 
178
259
  test('uses legacy initSession to replace a hidden-wallet session on Protocol V2', async () => {
@@ -201,10 +282,22 @@ describe('openWalletSession', () => {
201
282
  walletType: 'hidden',
202
283
  deviceId: 'device-1',
203
284
  passphraseState: 'new-hidden-state',
285
+ sessionId: 'new-hidden-session',
204
286
  resumed: false,
205
287
  });
288
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
289
+ eventless_wallet_session: true,
290
+ });
291
+ expect(typedCall).toHaveBeenCalledWith(
292
+ 'DeviceSessionAskPassphrase',
293
+ 'Success',
294
+ {},
295
+ {
296
+ timeoutMs: 120_000,
297
+ }
298
+ );
206
299
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
207
- expect(promptPassphrase).not.toHaveBeenCalled();
300
+ expect(promptPassphrase).toHaveBeenCalled();
208
301
  expect(deviceWalletSessionStore.get('device-1', 'stale-hidden-state')).toBeUndefined();
209
302
  expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
210
303
  });
@@ -266,6 +359,7 @@ describe('openWalletSession', () => {
266
359
  walletType: 'hidden',
267
360
  deviceId: 'device-1',
268
361
  passphraseState: 'hidden-state',
362
+ sessionId: 'renewed-session',
269
363
  resumed: true,
270
364
  });
271
365
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
@@ -378,7 +472,7 @@ describe('openWalletSession', () => {
378
472
  });
379
473
 
380
474
  test('opens the standard wallet without selecting a hidden wallet', async () => {
381
- const typedCall = jest.fn();
475
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
382
476
  const promptPassphrase = jest.fn();
383
477
  const method = new OpenWalletSession({
384
478
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
@@ -395,17 +489,61 @@ describe('openWalletSession', () => {
395
489
  passphraseState: null,
396
490
  resumed: false,
397
491
  });
398
- expect(typedCall).not.toHaveBeenCalled();
492
+ expect(typedCall).toHaveBeenCalledTimes(1);
493
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
494
+ eventless_wallet_session: true,
495
+ });
399
496
  expect(promptPassphrase).not.toHaveBeenCalled();
400
497
  expect(device.passphraseState).toBeUndefined();
401
498
  });
402
499
 
500
+ test('opens a locked Protocol V2 standard wallet after deviceId becomes available', async () => {
501
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
502
+ const method = new OpenWalletSession({
503
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
504
+ });
505
+ method.init();
506
+ const device = createDevice({ typedCall });
507
+ device.features.unlocked = false;
508
+ device.getDeviceState = jest
509
+ .fn()
510
+ .mockResolvedValueOnce({
511
+ identity: { deviceId: undefined },
512
+ status: { passphraseProtection: true },
513
+ })
514
+ .mockResolvedValueOnce({
515
+ identity: { deviceId: 'device-1' },
516
+ status: { passphraseProtection: true },
517
+ });
518
+ device.unlockDevice = jest.fn().mockImplementation(() => {
519
+ device.features.unlocked = true;
520
+ return Promise.resolve(device.features);
521
+ });
522
+ method.device = device as any;
523
+
524
+ await expect(method.run()).resolves.toEqual({
525
+ protocol: 'V2',
526
+ walletType: 'standard',
527
+ deviceId: 'device-1',
528
+ passphraseState: null,
529
+ resumed: false,
530
+ });
531
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
532
+ expect(device.getDeviceState).toHaveBeenCalledTimes(2);
533
+ });
534
+
403
535
  test('rejects a standard-wallet request when Protocol V2 is unlocked by Attach PIN', async () => {
536
+ const typedCall = jest.fn().mockResolvedValue({
537
+ message: {
538
+ btc_test_address: 'attach-wallet-state',
539
+ session_id: 'attach-wallet-session',
540
+ },
541
+ });
404
542
  const method = new OpenWalletSession({
405
543
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
406
544
  });
407
545
  method.init();
408
- const device = createDevice();
546
+ const device = createDevice({ typedCall });
409
547
  device.features.unlockedAttachPin = true;
410
548
  method.device = device as any;
411
549
 
@@ -416,7 +554,7 @@ describe('openWalletSession', () => {
416
554
  expect(device.clearInternalState).toHaveBeenCalled();
417
555
  });
418
556
 
419
- test('selects a hidden wallet and keeps the session inside the SDK store', async () => {
557
+ test('selects a hidden wallet and returns the CLI compatibility session', async () => {
420
558
  const typedCall = jest.fn().mockResolvedValue({
421
559
  message: {
422
560
  btc_test_address: 'hidden-state',
@@ -437,14 +575,74 @@ describe('openWalletSession', () => {
437
575
  walletType: 'hidden',
438
576
  deviceId: 'device-1',
439
577
  passphraseState: 'hidden-state',
578
+ sessionId: 'hidden-session',
440
579
  resumed: false,
441
580
  });
581
+ expect(typedCall).toHaveBeenCalledWith(
582
+ 'DeviceSessionAskPassphrase',
583
+ 'Success',
584
+ {},
585
+ {
586
+ timeoutMs: 120_000,
587
+ }
588
+ );
442
589
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
443
- expect(promptPassphrase).not.toHaveBeenCalled();
590
+ expect(promptPassphrase).toHaveBeenCalled();
444
591
  expect(device.passphraseState).toBeUndefined();
445
592
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
446
593
  });
447
594
 
595
+ test('selects on-device passphrase entry before getting the prepared session', async () => {
596
+ const typedCall = jest.fn().mockResolvedValue({
597
+ message: { btc_test_address: 'device-state', session_id: 'device-session' },
598
+ });
599
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
600
+ const method = new OpenWalletSession({
601
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
602
+ });
603
+ method.init();
604
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
605
+
606
+ await expect(method.run()).resolves.toMatchObject({
607
+ walletType: 'hidden',
608
+ passphraseState: 'device-state',
609
+ });
610
+ expect(typedCall).toHaveBeenCalledWith(
611
+ 'DeviceSessionAskPassphrase',
612
+ 'Success',
613
+ {},
614
+ {
615
+ timeoutMs: 120_000,
616
+ }
617
+ );
618
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
619
+ });
620
+
621
+ test('selects Attach PIN only when the device reports an existing binding', async () => {
622
+ const typedCall = jest.fn().mockResolvedValue({
623
+ message: { btc_test_address: 'attach-state', session_id: 'attach-session' },
624
+ });
625
+ const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
626
+ const method = new OpenWalletSession({
627
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
628
+ });
629
+ method.init();
630
+ const device = createDevice({ typedCall, promptPassphrase });
631
+ device.features.attachToPinEnabled = true;
632
+ method.device = device as any;
633
+
634
+ await expect(method.run()).resolves.toMatchObject({
635
+ walletType: 'hidden',
636
+ passphraseState: 'attach-state',
637
+ });
638
+ expect(promptPassphrase).toHaveBeenCalledWith(
639
+ expect.objectContaining({ existsAttachPinUser: true, deviceOnly: true }),
640
+ { cancelDeviceOnReject: false }
641
+ );
642
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
643
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
644
+ });
645
+
448
646
  test.each([
449
647
  {
450
648
  missingField: 'session_id',
@@ -501,6 +699,7 @@ describe('openWalletSession', () => {
501
699
  walletType: 'hidden',
502
700
  deviceId: 'device-1',
503
701
  passphraseState: 'hidden-state',
702
+ sessionId: 'renewed-session',
504
703
  resumed: true,
505
704
  });
506
705
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
@@ -509,6 +708,106 @@ describe('openWalletSession', () => {
509
708
  expect(promptPassphrase).not.toHaveBeenCalled();
510
709
  });
511
710
 
711
+ test('validates a locked Protocol V2 resume binding after deviceId refresh', async () => {
712
+ const typedCall = jest.fn().mockResolvedValue({
713
+ message: {
714
+ btc_test_address: 'hidden-state',
715
+ session_id: 'renewed-session',
716
+ },
717
+ });
718
+ const method = new OpenWalletSession({
719
+ payload: {
720
+ method: 'openWalletSession',
721
+ connectId: 'connect-id',
722
+ mode: 'resume-hidden',
723
+ deviceId: 'device-1',
724
+ passphraseState: 'hidden-state',
725
+ },
726
+ });
727
+ method.init();
728
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
729
+ const device = createDevice({ typedCall });
730
+ device.features.unlocked = false;
731
+ device.getDeviceState = jest
732
+ .fn()
733
+ .mockResolvedValueOnce({
734
+ identity: { deviceId: undefined },
735
+ status: { passphraseProtection: true },
736
+ })
737
+ .mockResolvedValueOnce({
738
+ identity: { deviceId: 'device-1' },
739
+ status: { passphraseProtection: true },
740
+ });
741
+ device.unlockDevice = jest.fn().mockImplementation(() => {
742
+ device.features.unlocked = true;
743
+ return Promise.resolve(device.features);
744
+ });
745
+ method.device = device as any;
746
+
747
+ await expect(method.run()).resolves.toMatchObject({
748
+ protocol: 'V2',
749
+ walletType: 'hidden',
750
+ deviceId: 'device-1',
751
+ passphraseState: 'hidden-state',
752
+ resumed: true,
753
+ });
754
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
755
+ expect(device.getDeviceState).toHaveBeenCalledTimes(2);
756
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
757
+ session_id: 'known-session',
758
+ });
759
+ const sessionGetCall = typedCall.mock.calls.findIndex(
760
+ ([requestName]) => requestName === 'DeviceSessionGet'
761
+ );
762
+ expect(device.getDeviceState.mock.invocationCallOrder[1]).toBeLessThan(
763
+ typedCall.mock.invocationCallOrder[sessionGetCall]
764
+ );
765
+ });
766
+
767
+ test('rejects a Protocol V2 resume when the refreshed deviceId does not match', async () => {
768
+ const typedCall = jest.fn().mockResolvedValue({
769
+ message: {
770
+ btc_test_address: 'hidden-state',
771
+ session_id: 'renewed-session',
772
+ },
773
+ });
774
+ const method = new OpenWalletSession({
775
+ payload: {
776
+ method: 'openWalletSession',
777
+ connectId: 'connect-id',
778
+ mode: 'resume-hidden',
779
+ deviceId: 'device-1',
780
+ passphraseState: 'hidden-state',
781
+ },
782
+ });
783
+ method.init();
784
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
785
+ const device = createDevice({ typedCall });
786
+ device.features.unlocked = false;
787
+ device.getDeviceState = jest
788
+ .fn()
789
+ .mockResolvedValueOnce({
790
+ identity: { deviceId: undefined },
791
+ status: { passphraseProtection: true },
792
+ })
793
+ .mockResolvedValueOnce({
794
+ identity: { deviceId: 'other-device' },
795
+ status: { passphraseProtection: true },
796
+ });
797
+ device.unlockDevice = jest.fn().mockImplementation(() => {
798
+ device.features.unlocked = true;
799
+ return Promise.resolve(device.features);
800
+ });
801
+ method.device = device as any;
802
+
803
+ await expect(method.run()).rejects.toMatchObject({
804
+ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError,
805
+ });
806
+ expect(typedCall).not.toHaveBeenCalled();
807
+ expect(device.clearInternalState).not.toHaveBeenCalled();
808
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
809
+ });
810
+
512
811
  test('does not fall back to wallet selection when a cached resume is invalid', async () => {
513
812
  const typedCall = jest.fn().mockRejectedValue(
514
813
  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
+ });