@onekeyfe/hd-core 1.2.0-alpha.41 → 1.2.0-alpha.42

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 (200) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +78 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +14 -1
  4. package/__tests__/connectSettings.test.ts +5 -47
  5. package/__tests__/deviceUploadNft.test.ts +187 -0
  6. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +31 -59
  7. package/__tests__/get-device-state.test.ts +132 -12
  8. package/__tests__/homescreen.test.ts +41 -1
  9. package/__tests__/logBlockEvent.test.ts +1 -1
  10. package/__tests__/open-wallet-session.test.ts +259 -18
  11. package/__tests__/pro2Nft.test.ts +108 -0
  12. package/__tests__/protocol-v2-resources.test.ts +204 -0
  13. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  14. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  15. package/__tests__/protocol-v2.test.ts +526 -955
  16. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  17. package/__tests__/uiProgressThrottle.test.ts +74 -0
  18. package/dist/api/BaseMethod.d.ts +3 -1
  19. package/dist/api/BaseMethod.d.ts.map +1 -1
  20. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  22. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +6 -25
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  28. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  29. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  30. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -8
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  34. package/dist/api/firmware/getBinary.d.ts +0 -7
  35. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  36. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  37. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  38. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  39. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  40. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  41. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  42. package/dist/api/index.d.ts +1 -0
  43. package/dist/api/index.d.ts.map +1 -1
  44. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  45. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  46. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/core/deviceEventRegistration.d.ts +2 -0
  49. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  50. package/dist/core/index.d.ts.map +1 -1
  51. package/dist/data-manager/DataManager.d.ts +3 -2
  52. package/dist/data-manager/DataManager.d.ts.map +1 -1
  53. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  54. package/dist/device/Device.d.ts +22 -4
  55. package/dist/device/Device.d.ts.map +1 -1
  56. package/dist/events/device.d.ts +4 -0
  57. package/dist/events/device.d.ts.map +1 -1
  58. package/dist/events/logBlockEvent.d.ts.map +1 -1
  59. package/dist/events/ui-request.d.ts +27 -2
  60. package/dist/events/ui-request.d.ts.map +1 -1
  61. package/dist/index.d.ts +110 -182
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1919 -3336
  64. package/dist/inject.d.ts.map +1 -1
  65. package/dist/lowLevelInject.d.ts.map +1 -1
  66. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  67. package/dist/protocols/protocol-v2/resources.d.ts +30 -0
  68. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
  69. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  70. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  71. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  72. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  74. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  75. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  76. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  77. package/dist/topLevelInject.d.ts.map +1 -1
  78. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -4
  79. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  80. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  81. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  82. package/dist/types/api/export.d.ts +1 -4
  83. package/dist/types/api/export.d.ts.map +1 -1
  84. package/dist/types/api/firmwareUpdate.d.ts +0 -69
  85. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  86. package/dist/types/api/index.d.ts +3 -10
  87. package/dist/types/api/index.d.ts.map +1 -1
  88. package/dist/types/api/protocolV2.d.ts +3 -0
  89. package/dist/types/api/protocolV2.d.ts.map +1 -1
  90. package/dist/types/settings.d.ts +12 -13
  91. package/dist/types/settings.d.ts.map +1 -1
  92. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  93. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  94. package/dist/utils/homescreen.d.ts +4 -0
  95. package/dist/utils/homescreen.d.ts.map +1 -1
  96. package/dist/utils/index.d.ts +1 -1
  97. package/dist/utils/index.d.ts.map +1 -1
  98. package/dist/utils/patch.d.ts +1 -1
  99. package/dist/utils/patch.d.ts.map +1 -1
  100. package/dist/utils/pro2Nft.d.ts +29 -0
  101. package/dist/utils/pro2Nft.d.ts.map +1 -0
  102. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  103. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  104. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  105. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  106. package/package.json +4 -4
  107. package/src/api/BaseMethod.ts +8 -10
  108. package/src/api/CheckAllFirmwareRelease.ts +40 -53
  109. package/src/api/FirmwareUpdateV2.ts +120 -298
  110. package/src/api/FirmwareUpdateV3.ts +57 -263
  111. package/src/api/FirmwareUpdateV4.ts +307 -912
  112. package/src/api/GetPassphraseState.ts +7 -1
  113. package/src/api/OpenWalletSession.ts +54 -17
  114. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  115. package/src/api/device/DeviceUnlock.ts +1 -1
  116. package/src/api/device/DeviceUpdateBootloader.ts +6 -122
  117. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -57
  118. package/src/api/firmware/updateBootloader.ts +4 -19
  119. package/src/api/firmware/uploadFirmware.ts +22 -140
  120. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  121. package/src/api/index.ts +1 -0
  122. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  123. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  124. package/src/core/deviceEventRegistration.ts +4 -0
  125. package/src/core/index.ts +118 -75
  126. package/src/data/messages/messages-protocol-v2.json +53 -0
  127. package/src/data-manager/DataManager.ts +63 -38
  128. package/src/data-manager/connectSettings.ts +0 -11
  129. package/src/device/Device.ts +193 -21
  130. package/src/events/device.ts +4 -0
  131. package/src/events/logBlockEvent.ts +1 -0
  132. package/src/events/ui-request.ts +32 -2
  133. package/src/index.ts +0 -5
  134. package/src/inject.ts +3 -22
  135. package/src/lowLevelInject.ts +1 -5
  136. package/src/protocols/protocol-v2/features.ts +9 -2
  137. package/src/protocols/protocol-v2/resources.ts +233 -0
  138. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  139. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  140. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  141. package/src/protocols/protocol-v2/walletSession.ts +93 -18
  142. package/src/topLevelInject.ts +1 -5
  143. package/src/types/api/checkAllFirmwareRelease.ts +1 -4
  144. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  145. package/src/types/api/export.ts +0 -18
  146. package/src/types/api/firmwareUpdate.ts +0 -76
  147. package/src/types/api/index.ts +2 -14
  148. package/src/types/api/protocolV2.ts +13 -0
  149. package/src/types/settings.ts +26 -13
  150. package/src/utils/deviceFeaturesUtils.ts +4 -0
  151. package/src/utils/homescreen.ts +32 -6
  152. package/src/utils/index.ts +6 -1
  153. package/src/utils/pro2Nft.ts +91 -0
  154. package/src/utils/pro2Wallpaper.ts +35 -8
  155. package/src/utils/uiProgressThrottle.ts +63 -0
  156. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -175
  157. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  158. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  159. package/__tests__/firmware-update/firmware-preparation-error.test.ts +0 -27
  160. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  161. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -593
  162. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -213
  163. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +0 -58
  164. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  165. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  166. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  167. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  168. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  169. package/dist/api/firmware/FirmwarePreparationError.d.ts +0 -3
  170. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +0 -1
  171. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  172. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  173. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -25
  174. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  175. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -30
  176. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  177. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  178. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  179. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  180. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  181. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  182. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  183. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  184. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  185. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -28
  186. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  187. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  188. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  189. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  190. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  191. package/src/api/firmware/FirmwarePreparationError.ts +0 -12
  192. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  193. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -745
  194. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -392
  195. package/src/api/firmware/progressThrottle.ts +0 -44
  196. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  197. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
  198. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  199. package/src/types/api/firmwareUpdatePlan.ts +0 -38
  200. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -1,4 +1,4 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import { DeviceType } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import { Device } from '../src/device/Device';
