@onekeyfe/hd-core 1.2.0-alpha.99 → 1.2.1-alpha.0

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 +20 -4
  2. package/__tests__/DeviceCommands.test.ts +203 -27
  3. package/__tests__/base64Data.test.ts +69 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +48 -2
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +82 -13
  6. package/__tests__/device-connector-protocol.test.ts +21 -0
  7. package/__tests__/device-lifecycle-events.test.ts +427 -7
  8. package/__tests__/device-pool-state.test.ts +25 -0
  9. package/__tests__/device-settings.test.ts +237 -9
  10. package/__tests__/device-state-events.test.ts +2 -2
  11. package/__tests__/device-state-mapper.test.ts +61 -5
  12. package/__tests__/device-utils.test.ts +15 -1
  13. package/__tests__/device-wallet-session-store.test.ts +147 -21
  14. package/__tests__/deviceSettings.test.ts +7 -3
  15. package/__tests__/deviceUploadNft.test.ts +36 -4
  16. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -3
  17. package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
  18. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
  19. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
  20. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +63 -0
  21. package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
  22. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  23. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  24. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +979 -0
  25. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  26. package/__tests__/get-device-state.test.ts +60 -21
  27. package/__tests__/logBlockEvent.test.ts +13 -1
  28. package/__tests__/open-wallet-session.test.ts +127 -1
  29. package/__tests__/protocol-v2-firmware-targets.test.ts +23 -0
  30. package/__tests__/protocol-v2-resources.test.ts +185 -88
  31. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  32. package/__tests__/protocol-v2.test.ts +2199 -486
  33. package/__tests__/refresh-device-state.test.ts +3 -1
  34. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  35. package/__tests__/search-devices.test.ts +79 -32
  36. package/__tests__/ton-sign-message.test.ts +84 -0
  37. package/dist/api/BaseMethod.d.ts +2 -1
  38. package/dist/api/BaseMethod.d.ts.map +1 -1
  39. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  40. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  41. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV4.d.ts +20 -6
  44. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  45. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  46. package/dist/api/SearchDevices.d.ts.map +1 -1
  47. package/dist/api/UploadPortfolio.d.ts +1 -1
  48. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  49. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  50. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  51. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  54. package/dist/api/device/DeviceVerify.d.ts +1 -0
  55. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  56. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  57. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  59. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  61. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  63. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  65. package/dist/api/firmware/getBinary.d.ts +3 -2
  66. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  67. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  68. package/dist/api/helpers/base64Data.d.ts +16 -0
  69. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  70. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  71. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  72. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  73. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  74. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  76. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  77. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  78. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  79. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  80. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  81. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  82. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  83. package/dist/core/index.d.ts +6 -1
  84. package/dist/core/index.d.ts.map +1 -1
  85. package/dist/data-manager/DataManager.d.ts +4 -3
  86. package/dist/data-manager/DataManager.d.ts.map +1 -1
  87. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  88. package/dist/device/Device.d.ts +16 -3
  89. package/dist/device/Device.d.ts.map +1 -1
  90. package/dist/device/DeviceCommands.d.ts +4 -4
  91. package/dist/device/DeviceCommands.d.ts.map +1 -1
  92. package/dist/device/DeviceConnector.d.ts +1 -1
  93. package/dist/device/DeviceConnector.d.ts.map +1 -1
  94. package/dist/device/DevicePool.d.ts.map +1 -1
  95. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  96. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  97. package/dist/events/logBlockEvent.d.ts.map +1 -1
  98. package/dist/events/ui-request.d.ts +3 -0
  99. package/dist/events/ui-request.d.ts.map +1 -1
  100. package/dist/index.d.ts +54 -84
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +2150 -1469
  103. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  104. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  105. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  106. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  107. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  108. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  109. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  110. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  111. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  112. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  113. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  114. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  115. package/dist/types/api/protocolV2.d.ts +1 -1
  116. package/dist/types/api/protocolV2.d.ts.map +1 -1
  117. package/dist/types/device.d.ts.map +1 -1
  118. package/dist/types/settings.d.ts +3 -31
  119. package/dist/types/settings.d.ts.map +1 -1
  120. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  121. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  122. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  123. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  124. package/dist/utils/deviceSettings.d.ts +1 -0
  125. package/dist/utils/deviceSettings.d.ts.map +1 -1
  126. package/dist/utils/index.d.ts +1 -1
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/patch.d.ts +1 -1
  129. package/dist/utils/patch.d.ts.map +1 -1
  130. package/dist/utils/pro2Nft.d.ts +7 -0
  131. package/dist/utils/pro2Nft.d.ts.map +1 -1
  132. package/package.json +6 -4
  133. package/src/api/BaseMethod.ts +8 -1
  134. package/src/api/CheckAllFirmwareRelease.ts +2 -4
  135. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  136. package/src/api/CheckBootloaderRelease.ts +3 -2
  137. package/src/api/CheckFirmwareRelease.ts +2 -2
  138. package/src/api/FirmwareUpdateV2.ts +12 -9
  139. package/src/api/FirmwareUpdateV3.ts +29 -19
  140. package/src/api/FirmwareUpdateV4.ts +934 -739
  141. package/src/api/OpenWalletSession.ts +39 -9
  142. package/src/api/PromptWebDeviceAccess.ts +2 -2
  143. package/src/api/SearchDevices.ts +6 -3
  144. package/src/api/UploadPortfolio.ts +9 -2
  145. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  146. package/src/api/device/DeviceChangePin.ts +4 -1
  147. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  148. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  149. package/src/api/device/DeviceSettings.ts +34 -20
  150. package/src/api/device/DeviceVerify.ts +25 -0
  151. package/src/api/device/DeviceWipe.ts +4 -1
  152. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  153. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  154. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  155. package/src/api/firmware/FirmwareUpdatePlan.ts +37 -77
  156. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +10 -6
  157. package/src/api/firmware/protocolV2Release.ts +7 -2
  158. package/src/api/helpers/base64Data.ts +85 -0
  159. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  160. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  161. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  162. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  163. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  164. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  165. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  166. package/src/api/protocol-v2/helpers.ts +8 -0
  167. package/src/api/ton/TonSignMessage.ts +5 -3
  168. package/src/core/index.ts +264 -55
  169. package/src/data/messages/messages-protocol-v2.json +140 -13
  170. package/src/data-manager/DataManager.ts +46 -39
  171. package/src/data-manager/TransportManager.ts +3 -1
  172. package/src/device/Device.ts +210 -44
  173. package/src/device/DeviceCommands.ts +27 -18
  174. package/src/device/DeviceConnector.ts +17 -3
  175. package/src/device/DevicePool.ts +9 -2
  176. package/src/device/DeviceStateMapper.ts +30 -24
  177. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  178. package/src/events/logBlockEvent.ts +14 -1
  179. package/src/events/ui-request.ts +3 -0
  180. package/src/index.ts +0 -10
  181. package/src/protocols/protocol-v2/features.ts +22 -5
  182. package/src/protocols/protocol-v2/index.ts +2 -0
  183. package/src/protocols/protocol-v2/resources.ts +110 -147
  184. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  185. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  186. package/src/types/api/firmwareUpdate.ts +1 -1
  187. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  188. package/src/types/api/protocolV2.ts +1 -1
  189. package/src/types/device.ts +2 -1
  190. package/src/types/settings.ts +6 -34
  191. package/src/utils/deviceFeaturesCompat.ts +9 -4
  192. package/src/utils/deviceFeaturesUtils.ts +4 -0
  193. package/src/utils/deviceInfoUtils.ts +9 -2
  194. package/src/utils/deviceSettings.ts +11 -4
  195. package/src/utils/index.ts +1 -0
  196. package/src/utils/pro2Nft.ts +31 -8
  197. package/__tests__/firmware-memory-host.test.ts +0 -126
  198. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  199. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  200. package/src/api/firmware/FirmwareMemoryHost.ts +0 -143