@@ -43,6 +43,70 @@ const getProtocolV2LoaderInfo = (mode: 'bootloader' | 'romloader') => ({
43
43
  });
44
44
 
45
45
  describe('getDeviceState', () => {
46
+ test('coalesces concurrent Protocol V2 runtime-context negotiation', async () => {
47
+ let resolveProtocolInfo:
48
+ | ((value: { message: typeof protocolV2ApplicationInfo }) => void)
49
+ | undefined;
50
+ const typedCall = jest.fn(
51
+ () =>
52
+ new Promise<{ message: typeof protocolV2ApplicationInfo }>(resolve => {
53
+ resolveProtocolInfo = resolve;
54
+ })
55
+ );
56
+ const device = createV2Device(typedCall);
57
+
58
+ const first = device.ensureProtocolV2RuntimeContext();
59
+ const second = device.ensureProtocolV2RuntimeContext();
60
+ resolveProtocolInfo?.({ message: protocolV2ApplicationInfo });
61
+
62
+ await expect(Promise.all([first, second])).resolves.toEqual([
63
+ protocolV2ApplicationInfo,
64
+ protocolV2ApplicationInfo,
65
+ ]);
66
+ expect(typedCall).toHaveBeenCalledTimes(1);
67
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
68
+ eventless_wallet_session: true,
69
+ });
70
+ });
71
+
72
+ test('renegotiates Protocol V2 runtime context after transport disconnect', async () => {
73
+ const typedCall = jest.fn().mockResolvedValue({ message: protocolV2ApplicationInfo });
74
+ const device = createV2Device(typedCall);
75
+
76
+ await device.ensureProtocolV2RuntimeContext();
77
+ await device.ensureProtocolV2RuntimeContext();
78
+ device.markTransportDisconnected();
79
+ await device.ensureProtocolV2RuntimeContext();
80
+
81
+ expect(typedCall).toHaveBeenCalledTimes(2);
82
+ });
83
+
84
+ test('rejects every waiter when runtime-context negotiation is invalidated', async () => {
85
+ let resolveProtocolInfo:
86
+ | ((value: { message: typeof protocolV2ApplicationInfo }) => void)
87
+ | undefined;
88
+ const typedCall = jest.fn(
89
+ () =>
90
+ new Promise<{ message: typeof protocolV2ApplicationInfo }>(resolve => {
91
+ resolveProtocolInfo = resolve;
92
+ })
93
+ );
94
+ const device = createV2Device(typedCall);
95
+
96
+ const first = device.ensureProtocolV2RuntimeContext();
97
+ const second = device.ensureProtocolV2RuntimeContext();
98
+ const firstExpectation = expect(first).rejects.toMatchObject({
99
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
100
+ });
101
+ const secondExpectation = expect(second).rejects.toMatchObject({
102
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
103
+ });
104
+ device.markTransportDisconnected();
105
+ resolveProtocolInfo?.({ message: protocolV2ApplicationInfo });
106
+
107
+ await Promise.all([firstExpectation, secondExpectation]);
108
+ });
109
+
46
110
  test('does not expose the internal wallet session', async () => {
47
111
  const device = createV2Device(jest.fn());
48
112
  device.updateState({ protocol: 'V2' }, 'initialize');
@@ -159,15 +223,20 @@ describe('getDeviceState', () => {
159
223
  throw new Error(`Unexpected request: ${requestType}`);
160
224
  });
161
225
  const device = createV2Device(typedCall);
162
- device.updateState({ protocol: 'V2', status: { mode } }, 'initialize');
226
+ device.updateState(
227
+ {
228
+ protocol: 'V2',
229
+ identity: { deviceType: EDeviceType.Pro2 },
230
+ status: { mode },
231
+ raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo(mode) },
232
+ },
233
+ 'initialize'
234
+ );
163
235
 
164
236
  const state = await device.getDeviceState({ refreshSections: ['status'] });
165
237
 
166
238
  expect(state.status.mode).toBe(mode);
167
- expect(typedCall.mock.calls.map(call => call[0])).toEqual([
168
- 'DeviceInfoGet',
169
- 'ProtocolInfoRequest',
170
- ]);
239
+ expect(typedCall).not.toHaveBeenCalled();
171
240
  }
172
241
  );
173
242
 
@@ -187,7 +256,14 @@ describe('getDeviceState', () => {
187
256
  return { message: { init_states: true, unlocked: true, device_id: 'device-1' } };
188
257
  });
189
258
  const device = createV2Device(typedCall);
190
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
259
+ device.updateState(
260
+ {
261
+ protocol: 'V2',
262
+ status: { mode: 'normal' },
263
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
264
+ },
265
+ 'initialize'
266
+ );
191
267
 
192
268
  const state = await device.getDeviceState({ refreshSections: ['status'] });
193
269
 
@@ -205,7 +281,14 @@ describe('getDeviceState', () => {
205
281
  },
206
282
  });
207
283
  const device = createV2Device(typedCall);
208
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
284
+ device.updateState(
285
+ {
286
+ protocol: 'V2',
287
+ status: { mode: 'normal' },
288
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
289
+ },
290
+ 'initialize'
291
+ );
209
292
  const onState = jest.fn();
210
293
  device.on(DEVICE.STATE, onState);
211
294
 
@@ -250,13 +333,18 @@ describe('getDeviceState', () => {
250
333
  throw new Error(`Unexpected request: ${requestType}`);
251
334
  });
252
335
  const device = createV2Device(typedCall);
253
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
336
+ device.updateState(
337
+ {
338
+ protocol: 'V2',
339
+ status: { mode: 'normal' },
340
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
341
+ },
342
+ 'initialize'
343
+ );
254
344
 