@@ -352,7 +352,75 @@ describe('Protocol V1 wallet identity initialization', () => {
352
352
  jest.restoreAllMocks();
353
353
  });
354
354
 
355
- test('verifies the live device id without resetting the cached wallet session', async () => {
355
+ test('sends no wallet context before identity is proven on first contact', async () => {
356
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
357
+ // No cached features: there is no local evidence which device sits here.
358
+ deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
359
+ const typedCall = jest.fn().mockResolvedValue({
360
+ type: 'Features',
361
+ message: { device_id: 'intruder-device', session_id: 'intruder-session' },
362
+ });
363
+ device.commands = { typedCall } as never;
364
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
365
+
366
+ await expect(
367
+ device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' })
368
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
369
+
370
+ // Exactly one context-free Initialize: neither the cached session_id nor
371
+ // the passphrase_state may reach an unproven device.
372
+ expect(typedCall).toHaveBeenCalledTimes(1);
373
+ expect(typedCall).toHaveBeenCalledWith(
374
+ 'Initialize',
375
+ 'Features',
376
+ { is_contains_attach: true },
377
+ expect.any(Object)
378
+ );
379
+ // The expected device's cached session is untouched.
380
+ expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
381
+ });
382
+
383
+ test('proves identity first, then resumes the wallet session on first contact', async () => {
384
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
385
+ deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
386
+ const typedCall = jest
387
+ .fn()
388
+ .mockResolvedValueOnce({
389
+ type: 'Features',
390
+ message: { device_id: 'device-a', session_id: 'standard-session' },
391
+ })
392
+ .mockResolvedValueOnce({
393
+ type: 'Features',
394
+ message: { device_id: 'device-a', session_id: 'session-a' },
395
+ });
396
+ device.commands = { typedCall } as never;
397
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
398
+
399
+ await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
400
+
401
+ expect(typedCall).toHaveBeenCalledTimes(2);
402
+ expect(typedCall).toHaveBeenNthCalledWith(
403
+ 1,
404
+ 'Initialize',
405
+ 'Features',
406
+ { is_contains_attach: true },
407
+ expect.any(Object)
408
+ );
409
+ expect(typedCall).toHaveBeenNthCalledWith(
410
+ 2,
411
+ 'Initialize',
412
+ 'Features',
413
+ expect.objectContaining({
414
+ session_id: 'session-a',
415
+ passphrase_state: 'hidden-a',
416
+ }),
417
+ expect.any(Object)
418
+ );
419
+ // The context-free identity call must not overwrite the cached session.
420
+ expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
421
+ });
422
+
423
+ test('purges cached sessions and rejects when the live device identity changes', async () => {
356
424
  const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
357
425
  device.features = {
358
426
  protocol: 'V1',
@@ -363,7 +431,7 @@ describe('Protocol V1 wallet identity initialization', () => {
363
431
  deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
364
432
  const typedCall = jest.fn().mockResolvedValue({
365
433
  type: 'Features',
366
- message: { device_id: 'live-device-b' },
434
+ message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
367
435
  });
368
436
  device.commands = { typedCall } as never;
369
437
  jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
@@ -371,11 +439,76 @@ describe('Protocol V1 wallet identity initialization', () => {
371
439
  await expect(
372
440
  device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
373
441
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
442
+ // Identity is validated from the single Initialize round trip; no separate
443
+ // GetFeatures preflight.
374
444
  expect(typedCall).toHaveBeenCalledTimes(1);
375
- expect(typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
445
+ expect(typedCall).toHaveBeenCalledWith(
446
+ 'Initialize',
447
+ 'Features',
448
+ expect.objectContaining({ passphrase_state: 'hidden-a' }),
449
+ expect.any(Object)
450
+ );
451
+ // Identity change purges the previous device's cached sessions (pre-existing
452
+ // reconcileDeviceIdentity behavior — never carry sessions across devices)…
453
+ expect(deviceWalletSessionStore.get('cached-device-a', 'hidden-a')).toBeUndefined();
454
+ // …and the session the mismatched Initialize cached under the wrong
455
+ // device's identity is dropped as well.
456
+ expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
376
457
  });
377
458
 
378
- test('resumes a cached V1 wallet after a non-destructive live identity read', async () => {
459
+ test('drops the wrong-device session even when reconfigure fails after Initialize', async () => {
460
+ const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
461
+ device.features = {
462
+ protocol: 'V1',
463
+ deviceId: 'cached-device-a',
464
+ unlocked: true,
465
+ passphraseProtection: true,
466
+ } as never;
467
+ deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
468
+ const typedCall = jest.fn().mockResolvedValue({
469
+ type: 'Features',
470
+ message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
471
+ });
472
+ device.commands = { typedCall } as never;
473
+ // Pre-encode resync succeeds; the post-response reconfigure inside
474
+ // callInitialize rejects — the session write has already happened by then.
475
+ jest
476
+ .spyOn(TransportManager, 'reconfigure')
477
+ .mockResolvedValueOnce(undefined)
478
+ .mockRejectedValueOnce(new Error('configure failed'));
479
+
480
+ await expect(
481
+ device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
482
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
483
+ // The wrong-device session must not survive the error path either.
484
+ expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
485
+ });
486
+
487
+ test('re-syncs the V1 message schema for this device before encoding Initialize', async () => {
488
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
489
+ device.features = {
490
+ protocol: 'V1',
491
+ deviceId: 'device-a',
492
+ unlocked: true,
493
+ passphraseProtection: true,
494
+ } as never;
495
+ const typedCall = jest.fn().mockResolvedValue({
496
+ type: 'Features',
497
+ message: { device_id: 'device-a' },
498
+ });
499
+ device.commands = { typedCall } as never;
500
+ const reconfigure = jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
501
+
502
+ await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
503
+
504
+ // A stale process-global schema (from another device) would silently strip
505
+ // passphrase_state from the wire message, so the resync must come first.
506
+ expect(reconfigure.mock.invocationCallOrder[0]).toBeLessThan(
507
+ typedCall.mock.invocationCallOrder[0]
508
+ );
509
+ });
510
+
511
+ test('resumes a cached V1 wallet with a single identity-validated Initialize', async () => {
379
512
  const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
380
513
  device.features = {
381
514
  protocol: 'V1',
@@ -384,22 +517,17 @@ describe('Protocol V1 wallet identity initialization', () => {
384
517
  passphraseProtection: true,
385
518
  } as never;
386
519
  deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
387
- const typedCall = jest
388
- .fn()
389
- .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
390
- .mockResolvedValueOnce({
391
- type: 'Features',
392
- message: { device_id: 'device-a', session_id: 'session-a' },
393
- });
520
+ const typedCall = jest.fn().mockResolvedValueOnce({
521
+ type: 'Features',
522
+ message: { device_id: 'device-a', session_id: 'session-a' },
523
+ });
394
524
  device.commands = { typedCall } as never;
395
525
  jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
396
526
 
397
527
  await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
398
528
 
399
- expect(typedCall).toHaveBeenCalledTimes(2);
400
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
401
- expect(typedCall).toHaveBeenNthCalledWith(
402
- 2,
529
+ expect(typedCall).toHaveBeenCalledTimes(1);
530
+ expect(typedCall).toHaveBeenCalledWith(
403
531
  'Initialize',
404
532
  'Features',
405
533
  expect.objectContaining({
@@ -408,9 +536,10 @@ describe('Protocol V1 wallet identity initialization', () => {
408
536
  }),
409
537
  expect.any(Object)
410
538
  );
539
+ expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
411
540
  });
412
541
 
413
- test('selects the standard V1 wallet after a non-destructive live identity read', async () => {
542
+ test('selects the standard V1 wallet with a single identity-validated Initialize', async () => {
414
543
  const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
415
544
  device.features = {
416
545
  protocol: 'V1',
@@ -420,17 +549,14 @@ describe('Protocol V1 wallet identity initialization', () => {
420
549
  } as never;
421
550
  const typedCall = jest
422
551
  .fn()
423
- .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
424
552
  .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
425
553
  device.commands = { typedCall } as never;
426
554
  jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
427
555
 
428
556
  await device.initialize({ deviceId: 'device-a' });
429
557
 
430
- expect(typedCall).toHaveBeenCalledTimes(2);
431
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
432
- expect(typedCall).toHaveBeenNthCalledWith(
433
- 2,
558
+ expect(typedCall).toHaveBeenCalledTimes(1);
559
+ expect(typedCall).toHaveBeenCalledWith(
434
560
  'Initialize',
435
561
  'Features',
436
562
  {
@@ -1,6 +1,7 @@
1
1
  import { EDeviceType } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import {
4
+ DEVICE_SETTINGS_NEVER_TIMEOUT_MS,
4
5
  PROTOCOL_V2_NEVER_TIMEOUT_MS,
5
6
  getAutoLockOptions,
6
7
  getAutoShutDownOptions,
@@ -67,7 +68,6 @@ describe('Pro 2 device settings options', () => {
67
68
  { label: '2 minutes', valueMs: 120_000 },
68
69
  { label: '5 minutes', valueMs: 300_000 },
69
70
  { label: '10 minutes', valueMs: 600_000 },
70
- { label: '30 minutes', valueMs: 1_800_000 },
71
71
  { label: 'Never', valueMs: PROTOCOL_V2_NEVER_TIMEOUT_MS },
72
72
  ]);
73
73
  });
@@ -82,10 +82,14 @@ describe('Pro 2 device settings options', () => {
82
82
  );
83
83
  });
84
84
 
85
- test('keeps the legacy and Protocol V2 never values protocol-specific', () => {
85
+ test('returns the firmware-supported never value for both protocols', () => {
86
86
  expect(getAutoLockOptions(EDeviceType.Pro, 'V1').slice(-1)[0]).toEqual({
87
87
  label: 'Never',
88
- valueMs: 0,
88
+ valueMs: DEVICE_SETTINGS_NEVER_TIMEOUT_MS,
89
+ });
90
+ expect(getAutoShutDownOptions(EDeviceType.Pro, 'V1').slice(-1)[0]).toEqual({
91
+ label: 'Never',
92
+ valueMs: DEVICE_SETTINGS_NEVER_TIMEOUT_MS,
89
93
  });
90
94
  expect(getAutoLockOptions(EDeviceType.Pro2, 'V2').slice(-1)[0]).toEqual({
91
95
  label: 'Never',
@@ -1,4 +1,6 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
3
+ import { encode as encodeJpeg } from 'jpeg-js';
2
4
 
3
5
  import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
4
6
 
@@ -13,6 +15,19 @@ const createRgba = (width: number, height: number) => {
13
15
  return data;
14
16
  };
15
17
 
18
+ const jpegBase64Cache = new Map<string, string>();
19
+
20
+ const createJpegBase64 = (width: number, height: number) => {
21
+ const key = `${width}x${height}`;
22
+ const cached = jpegBase64Cache.get(key);
23
+ if (cached) return cached;
24
+ const value = encodeJpeg({ width, height, data: createRgba(width, height) }, 80).data.toString(
25
+ 'base64'
26
+ );
27
+ jpegBase64Cache.set(key, value);
28
+ return value;
29
+ };
30
+
16
31
  const createMethod = ({
17
32
  typedCall,
18
33
  supportedMessages = [60802, 60805, 60808, 61500],
@@ -26,8 +41,8 @@ const createMethod = ({
26
41
  id: 1,
27
42
  payload: {
28
43
  method: 'deviceUploadNft',
29
- image: { width: 540, height: 540, rgba: createRgba(540, 540) },
30
- thumbnail: { width: 263, height: 263, rgba: createRgba(263, 263) },
44
+ imageJpegBase64: createJpegBase64(540, 540),
45
+ thumbnailJpegBase64: createJpegBase64(263, 263),
31
46
  title: 'CryptoPunk #3100',
32
47
  subtitle: 'CryptoPunks',
33
48
  timestampMs: 1_760_000_000_000,
@@ -86,8 +101,8 @@ describe('DeviceUploadNft', () => {
86
101
  id: 1,
87
102
  payload: {
88
103
  method: 'deviceUploadNft',
89
- image: { width: 540, height: 540, rgba: createRgba(540, 540) },
90
- thumbnail: { width: 263, height: 263, rgba: createRgba(263, 263) },
104
+ imageJpegBase64: createJpegBase64(540, 540),
105
+ thumbnailJpegBase64: createJpegBase64(263, 263),
91
106
  title: 'CryptoPunk #3100',
92
107
  subtitle: 'CryptoPunks',
93
108
  timestampMs: 1_760_000_000_000,
@@ -96,12 +111,29 @@ describe('DeviceUploadNft', () => {
96
111
 
97
112
  method.init();
98
113
 
114
+ expect(method.unlockPolicy).toBe('unlock-before-run');
115
+ expect(method.protocolV2PreUnlockPinType).toBe(DeviceSessionPinType.Any);
99
116
  expect((method as any).params).toMatchObject({
100
117
  chunkSize: 2048,
101
118
  paceMs: 0,
102
119
  });
103
120
  });
104
121
 
122
+ test('rejects invalid image Base64 before device communication', () => {
123
+ const method = new DeviceUploadNft({
124
+ id: 1,
125
+ payload: {
126
+ method: 'deviceUploadNft',
127
+ imageJpegBase64: 'not-base64',
128
+ thumbnailJpegBase64: createJpegBase64(263, 263),
129
+ title: 'NFT',
130
+ subtitle: '',
131
+ },
132
+ });
133
+
134
+ expect(() => method.init()).toThrow('canonical Base64');
135
+ });
136
+
105
137
  test('uploads the triplet in order without creating the firmware-owned directory', async () => {
106
138
  const typedCall = jest.fn((request: string, _response: string, params: any) => {
107
139
  if (request === 'FilesystemPathInfoQuery') {
@@ -1,4 +1,6 @@
1
1
  import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { bytesToHex } from '@noble/hashes/utils';
2
4
 
3
5
  import DeviceUpdateBootloader from '../../src/api/device/DeviceUpdateBootloader';
4
6
  import {
@@ -27,10 +29,11 @@ jest.mock('../../src/device/DevicePool', () => ({
27
29
  DevicePool: {},
28
30
  }));
29
31
 
32
+ const artifactBytes = Uint8Array.from([1, 2, 3, 4]);
30
33
  const artifact = {
31
34
  artifactRef: `fw:${'a'.repeat(64)}`,
32
- size: 4,
33
- sha256: 'a'.repeat(64),
35
+ size: artifactBytes.byteLength,
36
+ sha256: bytesToHex(sha256(artifactBytes)),
34
37
  };
35
38
 
36
39
  const createMethod = () => {
@@ -38,7 +41,14 @@ const createMethod = () => {
38
41
  const close = jest.fn().mockRejectedValue(closeError);
39
42
  const artifactReader = {
40
43
  open: jest.fn().mockResolvedValue({ readerId: 'reader-1', size: artifact.size }),
41
- read: jest.fn(),
44
+ read: jest.fn(({ offset, length }: { offset: number; length: number }) => {
45
+ const data = artifactBytes.slice(offset, offset + length).buffer;
46
+ return Promise.resolve({
47
+ data,
48
+ bytesRead: data.byteLength,
49
+ eof: offset + length === artifactBytes.byteLength,
50
+ });
51
+ }),
42
52
  close,
43
53
  };
44
54
  const hostBindingGeneration = registerFirmwareUpdateHostBinding({
@@ -1,3 +1,6 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+
1
4
  import {
2
5
  MAX_FIRMWARE_ARTIFACT_READ_BYTES,
3
6
  openFirmwareByteSource,
@@ -6,19 +9,20 @@ import {
6
9
 
7
10
  import type { FirmwareArtifactReader } from '../../src/types/api/firmwareUpdate';
8
11
 
12
+ const artifactBytes = Uint8Array.from([1, 2, 3, 4, 5, 6]);
9
13
  const artifact = {
10
14
  artifactRef: `fw:${'a'.repeat(64)}`,
11
- size: 6,
12
- sha256: 'b'.repeat(64),
15
+ size: artifactBytes.byteLength,
16
+ sha256: bytesToHex(sha256(artifactBytes)),
13
17
  };
14
18
 
15
- const createReader = () => {
16
- const open = jest.fn(() => Promise.resolve({ readerId: 'reader-1', size: artifact.size }));
19
+ const createReader = (bytes = artifactBytes) => {
20
+ const open = jest.fn(() => Promise.resolve({ readerId: 'reader-1', size: bytes.byteLength }));
17
21
  const read = jest.fn(({ offset, length }: { readerId: string; offset: number; length: number }) =>
18
22
  Promise.resolve({
19
- data: Uint8Array.from([1, 2, 3, 4, 5, 6]).slice(offset, offset + length).buffer,
23
+ data: bytes.slice(offset, offset + length).buffer,
20
24
  bytesRead: length,
21
- eof: offset + length === artifact.size,
25
+ eof: offset + length === bytes.byteLength,
22
26
  })
23
27
  );
24
28
  const close = jest.fn(() => Promise.resolve());
@@ -27,12 +31,18 @@ const createReader = () => {
27
31
  };
28
32
 
29
33
  describe('FirmwareArtifactSource', () => {
30
- it('reads an opaque artifact through bounded sequential reads', async () => {
31
- const { reader, close } = createReader();
34
+ it('materializes verified bytes before exposing the source', async () => {
35
+ const backingBytes = Uint8Array.from(artifactBytes);
36
+ const { reader, read, close } = createReader(backingBytes);
32
37
  const source = await openFirmwareByteSource({ artifact, reader });
33
38
  if (!source) {
34
39
  throw new Error('Expected a firmware byte source');
35
40
  }
41
+ expect(read).toHaveBeenCalledWith({ readerId: 'reader-1', offset: 0, length: artifact.size });
42
+ expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
43
+
44
+ backingBytes.fill(9);
45
+ read.mockClear();
36
46
 
37
47
  const writes: Array<{ offset: number; data: number[] }> = [];
38
48
  await writeFirmwareByteSource({
@@ -52,9 +62,50 @@ describe('FirmwareArtifactSource', () => {
52
62
  { offset: 0, data: [1, 2, 3, 4] },
53
63
  { offset: 4, data: [5, 6] },
54
64
  ]);
65
+ expect(read).not.toHaveBeenCalled();
66
+ expect(close).toHaveBeenCalledTimes(1);
67
+ });
68
+
69
+ it('rejects same-sized bytes that do not match the receipt digest', async () => {
70
+ const { reader, close } = createReader(Uint8Array.from([6, 5, 4, 3, 2, 1]));
71
+
72
+ await expect(openFirmwareByteSource({ artifact, reader })).rejects.toMatchObject({
73
+ params: {
74
+ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
75
+ },
76
+ });
55
77
  expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
56
78
  });
57
79
 
80
+ it('verifies large artifacts through bounded sequential reads', async () => {
81
+ const bytes = new Uint8Array(MAX_FIRMWARE_ARTIFACT_READ_BYTES + 3);
82
+ bytes.fill(7);
83
+ const largeArtifact = {
84
+ ...artifact,
85
+ size: bytes.byteLength,
86
+ sha256: bytesToHex(sha256(bytes)),
87
+ };
88
+ const { reader, read } = createReader(bytes);
89
+
90
+ const source = await openFirmwareByteSource({ artifact: largeArtifact, reader });
91
+ if (!source) {
92
+ throw new Error('Expected a firmware byte source');
93
+ }
94
+ expect(read.mock.calls.map(([request]) => request)).toEqual([
95
+ {
96
+ readerId: 'reader-1',
97
+ offset: 0,
98
+ length: MAX_FIRMWARE_ARTIFACT_READ_BYTES,
99
+ },
100
+ {
101
+ readerId: 'reader-1',
102
+ offset: MAX_FIRMWARE_ARTIFACT_READ_BYTES,
103
+ length: 3,
104
+ },
105
+ ]);
106
+ await source.close();
107
+ });
108
+
58
109
  it('rejects a reader whose opened size differs from the receipt', async () => {
59
110
  const { reader, open, close } = createReader();
60
111
  open.mockResolvedValue({
@@ -70,7 +121,7 @@ describe('FirmwareArtifactSource', () => {
70
121
  expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
71
122
  });
72
123
 
73
- it('rejects paths and over-limit reads before invoking the host', async () => {
124
+ it('rejects paths and over-limit writes before invoking the host again', async () => {
74
125
  const { reader, read } = createReader();
75
126
  await expect(
76
127
  openFirmwareByteSource({
@@ -87,6 +138,7 @@ describe('FirmwareArtifactSource', () => {
87
138
  if (!source) {
88
139
  throw new Error('Expected a firmware byte source');
89
140
  }
141
+ read.mockClear();
90
142
  await expect(
91
143
  writeFirmwareByteSource({
92
144
  source,
@@ -1,4 +1,6 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { bytesToHex } from '@noble/hashes/utils';
2
4
 
3
5
  import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
4
6
  import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
@@ -24,6 +26,9 @@ const noUpdate = {
24
26
  release: undefined,
25
27
  };
26
28
 
29
+ const preparedArtifactBytes = Uint8Array.from([1, 2, 3, 4]);
30
+ const preparedArtifactSha256 = bytesToHex(sha256(preparedArtifactBytes));
31
+
27
32
  const createPreparedPlan = ({
28
33
  executor,
29
34
  target,
@@ -31,11 +36,10 @@ const createPreparedPlan = ({
31
36
  executor: 'v2' | 'v3';
32
37
  target: 'firmware' | 'bootloader';
33
38
  }) => {
34
- const sha256 = target === 'firmware' ? 'a'.repeat(64) : 'b'.repeat(64);
35
39
  const artifact: FirmwareArtifactReference = {
36
- artifactRef: `fw:${sha256}`,
37
- size: 4,
38
- sha256,
40
+ artifactRef: `fw:${preparedArtifactSha256}`,
41
+ size: preparedArtifactBytes.byteLength,
42
+ sha256: preparedArtifactSha256,
39
43
  };
40
44
  const isV3 = executor === 'v3';
41
45
  const plan = buildFirmwareUpdatePlan({
@@ -97,11 +101,10 @@ const registerMismatchedHost = () => {
97
101
  };
98
102
 
99
103
  const createPreparedV4Plan = () => {
100
- const sha256 = 'c'.repeat(64);
101
104
  const artifact: FirmwareArtifactReference = {
102
- artifactRef: `fw:${sha256}`,
103
- size: 4,
104
- sha256,
105
+ artifactRef: `fw:${preparedArtifactSha256}`,
106
+ size: preparedArtifactBytes.byteLength,
107
+ sha256: preparedArtifactSha256,
105
108
  };
106
109
  const plan = buildFirmwareUpdatePlan({
107
110
  features: {
@@ -424,7 +427,14 @@ describe('prepared firmware host digest binding', () => {
424
427
  });
425
428
  const artifactReader = {
426
429
  open: jest.fn().mockResolvedValue({ readerId: 'bootloader-reader', size: artifact.size }),
427
- read: jest.fn(),
430
+ read: jest.fn(({ offset, length }: { offset: number; length: number }) => {
431
+ const data = preparedArtifactBytes.slice(offset, offset + length).buffer;
432
+ return Promise.resolve({
433
+ data,
434
+ bytesRead: data.byteLength,
435
+ eof: offset + length === preparedArtifactBytes.byteLength,
436
+ });
437
+ }),
428
438
  close: jest.fn().mockResolvedValue(undefined),
429
439
  };
430
440
  const hostBindingGeneration = registerFirmwareUpdateHostBinding({
@@ -105,6 +105,27 @@ const createPreparedResourcePlan = ({
105
105
  };
106
106
 
107
107
  describe.each(['v2', 'v3'] as const)('%s prepared resource archive binding', executor => {
108
+ test('rejects a missing artifact reader', async () => {
109
+ const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
110
+ const zip = new JSZip();
111
+ zip.file('images/logo.bin', entryBinary);
112
+ const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
113
+ const { preparedPlan } = createPreparedResourcePlan({
114
+ executor,
115
+ archiveBinary,
116
+ entryBinary,
117
+ });
118
+
119
+ await expect(
120
+ readVerifiedPreparedResourceArchive({
121
+ preparedPlan,
122
+ reader: undefined,
123
+ })
124
+ ).rejects.toMatchObject({
125
+ params: { firmwareUpdateCode: 'FirmwareArtifactReaderInvalid' },
126
+ });
127
+ });
128
+
108
129
  test('uses canonical bytes extracted from the approved ZIP', async () => {
109
130
  const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
110
131
  const zip = new JSZip();
@@ -203,6 +203,69 @@ describe.each([
203
203
  });
204
204
  });
205
205
 
206
+ describe('FirmwareUpdateV2 WebUSB bootloader polling', () => {
207
+ beforeEach(() => {
208
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
209
+ });
210
+
211
+ afterEach(() => {
212
+ jest.clearAllTimers();
213
+ jest.useRealTimers();
214
+ jest.restoreAllMocks();
215
+ });
216
+
217
+ test('skips interval ticks while a bootloader probe is in flight and resumes afterward', async () => {
218
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop' as never);
219
+ jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
220
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
221
+
222
+ const method = new FirmwareUpdateV2({
223
+ id: 1,
224
+ payload: { method: 'firmwareUpdateV2', connectId: WEBUSB_ID },
225
+ });
226
+
227
+ let inFlight = 0;
228
+ let maxInFlight = 0;
229
+ const resolveProbes: Array<(found: boolean) => void> = [];
230
+ const probe = jest.spyOn(method as any, '_checkDeviceInBootloaderMode').mockImplementation(
231
+ () =>
232
+ new Promise<boolean>(resolve => {
233
+ inFlight += 1;
234
+ maxInFlight = Math.max(maxInFlight, inFlight);
235
+ resolveProbes.push(found => {
236
+ inFlight -= 1;
237
+ resolve(found);
238
+ });
239
+ })
240
+ );
241
+
242
+ method.device = {
243
+ getCurrentDeviceType: jest.fn(() => EDeviceType.Classic),
244
+ } as unknown as Device;
245
+
246
+ method.checkDeviceToBootloader(WEBUSB_ID);
247
+
248
+ for (let elapsed = 0; elapsed < 6500; elapsed += 500) {
249
+ jest.advanceTimersByTime(500);
250
+ // eslint-disable-next-line no-await-in-loop
251
+ await flush();
252
+ }
253
+
254
+ expect(probe).toHaveBeenCalled();
255
+ expect(maxInFlight).toBe(1);
256
+
257
+ resolveProbes[0]?.(false);
258
+ await flush();
259
+ jest.advanceTimersByTime(2000);
260
+ await flush();
261
+
262
+ expect(probe).toHaveBeenCalledTimes(2);
263
+ expect(maxInFlight).toBe(1);
264
+ resolveProbes[1]?.(false);
265
+ await flush();
266
+ });
267
+ });
268
+
206
269
  describe.each([
207
270
  [
208
271
  'FirmwareUpdateV2',