255
345
  const state = await device.getDeviceState({ refreshSections: ['status', 'settings'] });
256
346
 
257
347
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
258
- 'DeviceInfoGet',
259
- 'ProtocolInfoRequest',
260
348
  'DeviceStatusGet',
261
349
  'DeviceSettingsGet',
262
350
  ]);
@@ -288,7 +376,11 @@ describe('getDeviceState', () => {
288
376
  });
289
377
  const device = createV2Device(typedCall);
290
378
  device.updateState(
291
- { protocol: 'V2', status: { mode: 'normal', unlocked: true } },
379
+ {
380
+ protocol: 'V2',
381
+ status: { mode: 'normal', unlocked: true },
382
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
383
+ },
292
384
  'initialize'
293
385
  );
294
386
 
@@ -374,6 +466,34 @@ describe('getDeviceState', () => {
374
466
  });
375
467
  });
376
468
 
469
+ test.each([
470
+ EDeviceType.Classic,
471
+ EDeviceType.Classic1s,
472
+ EDeviceType.ClassicPure,
473
+ EDeviceType.Mini,
474
+ ])('keeps the GetFeatures-only firmware path for %s', async deviceType => {
475
+ const typedCall = jest.fn().mockResolvedValue({
476
+ message: {
477
+ onekey_device_type: deviceType.toUpperCase(),
478
+ initialized: true,
479
+ onekey_firmware_version: '3.5.0',
480
+ se_ver: '1.1.0.2',
481
+ },
482
+ });
483
+ const device = createV1Device(typedCall);
484
+
485
+ const state = await device.getDeviceState({
486
+ refreshSections: ['identity', 'versions', 'verification'],
487
+ });
488
+
489
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual(['GetFeatures']);
490
+ expect(state.versions).toMatchObject({
491
+ firmware: '3.5.0',
492
+ se: '1.1.0.2',
493
+ se01: '1.1.0.2',
494
+ });
495
+ });
496
+
377
497
  test('does not clear Protocol V1 firmware details during a later runtime refresh', async () => {
378
498
  let getFeaturesCount = 0;
379
499
  const typedCall = jest.fn().mockImplementation((requestType: string) => {
@@ -1,6 +1,6 @@
1
1
  import { EDeviceType } from '@onekeyfe/hd-shared';
2
2
 
3
- import { getHomeScreenSize } from '../src/utils/homescreen';
3
+ import { getHomeScreenSize, getNftSize } from '../src/utils/homescreen';
4
4
 
5
5
  describe('getHomeScreenSize', () => {
6
6
  it('returns the Pro 2 wallpaper dimensions', () => {
@@ -21,4 +21,44 @@ describe('getHomeScreenSize', () => {
21
21
  })
22
22
  ).toBeUndefined();
23
23
  });
24
+
25
+ it('does not reuse the wallpaper homeScreenType for Pro 2 NFT dimensions', () => {
26
+ expect(
27
+ getHomeScreenSize({
28
+ deviceType: EDeviceType.Pro2,
29
+ homeScreenType: 'Nft',
30
+ thumbnail: false,
31
+ })
32
+ ).toEqual({ width: 604, height: 1024 });
33
+ expect(
34
+ getHomeScreenSize({
35
+ deviceType: EDeviceType.Pro2,
36
+ homeScreenType: 'Nft',
37
+ thumbnail: true,
38
+ })
39
+ ).toBeUndefined();
40
+ });
41
+ });
42
+
43
+ describe('getNftSize', () => {
44
+ it('returns the Pro 2 NFT image and thumbnail dimensions independently', () => {
45
+ expect(getNftSize({ deviceType: EDeviceType.Pro2 })).toEqual({ width: 540, height: 540 });
46
+ expect(getNftSize({ deviceType: EDeviceType.Pro2, thumbnail: true })).toEqual({
47
+ width: 263,
48
+ height: 263,
49
+ });
50
+ });
51
+
52
+ it('preserves the legacy NFT dimensions', () => {
53
+ expect(getNftSize({ deviceType: EDeviceType.Touch })).toEqual({ width: 480, height: 800 });
54
+ expect(getNftSize({ deviceType: EDeviceType.Touch, thumbnail: true })).toEqual({
55
+ width: 238,
56
+ height: 238,
57
+ });
58
+ expect(getNftSize({ deviceType: EDeviceType.Pro, thumbnail: true })).toEqual({
59
+ width: 226,
60
+ height: 226,
61
+ radius: 40,
62
+ });
63
+ });
24
64
  });
@@ -104,7 +104,7 @@ describe('getLogBlockLabel', () => {
104
104
  });
105
105
  });
106
106
 
107
- it.each(['deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
107
+ it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
108
108
  'keeps metadata and replaces binary payloads with their size for %s',
109
109
  method => {
110
110
  const request = { method, path: 'resource.bin', data: new Uint8Array(1024) };
@@ -5,6 +5,7 @@ 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'),
@@ -26,6 +27,7 @@ const createDevice = ({
26
27
  unlocked: true,
27
28
  passphraseProtection,
28
29
  attachToPinEnabled: false,
30
+ unlockedAttachPin: false,
29
31
  },
30
32
  commands: {
31
33
  typedCall: jest.fn((request: string, ...args: unknown[]) => {
@@ -43,10 +45,21 @@ const createDevice = ({
43
45
  }),
44
46
  promptPassphrase,
45
47
  },
46
- getDeviceState: jest.fn().mockResolvedValue({
47
- identity: { deviceId: 'device-1' },
48
- status: { passphraseProtection },
49
- }),
48
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
49
+ device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
50
+ eventless_wallet_session: true,
51
+ })
52
+ ),
53
+ getDeviceState: jest.fn(() =>
54
+ Promise.resolve({
55
+ identity: { deviceId: 'device-1' },
56
+ status: {
57
+ unlocked: device.features.unlocked,
58
+ passphraseProtection: device.features.passphraseProtection,
59
+ unlockedAttachPin: device.features.unlockedAttachPin,
60
+ },
61
+ })
62
+ ),
50
63
  getCurrentDeviceId: () => 'device-1',
51
64
  getCurrentPassphraseProtection: () => passphraseProtection,
52
65
  getInternalState: () =>
@@ -82,6 +95,50 @@ describe('openWalletSession', () => {
82
95
  deviceWalletSessionStore.clear();
83
96
  });
84
97
 
98
+ test('unlocks a locked Protocol V2 device before restoring a wallet session', async () => {
99
+ const device = {
100
+ commands: {
101
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
102
+ },
103
+ isBootloader: jest.fn().mockReturnValue(false),
104
+ isProtocolV2: jest.fn().mockReturnValue(true),
105
+ isRomloader: jest.fn().mockReturnValue(false),
106
+ state: { status: { unlocked: false } },
107
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
108
+ updateProtocolV2Status: jest.fn(function updateProtocolV2Status(
109
+ this: { state: { status: { unlocked: boolean } } },
110
+ status: { unlocked?: boolean }
111
+ ) {
112
+ this.state.status.unlocked = status.unlocked ?? this.state.status.unlocked;
113
+ }),
114
+ };
115
+
116
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(true);
117
+ expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
118
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
119
+ source: 'unlock-coordinator',
120
+ reason: 'device-locked',
121
+ deviceOnly: true,
122
+ });
123
+ });
124
+
125
+ test('does not request PIN when wallet-session recovery finds the device unlocked', async () => {
126
+ const device = {
127
+ commands: {
128
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
129
+ },
130
+ isBootloader: jest.fn().mockReturnValue(false),
131
+ isProtocolV2: jest.fn().mockReturnValue(true),
132
+ isRomloader: jest.fn().mockReturnValue(false),
133
+ state: { status: { unlocked: true } },
134
+ unlockDevice: jest.fn(),
135
+ updateProtocolV2Status: jest.fn(),
136
+ };
137
+
138
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(false);
139
+ expect(device.unlockDevice).not.toHaveBeenCalled();
140
+ });
141
+
85
142
  test.each([{ useEmptyPassphrase: true }, { initSession: true }, {}])(
86
143
  'requires the explicit mode in the new public API: %p',
87
144
  legacyParams => {
@@ -273,10 +330,24 @@ describe('openWalletSession', () => {
273
330
  typedCall,
274
331
  promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
275
332
  });
276
- device.getDeviceState = jest.fn().mockResolvedValue({
277
- identity: { deviceId: 'device-1' },
278
- status: { unlocked: false, passphraseProtection: true },
279
- });
333
+ device.getDeviceState = jest
334
+ .fn()
335
+ .mockResolvedValueOnce({
336
+ identity: { deviceId: 'device-1' },
337
+ status: {
338
+ unlocked: false,
339
+ passphraseProtection: null,
340
+ unlockedAttachPin: null,
341
+ },
342
+ })
343
+ .mockResolvedValue({
344
+ identity: { deviceId: 'device-1' },
345
+ status: {
346
+ unlocked: true,
347
+ passphraseProtection: true,
348
+ unlockedAttachPin: false,
349
+ },
350
+ });
280
351
  method.device = device as any;
281
352
 
282
353
  await expect(method.run()).resolves.toMatchObject({
@@ -330,6 +401,12 @@ describe('openWalletSession', () => {
330
401
  status.passphrase_enabled ?? device.features.passphraseProtection;
331
402
  return device.features;
332
403
  });
404
+ device.unlockDevice = jest.fn().mockImplementation(() => {
405
+ device.features.unlocked = true;
406
+ device.features.passphraseProtection = true;
407
+ device.features.unlockedAttachPin = false;
408
+ return Promise.resolve(device.features);
409
+ });
333
410
  method.device = device as any;
334
411
 
335
412
  await expect(method.run()).resolves.toEqual({
@@ -339,10 +416,98 @@ describe('openWalletSession', () => {
339
416
  passphraseState: 'hidden-state-after-unlock',
340
417
  resumed: false,
341
418
  });
342
- expect(device.unlockDevice).not.toHaveBeenCalled();
419
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
343
420
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
344
421
  });
345
422
 
423
+ test('unlocks before wallet selection when the Protocol V2 wallet status is unknown', async () => {
424
+ const typedCall = jest
425
+ .fn()
426
+ .mockResolvedValueOnce({ message: { version: 2 } })
427
+ .mockResolvedValueOnce({ message: {} })
428
+ .mockResolvedValueOnce({
429
+ message: {
430
+ btc_test_address: 'hidden-state-after-unknown-status',
431
+ session_id: 'hidden-session-after-unknown-status',
432
+ },
433
+ });
434
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
435
+ const method = new OpenWalletSession({
436
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
437
+ });
438
+ method.init();
439
+ const device = createDevice({ typedCall, promptPassphrase });
440
+ device.getDeviceState = jest
441
+ .fn()
442
+ .mockResolvedValueOnce({
443
+ identity: { deviceId: 'device-1' },
444
+ status: {
445
+ unlocked: undefined,
446
+ passphraseProtection: null,
447
+ unlockedAttachPin: null,
448
+ },
449
+ })
450
+ .mockResolvedValue({
451
+ identity: { deviceId: 'device-1' },
452
+ status: {
453
+ unlocked: true,
454
+ passphraseProtection: true,
455
+ unlockedAttachPin: false,
456
+ },
457
+ });
458
+ device.unlockDevice = jest.fn().mockImplementation(() => {
459
+ device.features.unlocked = true;
460
+ device.features.passphraseProtection = true;
461
+ device.features.unlockedAttachPin = false;
462
+ return Promise.resolve(device.features);
463
+ });
464
+ method.device = device as any;
465
+
466
+ await expect(method.run()).resolves.toMatchObject({
467
+ walletType: 'hidden',
468
+ passphraseState: 'hidden-state-after-unknown-status',
469
+ });
470
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
471
+ expect(device.unlockDevice.mock.invocationCallOrder[0]).toBeLessThan(
472
+ promptPassphrase.mock.invocationCallOrder[0]
473
+ );
474
+ });
475
+
476
+ test('rejects wallet selection when the post-unlock Protocol V2 status remains incomplete', async () => {
477
+ const typedCall = jest
478
+ .fn()
479
+ .mockResolvedValueOnce({ message: { version: 2 } })
480
+ .mockResolvedValueOnce({ message: {} })
481
+ .mockResolvedValueOnce({
482
+ message: {
483
+ btc_test_address: 'hidden-state-from-incomplete-status',
484
+ session_id: 'hidden-session-from-incomplete-status',
485
+ },
486
+ });
487
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
488
+ const method = new OpenWalletSession({
489
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
490
+ });
491
+ method.init();
492
+ const device = createDevice({ typedCall, promptPassphrase });
493
+ device.getDeviceState = jest.fn().mockResolvedValue({
494
+ identity: { deviceId: 'device-1' },
495
+ status: {
496
+ unlocked: true,
497
+ passphraseProtection: null,
498
+ unlockedAttachPin: null,
499
+ },
500
+ });
501
+ device.unlockDevice = jest.fn().mockResolvedValue(device.features);
502
+ method.device = device as any;
503
+
504
+ await expect(method.run()).rejects.toMatchObject({
505
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
506
+ });
507
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
508
+ expect(promptPassphrase).not.toHaveBeenCalled();
509
+ });
510
+
346
511
  test('rejects a Pro2 standard-wallet fallback after selecting a hidden wallet', async () => {
347
512
  const typedCall = jest
348
513
  .fn()
@@ -656,7 +821,11 @@ describe('openWalletSession', () => {
656
821
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
657
822
  eventless_wallet_session: true,
658
823
  });
659
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
824
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
825
+ source: 'wallet-session-coordinator',
826
+ reason: 'open-wallet',
827
+ deviceOnly: true,
828
+ });
660
829
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
661
830
  expect(promptPassphrase).not.toHaveBeenCalled();
662
831
  expect(device.passphraseState).toBeUndefined();
@@ -686,7 +855,11 @@ describe('openWalletSession', () => {
686
855
  })
687
856
  .mockResolvedValueOnce({
688
857
  identity: { deviceId: 'device-1' },
689
- status: { passphraseProtection: true },
858
+ status: {
859
+ unlocked: true,
860
+ passphraseProtection: true,
861
+ unlockedAttachPin: false,
862
+ },
690
863
  });
691
864
  device.unlockDevice = jest.fn().mockImplementation(() => {
692
865
  device.features.unlocked = true;
@@ -701,7 +874,11 @@ describe('openWalletSession', () => {
701
874
  passphraseState: null,
702
875
  resumed: false,
703
876
  });
704
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
877
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
878
+ source: 'wallet-session-coordinator',
879
+ reason: 'open-wallet',
880
+ deviceOnly: true,
881
+ });
705
882
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
706
883
  });
707
884
 
@@ -721,6 +898,24 @@ describe('openWalletSession', () => {
721
898
  method.init();
722
899
  const device = createDevice({ typedCall });
723
900
  device.features.unlockedAttachPin = true;
901
+ device.getDeviceState = jest
902
+ .fn()
903
+ .mockResolvedValueOnce({
904
+ identity: { deviceId: 'device-1' },
905
+ status: {
906
+ unlocked: true,
907
+ unlockedAttachPin: true,
908
+ passphraseProtection: true,
909
+ },
910
+ })
911
+ .mockResolvedValue({
912
+ identity: { deviceId: 'device-1' },
913
+ status: {
914
+ unlocked: true,
915
+ unlockedAttachPin: false,
916
+ passphraseProtection: true,
917
+ },
918
+ });
724
919
  device.unlockDevice = jest.fn().mockImplementation(() => {
725
920
  device.features.unlockedAttachPin = false;
726
921
  return Promise.resolve(device.features);
@@ -731,7 +926,11 @@ describe('openWalletSession', () => {
731
926
  walletType: 'standard',
732
927
  passphraseState: null,
733
928
  });
734
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
929
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
930
+ source: 'wallet-session-coordinator',
931
+ reason: 'open-wallet',
932
+ deviceOnly: true,
933
+ });
735
934
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
736
935
  });
737
936
 
@@ -939,7 +1138,9 @@ describe('openWalletSession', () => {
939
1138
  },
940
1139
  { cancelDeviceOnReject: false }
941
1140
  );
942
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
1141
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
1142
+ emitUiEvent: false,
1143
+ });
943
1144
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
944
1145
  });
945
1146
 
@@ -1215,7 +1416,11 @@ describe('openWalletSession', () => {
1215
1416
  })
1216
1417
  .mockResolvedValueOnce({
1217
1418
  identity: { deviceId: 'device-1' },
1218
- status: { passphraseProtection: true },
1419
+ status: {
1420
+ unlocked: true,
1421
+ passphraseProtection: true,
1422
+ unlockedAttachPin: false,
1423
+ },
1219
1424
  });
1220
1425
  device.unlockDevice = jest.fn().mockImplementation(() => {
1221
1426
  device.features.unlocked = true;
@@ -1271,7 +1476,11 @@ describe('openWalletSession', () => {
1271
1476
  })
1272
1477
  .mockResolvedValueOnce({
1273
1478
  identity: { deviceId: 'other-device' },
1274
- status: { passphraseProtection: true },
1479
+ status: {
1480
+ unlocked: true,
1481
+ passphraseProtection: true,
1482
+ unlockedAttachPin: false,
1483
+ },
1275
1484
  });
1276
1485
  device.unlockDevice = jest.fn().mockImplementation(() => {
1277
1486
  device.features.unlocked = true;
@@ -1335,7 +1544,6 @@ describe('openWalletSession', () => {
1335
1544
  const typedCall = jest
1336
1545
  .fn()
1337
1546
  .mockResolvedValueOnce({ message: { version: 2 } })
1338
- .mockResolvedValueOnce({ message: {} })
1339
1547
  .mockResolvedValueOnce({
1340
1548
  message: {
1341
1549
  btc_test_address: 'hidden-state',
@@ -1362,7 +1570,40 @@ describe('openWalletSession', () => {
1362
1570
  passphraseState: 'hidden-state',
1363
1571
  resumed: false,
1364
1572
  });
1365
- expect(promptPassphrase).toHaveBeenCalledTimes(1);
1573
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1574
+ expect(promptPassphrase).not.toHaveBeenCalled();
1366
1575
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
1367
1576
  });
1577
+
1578
+ test.each([false, true])('keeps deriveCardano API compatibility: %s', async deriveCardano => {
1579
+ const typedCall = jest
1580
+ .fn()
1581
+ .mockResolvedValueOnce({ message: { version: 2 } })
1582
+ .mockResolvedValueOnce({ message: {} })
1583
+ .mockResolvedValueOnce({
1584
+ message: {
1585
+ btc_test_address: 'hidden-state',
1586
+ session_id: 'new-session',
1587
+ },
1588
+ });
1589
+ const method = new OpenWalletSession({
1590
+ payload: {
1591
+ method: 'openWalletSession',
1592
+ connectId: 'connect-id',
1593
+ mode: 'select-hidden',
1594
+ deriveCardano,
1595
+ },
1596
+ });
1597
+ method.init();
1598
+ method.device = createDevice({
1599
+ typedCall,
1600
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
1601
+ }) as any;
1602
+
1603
+ await expect(method.run()).resolves.toMatchObject({
1604
+ walletType: 'hidden',
1605
+ passphraseState: 'hidden-state',
1606
+ });
1607
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1608
+ });
1368
1609
  });