@onekeyfe/hd-core 1.2.0-alpha.18 → 1.2.0-alpha.19

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 (141) hide show
  1. package/__tests__/device-features-state.test.ts +78 -0
  2. package/__tests__/device-identity.test.ts +30 -0
  3. package/__tests__/device-pool-state.test.ts +113 -0
  4. package/__tests__/device-settings.test.ts +108 -0
  5. package/__tests__/device-state-contract.test.ts +27 -0
  6. package/__tests__/device-state-events.test.ts +163 -0
  7. package/__tests__/device-state-mapper.test.ts +222 -0
  8. package/__tests__/device-state-projector.test.ts +94 -0
  9. package/__tests__/device-state-store.test.ts +115 -0
  10. package/__tests__/device-wallet-session-store.test.ts +46 -0
  11. package/__tests__/deviceFeaturesUtils.test.ts +13 -1
  12. package/__tests__/deviceSettings.test.ts +54 -0
  13. package/__tests__/get-device-state.test.ts +367 -0
  14. package/__tests__/logBlockEvent.test.ts +13 -0
  15. package/__tests__/protocol-v2.test.ts +358 -413
  16. package/__tests__/public-device-state-api.test.ts +153 -0
  17. package/__tests__/refresh-device-state.test.ts +101 -0
  18. package/__tests__/request-context-logging.test.ts +16 -0
  19. package/__tests__/search-devices.test.ts +93 -0
  20. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/GetDeviceState.d.ts +7 -0
  23. package/dist/api/GetDeviceState.d.ts.map +1 -0
  24. package/dist/api/GetFeatures.d.ts +1 -1
  25. package/dist/api/GetFeatures.d.ts.map +1 -1
  26. package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
  27. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  28. package/dist/api/SearchDevices.d.ts.map +1 -1
  29. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  30. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  31. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  32. package/dist/api/index.d.ts +1 -4
  33. package/dist/api/index.d.ts.map +1 -1
  34. package/dist/api/internal.d.ts +4 -0
  35. package/dist/api/internal.d.ts.map +1 -0
  36. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +0 -1
  37. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  38. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  39. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -1
  40. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -1
  41. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -1
  42. package/dist/api/utils.d.ts +2 -1
  43. package/dist/api/utils.d.ts.map +1 -1
  44. package/dist/constants/index.d.ts +1 -1
  45. package/dist/constants/index.d.ts.map +1 -1
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/device/Device.d.ts +22 -3
  48. package/dist/device/Device.d.ts.map +1 -1
  49. package/dist/device/DeviceFeaturesState.d.ts +4 -0
  50. package/dist/device/DeviceFeaturesState.d.ts.map +1 -0
  51. package/dist/device/DevicePool.d.ts +1 -0
  52. package/dist/device/DevicePool.d.ts.map +1 -1
  53. package/dist/device/DeviceStateMapper.d.ts +13 -0
  54. package/dist/device/DeviceStateMapper.d.ts.map +1 -0
  55. package/dist/device/DeviceStateProjector.d.ts +8 -0
  56. package/dist/device/DeviceStateProjector.d.ts.map +1 -0
  57. package/dist/device/DeviceStateStore.d.ts +23 -0
  58. package/dist/device/DeviceStateStore.d.ts.map +1 -0
  59. package/dist/device/deviceIdentity.d.ts +5 -0
  60. package/dist/device/deviceIdentity.d.ts.map +1 -0
  61. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  62. package/dist/deviceProfile/index.d.ts +0 -1
  63. package/dist/deviceProfile/index.d.ts.map +1 -1
  64. package/dist/events/device.d.ts +7 -2
  65. package/dist/events/device.d.ts.map +1 -1
  66. package/dist/index.d.ts +688 -639
  67. package/dist/index.js +1274 -942
  68. package/dist/inject.d.ts.map +1 -1
  69. package/dist/protocols/protocol-v2/features.d.ts +1 -0
  70. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  71. package/dist/protocols/protocol-v2/index.d.ts +1 -1
  72. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  74. package/dist/types/api/deviceSettings.d.ts +8 -0
  75. package/dist/types/api/deviceSettings.d.ts.map +1 -1
  76. package/dist/types/api/getDeviceState.d.ts +12 -0
  77. package/dist/types/api/getDeviceState.d.ts.map +1 -0
  78. package/dist/types/api/index.d.ts +4 -7
  79. package/dist/types/api/index.d.ts.map +1 -1
  80. package/dist/types/api/protocolV2.d.ts +1 -6
  81. package/dist/types/api/protocolV2.d.ts.map +1 -1
  82. package/dist/types/device.d.ts +121 -1
  83. package/dist/types/device.d.ts.map +1 -1
  84. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  85. package/dist/utils/deviceSettings.d.ts.map +1 -1
  86. package/dist/utils/patch.d.ts +1 -1
  87. package/dist/utils/tracing.d.ts.map +1 -1
  88. package/package.json +4 -4
  89. package/src/api/ClearSessionCache.ts +4 -0
  90. package/src/api/FirmwareUpdateV4.ts +15 -7
  91. package/src/api/GetDeviceState.ts +55 -0
  92. package/src/api/GetFeatures.ts +8 -3
  93. package/src/api/GetOnekeyFeatures.ts +3 -72
  94. package/src/api/GetPassphraseState.ts +3 -1
  95. package/src/api/SearchDevices.ts +25 -4
  96. package/src/api/device/DeviceRebootToBoardloader.ts +1 -0
  97. package/src/api/device/DeviceRebootToBootloader.ts +1 -0
  98. package/src/api/device/DeviceSettings.ts +28 -0
  99. package/src/api/index.ts +1 -4
  100. package/src/api/internal.ts +9 -0
  101. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -5
  102. package/src/api/protocol-v2/DeviceReboot.ts +3 -1
  103. package/src/api/protocol-v2/DeviceSettingsGet.ts +3 -1
  104. package/src/api/protocol-v2/DeviceSettingsSet.ts +2 -0
  105. package/src/api/protocol-v2/DeviceStatusGet.ts +1 -0
  106. package/src/api/utils.ts +9 -3
  107. package/src/constants/index.ts +1 -4
  108. package/src/core/index.ts +7 -1
  109. package/src/data/messages/messages-protocol-v2.json +22 -13
  110. package/src/device/Device.ts +256 -78
  111. package/src/device/DeviceFeaturesState.ts +34 -0
  112. package/src/device/DevicePool.ts +35 -9
  113. package/src/device/DeviceStateMapper.ts +364 -0
  114. package/src/device/DeviceStateProjector.ts +92 -0
  115. package/src/device/DeviceStateStore.ts +242 -0
  116. package/src/device/deviceIdentity.ts +18 -0
  117. package/src/deviceProfile/buildDeviceFeatures.ts +166 -109
  118. package/src/deviceProfile/index.ts +0 -1
  119. package/src/events/device.ts +13 -1
  120. package/src/events/logBlockEvent.ts +1 -1
  121. package/src/inject.ts +4 -5
  122. package/src/protocols/protocol-v2/features.ts +18 -9
  123. package/src/protocols/protocol-v2/index.ts +1 -0
  124. package/src/protocols/protocol-v2/walletSession.ts +5 -7
  125. package/src/types/api/deviceSettings.ts +8 -0
  126. package/src/types/api/getDeviceState.ts +19 -0
  127. package/src/types/api/index.ts +3 -20
  128. package/src/types/api/protocolV2.ts +0 -24
  129. package/src/types/device.ts +148 -1
  130. package/src/utils/deviceFeaturesUtils.ts +5 -1
  131. package/src/utils/deviceSettings.ts +41 -0
  132. package/src/utils/tracing.ts +3 -1
  133. package/dist/api/GetDeviceInfo.d.ts +0 -9
  134. package/dist/api/GetDeviceInfo.d.ts.map +0 -1
  135. package/dist/deviceProfile/buildDeviceProfile.d.ts +0 -22
  136. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +0 -1
  137. package/dist/types/api/getDeviceInfo.d.ts +0 -88
  138. package/dist/types/api/getDeviceInfo.d.ts.map +0 -1
  139. package/src/api/GetDeviceInfo.ts +0 -152
  140. package/src/deviceProfile/buildDeviceProfile.ts +0 -370
  141. package/src/types/api/getDeviceInfo.ts +0 -106
@@ -33,7 +33,6 @@ import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
33
33
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
34
34
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
35
35
  import FirmwareUpdateV4, { assertProtocolV2ReconnectIdentity } from '../src/api/FirmwareUpdateV4';
36
- import GetDeviceInfo from '../src/api/GetDeviceInfo';
37
36
  import GetPassphraseState from '../src/api/GetPassphraseState';
38
37
  import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures';
39
38
  import { batchGetPublickeys } from '../src/api/helpers/batchGetPublickeys';
@@ -73,7 +72,6 @@ import {
73
72
  import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry';
74
73
  import { BaseMethod } from '../src/api/BaseMethod';
75
74
  import {
76
- buildProfileFromProtocolV2,
77
75
  buildProtocolV1FeaturesPayload,
78
76
  buildProtocolV2FeaturesPayload,
79
77
  } from '../src/deviceProfile';
@@ -301,6 +299,8 @@ const descriptor = {
301
299
  */
302
300
  function stubDevice<T extends Record<string, any>>(device: T): T {
303
301
  const d = device as any;
302
+ d.updateState ??= jest.fn();
303
+ d.markProtocolV2Reboot ??= jest.fn();
304
304
  d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
305
305
  d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1');
306
306
  d.getCurrentDeviceType ??= () => getDeviceType(d.features);
@@ -337,6 +337,13 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
337
337
  });
338
338
  return d.features;
339
339
  };
340
+ d.updateFeaturesPatch ??= (patch: Record<string, unknown>) => {
341
+ d.features = {
342
+ ...d.features,
343
+ ...Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined)),
344
+ };
345
+ return d.features;
346
+ };
340
347
  return device;
341
348
  }
342
349
 
@@ -368,8 +375,7 @@ async function requestProtocolV2Features({
368
375
  descriptor?: unknown;
369
376
  }) {
370
377
  const deviceInfo = await requestProtocolV2DeviceInfo({ commands });
371
- const deviceStatus = await requestProtocolV2DeviceStatus({ commands });
372
- return buildProtocolV2FeaturesPayload({ deviceInfo, deviceStatus });
378
+ return buildProtocolV2FeaturesPayload({ deviceInfo });
373
379
  }
374
380
 
375
381
  describe('Protocol V2 feature adapter', () => {
@@ -389,6 +395,26 @@ describe('Protocol V2 feature adapter', () => {
389
395
  expect(features.passphrase_protection).toBeUndefined();
390
396
  });
391
397
 
398
+ test('preserves the last trusted passphrase setting while the device is locked', () => {
399
+ const features = buildProtocolV2FeaturesPayload({
400
+ deviceInfo: {
401
+ hw: { serial_no: 'P2-LOCKED' },
402
+ },
403
+ deviceStatus: {
404
+ unlocked: false,
405
+ passphrase_enabled: false,
406
+ },
407
+ previous: {
408
+ ...buildProtocolV2FeaturesPayload({
409
+ deviceInfo: { hw: { serial_no: 'P2-LOCKED' } },
410
+ }),
411
+ passphraseProtection: true,
412
+ },
413
+ });
414
+
415
+ expect(features.passphraseProtection).toBe(true);
416
+ });
417
+
392
418
  test('builds dynamic features from DeviceStatus when DeviceInfo has no nested status', () => {
393
419
  const deviceInfo: ProtocolV2DeviceInfo = {
394
420
  protocol_version: 1,
@@ -563,13 +589,9 @@ describe('Protocol V2 feature adapter', () => {
563
589
  },
564
590
  };
565
591
  const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
566
- const profile = buildProfileFromProtocolV2({ deviceInfo });
567
-
568
592
  expect(features.mode).toBe('bootloader');
569
593
  expect(features.bootloaderMode).toBe(true);
570
594
  expect(features.initialized).toBeNull();
571
- expect(profile.status.mode).toBe('bootloader');
572
- expect(profile.status.bootloaderMode).toBe(true);
573
595
  });
574
596
 
575
597
  test('marks current romloader-shaped Protocol V2 DeviceInfo as romloader mode', () => {
@@ -588,14 +610,9 @@ describe('Protocol V2 feature adapter', () => {
588
610
  },
589
611
  };
590
612
  const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
591
- const profile = buildProfileFromProtocolV2({ deviceInfo });
592
-
593
613
  expect(features.mode).toBe('romloader');
594
614
  expect(features.bootloaderMode).toBe(false);
595
615
  expect(features.boardVersion).toBe('1.0.0');
596
- expect(profile.status.mode).toBe('romloader');
597
- expect(profile.status.bootloaderMode).toBe(false);
598
- expect(profile.versions.board).toBe('1.0.0');
599
616
  });
600
617
 
601
618
  test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
@@ -627,14 +644,17 @@ describe('Protocol V2 feature adapter', () => {
627
644
  expect(device.getInternalState()).toBeUndefined();
628
645
  });
629
646
 
630
- test('uses the default main wallet context when Protocol V2 passphrase protection is disabled', async () => {
647
+ test('keeps Protocol V2 on the standard wallet when passphrase protection is disabled', async () => {
631
648
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
632
649
  (device as any).features = normalizeProtocolV2Features(
633
650
  { ...descriptor, protocolType: 'V2' } as any,
634
651
  { status: { passphrase_enabled: false, unlocked: true } }
635
652
  );
636
653
  const typedCall = jest.fn();
637
- (device as any).commands = { typedCall };
654
+ const promptPassphrase = jest
655
+ .fn()
656
+ .mockResolvedValue({ passphrase: 'hidden-wallet-with-disabled-flag' });
657
+ (device as any).commands = { typedCall, promptPassphrase };
638
658
 
639
659
  await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
640
660
  passphraseState: undefined,
@@ -642,10 +662,51 @@ describe('Protocol V2 feature adapter', () => {
642
662
  unlockedAttachPin: false,
643
663
  });
644
664
 
665
+ expect(promptPassphrase).not.toHaveBeenCalled();
645
666
  expect(typedCall).not.toHaveBeenCalled();
646
667
  });
647
668
 
648
- test('unlocks before deciding that Protocol V2 uses the default main wallet context', async () => {
669
+ test('preserves cached settings only while Protocol V2 physical identity stays the same', () => {
670
+ const previous = {
671
+ ...normalizeProtocolV2Features(descriptor as any, {
672
+ hw: { serial_no: 'PR2-SAME' },
673
+ }),
674
+ deviceId: 'wallet-device-id',
675
+ label: 'Renamed Pro 2',
676
+ autoLockDelayMs: 60_000,
677
+ experimentalFeatures: true,
678
+ passphraseProtection: true,
679
+ attachToPinEnabled: true,
680
+ };
681
+
682
+ const refreshed = buildProtocolV2FeaturesPayload({
683
+ deviceInfo: { hw: { serial_no: 'PR2-SAME' } },
684
+ previous,
685
+ });
686
+ expect(refreshed).toMatchObject({
687
+ deviceId: 'wallet-device-id',
688
+ label: 'Renamed Pro 2',
689
+ autoLockDelayMs: 60_000,
690
+ experimentalFeatures: true,
691
+ passphraseProtection: true,
692
+ attachToPinEnabled: true,
693
+ });
694
+
695
+ const replacedDevice = buildProtocolV2FeaturesPayload({
696
+ deviceInfo: { hw: { serial_no: 'PR2-OTHER' } },
697
+ previous,
698
+ });
699
+ expect(replacedDevice).toMatchObject({
700
+ deviceId: null,
701
+ label: null,
702
+ autoLockDelayMs: null,
703
+ experimentalFeatures: null,
704
+ passphraseProtection: null,
705
+ attachToPinEnabled: null,
706
+ });
707
+ });
708
+
709
+ test('uses refreshed passphrase status after unlocking before selecting a Protocol V2 wallet', async () => {
649
710
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
650
711
  (device as any).features = normalizeProtocolV2Features(
651
712
  { ...descriptor, protocolType: 'V2' } as any,
@@ -656,8 +717,23 @@ describe('Protocol V2 feature adapter', () => {
656
717
  },
657
718
  }
658
719
  );
659
- const typedCall = jest.fn();
660
- const promptPassphrase = jest.fn();
720
+ const typedCall = jest.fn().mockImplementation((request: string) => {
721
+ if (request === 'DeviceSessionOpen') {
722
+ return Promise.resolve({
723
+ message: {
724
+ btc_test_address: 'state-after-unlock',
725
+ session_id: 'session-after-unlock',
726
+ },
727
+ });
728
+ }
729
+ if (request === 'DeviceStatusGet') {
730
+ return Promise.resolve({
731
+ message: { passphrase_enabled: false, unlocked: true },
732
+ });
733
+ }
734
+ throw new Error(`Unexpected request: ${request}`);
735
+ });
736
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden-after-unlock' });
661
737
  const unlockDevice = jest.spyOn(device, 'unlockDevice').mockImplementation(() => {
662
738
  (device as any).features = {
663
739
  ...(device as any).features,
@@ -874,11 +950,10 @@ describe('Protocol V2 feature adapter', () => {
874
950
  expect(() => method.init()).toThrow();
875
951
  });
876
952
 
877
- test('routes raw status and session methods through CoreApi', async () => {
953
+ test('routes public onboarding and session methods through CoreApi', async () => {
878
954
  const call = jest.fn().mockResolvedValue({ success: true, payload: {} });
879
955
  const api = createCoreApi(call as any);
880
956
 
881
- await api.deviceStatusGet('connect-id', { retryCount: 1 });
882
957
  await api.deviceGetOnboardingStatus('connect-id', { retryCount: 1 });
883
958
  await api.deviceSessionOpen('connect-id', {
884
959
  retryCount: 1,
@@ -886,16 +961,11 @@ describe('Protocol V2 feature adapter', () => {
886
961
  });
887
962
 
888
963
  expect(call).toHaveBeenNthCalledWith(1, {
889
- method: 'deviceStatusGet',
890
- connectId: 'connect-id',
891
- retryCount: 1,
892
- });
893
- expect(call).toHaveBeenNthCalledWith(2, {
894
964
  method: 'deviceGetOnboardingStatus',
895
965
  connectId: 'connect-id',
896
966
  retryCount: 1,
897
967
  });
898
- expect(call).toHaveBeenNthCalledWith(3, {
968
+ expect(call).toHaveBeenNthCalledWith(2, {
899
969
  method: 'deviceSessionOpen',
900
970
  connectId: 'connect-id',
901
971
  retryCount: 1,
@@ -1190,6 +1260,8 @@ describe('Protocol V2 feature adapter', () => {
1190
1260
 
1191
1261
  expect(device.features?.mode).toBe('bootloader');
1192
1262
  expect(device.features?.bootloaderMode).toBe(true);
1263
+ expect(device.features?.initialized).toBeNull();
1264
+ expect(device.features?.unlocked).toBeNull();
1193
1265
  expect(device.features?.raw?.protocolV2DeviceStatus).toBeUndefined();
1194
1266
  });
1195
1267
 
@@ -1281,6 +1353,40 @@ describe('Protocol V2 feature adapter', () => {
1281
1353
  expect(getFeatures).not.toHaveBeenCalled();
1282
1354
  });
1283
1355
 
1356
+ test('uses the Pro2 standard wallet without passphraseState when passphrase is disabled', async () => {
1357
+ const features = {
1358
+ deviceId: 'pro2-device-id',
1359
+ deviceType: 'pro2',
1360
+ firmwareVersion: '9.9.9',
1361
+ passphraseProtection: false,
1362
+ unlocked: true,
1363
+ };
1364
+ const typedCall = jest.fn();
1365
+ const updateInternalState = jest.fn();
1366
+ const method = new GetPassphraseState({
1367
+ payload: {
1368
+ method: 'getPassphraseState',
1369
+ connectId: 'connect-id',
1370
+ },
1371
+ });
1372
+ method.device = stubDevice({
1373
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
1374
+ features,
1375
+ commands: {
1376
+ typedCall,
1377
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'mock-hidden-wallet' }),
1378
+ },
1379
+ updateInternalState,
1380
+ getCurrentDeviceType: () => 'pro2',
1381
+ getCurrentDeviceId: () => 'pro2-device-id',
1382
+ getCurrentPassphraseProtection: () => false,
1383
+ }) as any;
1384
+
1385
+ await expect(method.run()).resolves.toBeUndefined();
1386
+ expect(typedCall).not.toHaveBeenCalled();
1387
+ expect(updateInternalState).not.toHaveBeenCalled();
1388
+ });
1389
+
1284
1390
  test('honors initSession when getting Pro2 passphrase state', async () => {
1285
1391
  const features = {
1286
1392
  deviceId: 'pro2-device-id',
@@ -1378,9 +1484,13 @@ describe('Protocol V2 feature adapter', () => {
1378
1484
  hw: { serial_no: 'PR2SERIAL' },
1379
1485
  }
1380
1486
  );
1381
- (device as any).features.firmwareVersion = '4.15.0';
1382
- (device as any).features.passphraseProtection = true;
1383
- (device as any).features.unlocked = true;
1487
+ device.updateState(
1488
+ {
1489
+ versions: { firmware: '4.15.0' },
1490
+ status: { passphraseProtection: true, unlocked: true },
1491
+ },
1492
+ 'compatibility'
1493
+ );
1384
1494
  (device as any).commands = {
1385
1495
  typedCall,
1386
1496
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-auto-secret' }),
@@ -1541,6 +1651,8 @@ describe('Protocol V2 feature adapter', () => {
1541
1651
  uuid: 'CACHED-SERIAL',
1542
1652
  deviceId: null,
1543
1653
  bleName: 'Cached BLE',
1654
+ name: 'Cached BLE',
1655
+ displayName: 'Cached Label',
1544
1656
  label: 'Cached Label',
1545
1657
  deviceType: 'pro2',
1546
1658
  });
@@ -1569,22 +1681,13 @@ describe('Protocol V2 feature adapter', () => {
1569
1681
 
1570
1682
  test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => {
1571
1683
  const commands = {
1572
- typedCall: jest
1573
- .fn()
1574
- .mockResolvedValueOnce({
1575
- type: 'DeviceInfo',
1576
- message: {
1577
- hw: { serial_no: 'PR2SERIAL' },
1578
- },
1579
- })
1580
- .mockResolvedValueOnce({
1581
- type: 'DeviceStatus',
1582
- message: {
1583
- device_id: 'PRO2-STATUS-ID',
1584
- init_states: true,
1585
- passphrase_enabled: true,
1586
- },
1587
- }),
1684
+ typedCall: jest.fn().mockResolvedValueOnce({
1685
+ type: 'DeviceInfo',
1686
+ message: {
1687
+ hw: { serial_no: 'PR2SERIAL' },
1688
+ fw: { application: { version: '1.2.3' } },
1689
+ },
1690
+ }),
1588
1691
  };
1589
1692
 
1590
1693
  const features = await requestProtocolV2Features({
@@ -1592,12 +1695,12 @@ describe('Protocol V2 feature adapter', () => {
1592
1695
  descriptor: descriptor as any,
1593
1696
  });
1594
1697
 
1595
- expect(features.deviceId).toBe('PRO2-STATUS-ID');
1596
- expect(features.initialized).toBe(true);
1698
+ expect(features.deviceId).toBeNull();
1699
+ expect(features.initialized).toBeNull();
1597
1700
  expect(features.passphraseProtection).toBeNull();
1598
- expect(commands.typedCall).toHaveBeenCalledTimes(2);
1599
- expect(commands.typedCall).toHaveBeenNthCalledWith(
1600
- 1,
1701
+ expect(features.bootloaderMode).toBe(false);
1702
+ expect(commands.typedCall).toHaveBeenCalledTimes(1);
1703
+ expect(commands.typedCall).toHaveBeenCalledWith(
1601
1704
  'DeviceInfoGet',
1602
1705
  'DeviceInfo',
1603
1706
  {
@@ -1613,7 +1716,6 @@ describe('Protocol V2 feature adapter', () => {
1613
1716
  },
1614
1717
  { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
1615
1718
  );
1616
- expect(commands.typedCall).toHaveBeenNthCalledWith(2, 'DeviceStatusGet', 'DeviceStatus', {});
1617
1719
  });
1618
1720
 
1619
1721
  test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => {
@@ -1629,266 +1731,6 @@ describe('Protocol V2 feature adapter', () => {
1629
1731
  ).rejects.toThrow('DeviceInfo not supported');
1630
1732
  });
1631
1733
 
1632
- test('refreshes Protocol V2 basic device info with the lightweight request', async () => {
1633
- const typedCall = jest
1634
- .fn()
1635
- .mockResolvedValueOnce({
1636
- type: 'DeviceInfo',
1637
- message: {
1638
- hw: { serial_no: 'PR2SERIAL' },
1639
- fw: {
1640
- application: {
1641
- version: '5.6.7',
1642
- },
1643
- },
1644
- coprocessor: {
1645
- application: {
1646
- version: '8.9.10',
1647
- },
1648
- bt_adv_name: 'Raw Pro2 BLE',
1649
- },
1650
- },
1651
- })
1652
- .mockResolvedValueOnce({
1653
- type: 'DeviceStatus',
1654
- message: {
1655
- device_id: 'PRO2-BASIC-ID',
1656
- unlocked_by_attach_to_pin: true,
1657
- init_states: true,
1658
- passphrase_enabled: true,
1659
- },
1660
- });
1661
- const method = new GetDeviceInfo({
1662
- id: 1,
1663
- payload: {
1664
- method: 'getDeviceInfo',
1665
- scope: 'basic',
1666
- refresh: true,
1667
- },
1668
- });
1669
- method.init();
1670
- (method as any).device = stubDevice({
1671
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
1672
- features: {
1673
- ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1674
- device_id: 'CACHED-ID',
1675
- serial_no: 'CACHED-SERIAL',
1676
- label: 'Cached Pro2',
1677
- onekey_firmware_version: '1.2.3',
1678
- onekey_ble_version: '2.3.4',
1679
- },
1680
- commands: { typedCall },
1681
- _updateFeatures: jest.fn(),
1682
- });
1683
-
1684
- const result = await method.run();
1685
-
1686
- expect(typedCall).toHaveBeenCalledWith(
1687
- 'DeviceInfoGet',
1688
- 'DeviceInfo',
1689
- {
1690
- targets: {
1691
- hw: true,
1692
- fw: true,
1693
- coprocessor: true,
1694
- },
1695
- types: {
1696
- version: true,
1697
- specific: true,
1698
- },
1699
- },
1700
- { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
1701
- );
1702
- expect(result).toMatchObject({
1703
- protocol: 'V2',
1704
- deviceType: 'pro2',
1705
- deviceId: 'PRO2-BASIC-ID',
1706
- serialNo: 'PR2SERIAL',
1707
- label: null,
1708
- bleName: 'Raw Pro2 BLE',
1709
- status: {
1710
- initialized: true,
1711
- unlockedAttachPin: true,
1712
- passphraseProtection: true,
1713
- },
1714
- versions: {
1715
- firmware: '5.6.7',
1716
- ble: '8.9.10',
1717
- },
1718
- });
1719
- });
1720
-
1721
- test('does not fill Protocol V2 DeviceProfile from cached V1-shaped fields', async () => {
1722
- const typedCall = jest.fn().mockResolvedValueOnce({
1723
- type: 'DeviceInfo',
1724
- message: {
1725
- status: {
1726
- init_states: true,
1727
- },
1728
- },
1729
- });
1730
- const method = new GetDeviceInfo({
1731
- id: 1,
1732
- payload: {
1733
- method: 'getDeviceInfo',
1734
- scope: 'basic',
1735
- },
1736
- });
1737
- method.init();
1738
- (method as any).device = stubDevice({
1739
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
1740
- features: {
1741
- ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1742
- device_id: 'STALE-ID',
1743
- serial_no: 'STALE-SERIAL',
1744
- label: 'Stale Pro2',
1745
- ble_name: 'Stale BLE',
1746
- passphrase_protection: true,
1747
- onekey_firmware_version: '1.2.3',
1748
- onekey_ble_version: '2.3.4',
1749
- },
1750
- commands: { typedCall },
1751
- });
1752
-
1753
- const result = await method.run();
1754
-
1755
- expect(result).toMatchObject({
1756
- protocol: 'V2',
1757
- // 身份字段不得取自缓存的 V1-shaped features(STALE-ID / STALE-SERIAL),
1758
- // hw.serial_no 缺失时也不回退 descriptor.path。
1759
- deviceId: '',
1760
- serialNo: '',
1761
- label: null,
1762
- bleName: null,
1763
- status: {
1764
- passphraseProtection: null,
1765
- noBackup: null,
1766
- },
1767
- versions: {
1768
- firmware: null,
1769
- ble: null,
1770
- },
1771
- });
1772
- });
1773
-
1774
- test('reads full Protocol V2 device info only for verify scope', async () => {
1775
- const typedCall = jest
1776
- .fn()
1777
- .mockResolvedValueOnce({
1778
- type: 'DeviceInfo',
1779
- message: { hw: { serial_no: 'PR2SERIAL' } },
1780
- })
1781
- .mockResolvedValueOnce({
1782
- type: 'DeviceStatus',
1783
- message: { init_states: true },
1784
- });
1785
- const method = new GetDeviceInfo({
1786
- id: 1,
1787
- payload: {
1788
- method: 'getDeviceInfo',
1789
- scope: 'verify',
1790
- },
1791
- });
1792
- method.init();
1793
- (method as any).device = stubDevice({
1794
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
1795
- features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1796
- commands: { typedCall },
1797
- _updateFeatures: jest.fn(),
1798
- });
1799
-
1800
- await method.run();
1801
-
1802
- expect(typedCall).toHaveBeenCalledWith(
1803
- 'DeviceInfoGet',
1804
- 'DeviceInfo',
1805
- {
1806
- targets: {
1807
- hw: true,
1808
- fw: true,
1809
- coprocessor: true,
1810
- se1: true,
1811
- se2: true,
1812
- se3: true,
1813
- se4: true,
1814
- },
1815
- types: {
1816
- version: true,
1817
- build_id: true,
1818
- hash: true,
1819
- specific: true,
1820
- },
1821
- },
1822
- { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
1823
- );
1824
- });
1825
-
1826
- test('reads Protocol V2 SE version fields for versions scope', async () => {
1827
- const typedCall = jest
1828
- .fn()
1829
- .mockResolvedValueOnce({
1830
- type: 'DeviceInfo',
1831
- message: {
1832
- hw: { serial_no: 'PR2SERIAL' },
1833
- se1: {
1834
- application: { version: '1.0.1' },
1835
- bootloader: { version: '1.0.0' },
1836
- },
1837
- se2: {
1838
- application: { version: '2.0.1' },
1839
- bootloader: { version: '2.0.0' },
1840
- },
1841
- },
1842
- })
1843
- .mockResolvedValueOnce({
1844
- type: 'DeviceStatus',
1845
- message: { init_states: true },
1846
- });
1847
- const method = new GetDeviceInfo({
1848
- id: 1,
1849
- payload: {
1850
- method: 'getDeviceInfo',
1851
- scope: 'versions',
1852
- },
1853
- });
1854
- method.init();
1855
- (method as any).device = stubDevice({
1856
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
1857
- features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1858
- commands: { typedCall },
1859
- });
1860
-
1861
- const result = await method.run();
1862
-
1863
- expect(typedCall).toHaveBeenCalledWith(
1864
- 'DeviceInfoGet',
1865
- 'DeviceInfo',
1866
- {
1867
- targets: {
1868
- hw: true,
1869
- fw: true,
1870
- coprocessor: true,
1871
- se1: true,
1872
- se2: true,
1873
- se3: true,
1874
- se4: true,
1875
- },
1876
- types: {
1877
- version: true,
1878
- specific: true,
1879
- },
1880
- },
1881
- { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
1882
- );
1883
- expect(result.versions).toMatchObject({
1884
- se01: '1.0.1',
1885
- se01Boot: '1.0.0',
1886
- se02: '2.0.1',
1887
- se02Boot: '2.0.0',
1888
- });
1889
- expect(result).not.toHaveProperty('verify');
1890
- });
1891
-
1892
1734
  test('does not inherit Pro or Pro model fallback ranges for Protocol V2 devices', () => {
1893
1735
  const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
1894
1736
  const checkedTypes: string[] = [];
@@ -2007,68 +1849,38 @@ describe('Protocol V2 feature adapter', () => {
2007
1849
  });
2008
1850
  });
2009
1851
 
2010
- test('does not call V1 OnekeyGetFeatures for Protocol V2 devices', async () => {
1852
+ test('rejects legacy getOnekeyFeatures for Protocol V2 devices', async () => {
2011
1853
  const method = new GetOnekeyFeatures({
2012
1854
  id: 1,
2013
1855
  payload: {
2014
1856
  method: 'getOnekeyFeatures',
2015
1857
  },
2016
1858
  });
2017
- const typedCall = jest.fn().mockResolvedValue({
2018
- type: 'DeviceInfo',
2019
- message: {
2020
- protocol_version: 1,
2021
- hw: { serial_no: 'PR2SERIAL' },
2022
- fw: { application: { version: '1.2.3', build_id: 'app-build' } },
2023
- coprocessor: { bt_adv_name: 'Pro2 BLE' },
2024
- status: { init_states: true },
2025
- },
2026
- });
1859
+ const typedCall = jest.fn();
2027
1860
 
2028
1861
  (method as any).device = stubDevice({
2029
1862
  originalDescriptor: { protocolType: 'V2' },
2030
1863
  commands: { typedCall },
2031
1864
  });
2032
1865
 
2033
- const message = await method.run();
2034
-
2035
- // V2 走 DeviceInfoGet 完整请求(含 SE 与 hash/build_id),而不是 V1 OnekeyGetFeatures
2036
- expect(typedCall).toHaveBeenCalledTimes(1);
2037
- expect(typedCall).toHaveBeenCalledWith(
2038
- 'DeviceInfoGet',
2039
- 'DeviceInfo',
2040
- expect.objectContaining({
2041
- targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }),
2042
- types: expect.objectContaining({ build_id: true, hash: true }),
2043
- }),
2044
- expect.anything()
2045
- );
2046
- expect(message).toEqual({ onekey_device_type: 'PRO2' });
2047
- expect(message).not.toHaveProperty('label');
1866
+ await expect(method.run()).rejects.toMatchObject({
1867
+ errorCode: expect.any(Number),
1868
+ });
1869
+ expect(typedCall).not.toHaveBeenCalled();
2048
1870
  });
2049
1871
 
2050
- test('reuses cached Protocol V2 features on later runs without polling device status', async () => {
1872
+ test('initializes and reuses Protocol V2 features from DeviceInfo only', async () => {
2051
1873
  const device = Device.fromDescriptor({
2052
1874
  path: 'usb-path',
2053
1875
  protocolType: 'V2',
2054
1876
  } as any);
2055
- const typedCall = jest
2056
- .fn()
2057
- .mockResolvedValueOnce({
2058
- type: 'DeviceInfo',
2059
- message: {
2060
- hw: { serial_no: 'PR2SERIAL' },
2061
- fw: { application: { version: '1.2.3' } },
2062
- },
2063
- })
2064
- .mockResolvedValueOnce({
2065
- type: 'DeviceStatus',
2066
- message: {
2067
- device_id: 'PRO2-REFRESH-ID',
2068
- unlocked: true,
2069
- passphrase_enabled: true,
2070
- },
2071
- });
1877
+ const typedCall = jest.fn().mockResolvedValueOnce({
1878
+ type: 'DeviceInfo',
1879
+ message: {
1880
+ hw: { serial_no: 'PR2SERIAL' },
1881
+ fw: { application: { version: '1.2.3' } },
1882
+ },
1883
+ });
2072
1884
 
2073
1885
  (device as any).commands = { typedCall };
2074
1886
 
@@ -2076,18 +1888,17 @@ describe('Protocol V2 feature adapter', () => {
2076
1888
  await device.initialize();
2077
1889
 
2078
1890
  expect(device.features).toMatchObject({
2079
- deviceId: 'PRO2-REFRESH-ID',
1891
+ deviceId: null,
2080
1892
  firmwareVersion: '1.2.3',
2081
- passphraseProtection: true,
1893
+ passphraseProtection: null,
2082
1894
  label: null,
2083
1895
  });
2084
1896
  expect((device as any).profile).toBeUndefined();
2085
- expect(device.features?.passphraseProtection).toBe(true);
1897
+ expect(device.features?.passphraseProtection).toBeNull();
2086
1898
  expect(device.features?.label).toBeNull();
2087
1899
  expect(device.features?.firmwareVersion).toBe('1.2.3');
2088
- expect(typedCall).toHaveBeenCalledTimes(2);
2089
- expect(typedCall).toHaveBeenNthCalledWith(
2090
- 1,
1900
+ expect(typedCall).toHaveBeenCalledTimes(1);
1901
+ expect(typedCall).toHaveBeenCalledWith(
2091
1902
  'DeviceInfoGet',
2092
1903
  'DeviceInfo',
2093
1904
  {
@@ -2105,10 +1916,9 @@ describe('Protocol V2 feature adapter', () => {
2105
1916
  timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
2106
1917
  }
2107
1918
  );
2108
- expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceStatusGet', 'DeviceStatus', {});
2109
1919
  });
2110
1920
 
2111
- test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
1921
+ test('recognizes bootloader DeviceInfo without requesting device status', async () => {
2112
1922
  const device = Device.fromDescriptor({
2113
1923
  path: 'usb-path',
2114
1924
  protocolType: 'V2',
@@ -2119,23 +1929,124 @@ describe('Protocol V2 feature adapter', () => {
2119
1929
  type: 'DeviceInfo',
2120
1930
  message: {
2121
1931
  hw: { serial_no: 'PR2SERIAL' },
2122
- fw: { application: { version: '1.2.3' } },
1932
+ fw: { bootloader: { version: '0.2.0' } },
2123
1933
  },
2124
1934
  })
2125
1935
  .mockResolvedValueOnce({
2126
1936
  type: 'DeviceStatus',
2127
- message: { init_states: true },
2128
- })
1937
+ message: {
1938
+ device_id: 'UNEXPECTED-SEPARATE-STATUS',
1939
+ },
1940
+ });
1941
+
1942
+ (device as any).commands = { typedCall };
1943
+
1944
+ await device.initialize();
1945
+
1946
+ expect(typedCall).toHaveBeenCalledTimes(1);
1947
+ expect(typedCall).toHaveBeenCalledWith(
1948
+ 'DeviceInfoGet',
1949
+ 'DeviceInfo',
1950
+ expect.not.objectContaining({
1951
+ targets: expect.objectContaining({ status: true }),
1952
+ }),
1953
+ expect.anything()
1954
+ );
1955
+ expect(device.features).toMatchObject({
1956
+ deviceId: null,
1957
+ bootloaderMode: true,
1958
+ initialized: null,
1959
+ });
1960
+ });
1961
+
1962
+ test('does not probe DeviceStatusGet while firmware device is still in bootloader mode', async () => {
1963
+ const method = new FirmwareUpdateV4({
1964
+ id: 1,
1965
+ payload: {
1966
+ method: 'firmwareUpdateV4',
1967
+ },
1968
+ });
1969
+ const typedCall = jest.fn().mockImplementation((request: string) => {
1970
+ if (request === 'DeviceStatusGet') {
1971
+ throw new Error('bootloader must not receive DeviceStatusGet');
1972
+ }
1973
+ return Promise.resolve({
1974
+ type: 'DeviceInfo',
1975
+ message: {
1976
+ hw: { serial_no: 'PR2BOOT' },
1977
+ fw: { bootloader: { version: '0.2.0' } },
1978
+ se1: {},
1979
+ },
1980
+ });
1981
+ });
1982
+ (method as any).device = stubDevice({
1983
+ getCommands: () => ({ typedCall }),
1984
+ updateProtocolV2Features: jest.fn(),
1985
+ });
1986
+
1987
+ await expect((method as any).probeProtocolV2NormalMode()).resolves.toBe(false);
1988
+ expect(typedCall).toHaveBeenCalledTimes(1);
1989
+ expect(typedCall).toHaveBeenCalledWith(
1990
+ 'DeviceInfoGet',
1991
+ 'DeviceInfo',
1992
+ expect.not.objectContaining({
1993
+ targets: expect.objectContaining({ status: true }),
1994
+ }),
1995
+ expect.anything()
1996
+ );
1997
+ });
1998
+
1999
+ test('does not probe DeviceStatusGet while firmware device is still in romloader mode', async () => {
2000
+ const method = new FirmwareUpdateV4({
2001
+ id: 1,
2002
+ payload: {
2003
+ method: 'firmwareUpdateV4',
2004
+ },
2005
+ });
2006
+ const typedCall = jest.fn().mockImplementation((request: string) => {
2007
+ if (request === 'DeviceStatusGet') {
2008
+ throw new Error('romloader must not receive DeviceStatusGet');
2009
+ }
2010
+ return Promise.resolve({
2011
+ type: 'DeviceInfo',
2012
+ message: {
2013
+ hw: { serial_no: 'PR2ROM' },
2014
+ fw: {
2015
+ romloader: { version: '0.1.0' },
2016
+ bootloader: { version: '0.2.0' },
2017
+ },
2018
+ },
2019
+ });
2020
+ });
2021
+ (method as any).device = stubDevice({
2022
+ getCommands: () => ({ typedCall }),
2023
+ updateProtocolV2Features: jest.fn(),
2024
+ });
2025
+
2026
+ await expect((method as any).probeProtocolV2NormalMode()).resolves.toBe(false);
2027
+ expect(typedCall).toHaveBeenCalledTimes(1);
2028
+ });
2029
+
2030
+ test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
2031
+ const device = Device.fromDescriptor({
2032
+ path: 'usb-path',
2033
+ protocolType: 'V2',
2034
+ } as any);
2035
+ const typedCall = jest
2036
+ .fn()
2129
2037
  .mockResolvedValueOnce({
2130
2038
  type: 'DeviceInfo',
2131
2039
  message: {
2132
2040
  hw: { serial_no: 'PR2SERIAL' },
2133
- fw: { application: { version: '1.2.4' } },
2041
+ fw: { application: { version: '1.2.3' } },
2134
2042
  },
2135
2043
  })
2136
2044
  .mockResolvedValueOnce({
2137
- type: 'DeviceStatus',
2138
- message: { init_states: true, unlocked: true, passphrase_enabled: true },
2045
+ type: 'DeviceInfo',
2046
+ message: {
2047
+ hw: { serial_no: 'PR2SERIAL' },
2048
+ fw: { application: { version: '1.2.4' } },
2049
+ },
2139
2050
  });
2140
2051
 
2141
2052
  (device as any).commands = { typedCall };
@@ -2146,17 +2057,17 @@ describe('Protocol V2 feature adapter', () => {
2146
2057
  expect(device.features).toMatchObject({
2147
2058
  deviceId: null,
2148
2059
  firmwareVersion: '1.2.4',
2149
- passphraseProtection: true,
2060
+ passphraseProtection: null,
2150
2061
  });
2151
2062
  expect(features).toMatchObject({
2152
2063
  deviceType: 'pro2',
2153
2064
  serialNo: 'PR2SERIAL',
2154
2065
  firmwareVersion: '1.2.4',
2155
- passphraseProtection: true,
2066
+ passphraseProtection: null,
2156
2067
  });
2157
- expect(typedCall).toHaveBeenCalledTimes(4);
2068
+ expect(typedCall).toHaveBeenCalledTimes(2);
2158
2069
  expect(typedCall).toHaveBeenNthCalledWith(
2159
- 3,
2070
+ 2,
2160
2071
  'DeviceInfoGet',
2161
2072
  'DeviceInfo',
2162
2073
  {
@@ -2231,16 +2142,6 @@ describe('Protocol V2 feature adapter', () => {
2231
2142
  message: { message: 'ok' },
2232
2143
  };
2233
2144
  }
2234
- if (requestType === 'DeviceStatusGet') {
2235
- return {
2236
- type: 'DeviceStatus',
2237
- message: {
2238
- unlocked: true,
2239
- unlocked_by_attach_to_pin: true,
2240
- passphrase_enabled: true,
2241
- },
2242
- };
2243
- }
2244
2145
  throw new Error(`Unexpected request: ${requestType}`);
2245
2146
  });
2246
2147
 
@@ -2260,7 +2161,6 @@ describe('Protocol V2 feature adapter', () => {
2260
2161
 
2261
2162
  expect(typedCall.mock.calls).toEqual([
2262
2163
  ['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
2263
- ['DeviceStatusGet', 'DeviceStatus', {}],
2264
2164
  ]);
2265
2165
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
2266
2166
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
@@ -2268,17 +2168,10 @@ describe('Protocol V2 feature adapter', () => {
2268
2168
  deviceType: 'pro2',
2269
2169
  firmwareVersion: '1.2.3',
2270
2170
  unlocked: true,
2271
- unlockedAttachPin: true,
2272
- passphraseProtection: true,
2273
- });
2274
- expect(features.raw?.protocolV2DeviceStatus).toMatchObject({
2275
- unlocked: true,
2276
- unlocked_by_attach_to_pin: true,
2277
- passphrase_enabled: true,
2278
2171
  });
2279
2172
  });
2280
2173
 
2281
- test('syncs Protocol V2 features passphrase state from DeviceStatus after unlock', async () => {
2174
+ test('preserves cached Protocol V2 passphrase state without polling status after unlock', async () => {
2282
2175
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
2283
2176
  (device as any).features = normalizeProtocolV2Features(
2284
2177
  { ...descriptor, protocolType: 'V2' } as any,
@@ -2295,16 +2188,6 @@ describe('Protocol V2 feature adapter', () => {
2295
2188
  message: { message: 'ok' },
2296
2189
  };
2297
2190
  }
2298
- if (requestType === 'DeviceStatusGet') {
2299
- return {
2300
- type: 'DeviceStatus',
2301
- message: {
2302
- unlocked: true,
2303
- unlocked_by_attach_to_pin: true,
2304
- passphrase_enabled: true,
2305
- },
2306
- };
2307
- }
2308
2191
  throw new Error(`Unexpected request: ${requestType}`);
2309
2192
  });
2310
2193
  (device as any).commands = { typedCall };
@@ -2313,12 +2196,11 @@ describe('Protocol V2 feature adapter', () => {
2313
2196
 
2314
2197
  expect(typedCall.mock.calls).toEqual([
2315
2198
  ['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
2316
- ['DeviceStatusGet', 'DeviceStatus', {}],
2317
2199
  ]);
2318
2200
  expect((device as any).profile).toBeUndefined();
2319
2201
  expect(device.features?.unlocked).toBe(true);
2320
- expect(device.features?.passphraseProtection).toBe(true);
2321
- expect(device.features?.unlockedAttachPin).toBe(true);
2202
+ expect(device.features?.passphraseProtection).toBeNull();
2203
+ expect(device.features?.unlockedAttachPin).toBeUndefined();
2322
2204
  });
2323
2205
 
2324
2206
  test('maps unsupported Protocol V2 DeviceSessionAskPin to DeviceNotSupportMethod', async () => {
@@ -2934,6 +2816,29 @@ describe('Protocol V2 firmware update targets', () => {
2934
2816
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2935
2817
  });
2936
2818
 
2819
+ test('keeps Protocol V2 romloader active before firmware transfer', async () => {
2820
+ const method = new FirmwareUpdateV4({
2821
+ id: 1,
2822
+ payload: {
2823
+ method: 'firmwareUpdateV4',
2824
+ },
2825
+ });
2826
+ (method as any).device = stubDevice({
2827
+ originalDescriptor: { id: 'usb-id', path: 'romloader-path', protocolType: 'V2' },
2828
+ features: { mode: 'romloader', bootloaderMode: false, capabilities: [] },
2829
+ isBootloader: () => false,
2830
+ });
2831
+ (method as any).protocolV2Reboot = jest
2832
+ .fn()
2833
+ .mockRejectedValue(new Error('romloader rejected Bootloader reboot type'));
2834
+ method.postTipMessage = jest.fn();
2835
+
2836
+ await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
2837
+
2838
+ expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
2839
+ expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
2840
+ });
2841
+
2937
2842
  test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
2938
2843
  const method = new FirmwareUpdateV4({
2939
2844
  id: 1,
@@ -3147,7 +3052,10 @@ describe('Protocol V2 firmware update targets', () => {
3147
3052
  if (name === 'DeviceInfoGet') {
3148
3053
  return Promise.resolve({
3149
3054
  type: 'DeviceInfo',
3150
- message: { hw: { serial_no: 'PR9999999999' } },
3055
+ message: {
3056
+ hw: { serial_no: 'PR9999999999' },
3057
+ fw: { application: { version: '9.9.9' } },
3058
+ },
3151
3059
  });
3152
3060
  }
3153
3061
  return Promise.reject(new Error(`unexpected call ${name}`));
@@ -3172,9 +3080,10 @@ describe('Protocol V2 firmware update targets', () => {
3172
3080
 
3173
3081
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
3174
3082
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
3175
- 'DeviceStatusGet',
3083
+ 'DeviceInfoGet',
3176
3084
  'DeviceStatusGet',
3177
3085
  'DeviceInfoGet',
3086
+ 'DeviceStatusGet',
3178
3087
  ]);
3179
3088
  });
3180
3089
 
@@ -3199,7 +3108,10 @@ describe('Protocol V2 firmware update targets', () => {
3199
3108
  if (name === 'DeviceInfoGet') {
3200
3109
  return Promise.resolve({
3201
3110
  type: 'DeviceInfo',
3202
- message: { hw: { serial_no: 'PR9999999999' } },
3111
+ message: {
3112
+ hw: { serial_no: 'PR9999999999' },
3113
+ fw: { application: { version: '9.9.9' } },
3114
+ },
3203
3115
  });
3204
3116
  }
3205
3117
  return Promise.reject(new Error(`unexpected call ${name}`));
@@ -3224,9 +3136,10 @@ describe('Protocol V2 firmware update targets', () => {
3224
3136
 
3225
3137
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
3226
3138
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
3227
- 'DeviceStatusGet',
3139
+ 'DeviceInfoGet',
3228
3140
  'DeviceStatusGet',
3229
3141
  'DeviceInfoGet',
3142
+ 'DeviceStatusGet',
3230
3143
  ]);
3231
3144
  });
3232
3145
 
@@ -3299,7 +3212,7 @@ describe('Protocol V2 firmware update targets', () => {
3299
3212
  expect.objectContaining({ hw: { serial_no: 'PR9999999999' } }),
3300
3213
  expect.objectContaining({ device_id: 'PRO2-DEVICE-ID' })
3301
3214
  );
3302
- expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceStatusGet', 'DeviceInfoGet']);
3215
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet', 'DeviceStatusGet']);
3303
3216
  expect(typedCall).not.toHaveBeenCalledWith(
3304
3217
  'DeviceFirmwareUpdateStatusGet',
3305
3218
  expect.anything(),
@@ -4510,13 +4423,15 @@ describe('Protocol V2 reboot methods', () => {
4510
4423
  },
4511
4424
  });
4512
4425
  method.init();
4513
- (method as any).device = stubDevice({ commands: { typedCall } });
4426
+ const device = stubDevice({ commands: { typedCall } });
4427
+ (method as any).device = device;
4514
4428
 
4515
4429
  await method.run();
4516
4430
 
4517
4431
  expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
4518
4432
  reboot_type: 2,
4519
4433
  });
4434
+ expect(device.markProtocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Bootloader);
4520
4435
  });
4521
4436
  });
4522
4437
 
@@ -4557,6 +4472,20 @@ describe('Protocol V2 protected method execution', () => {
4557
4472
  expect(method.unlockPolicy).toBe('none');
4558
4473
  });
4559
4474
 
4475
+ test.each([
4476
+ ['deviceInfoGet', DeviceInfoGet],
4477
+ ['deviceStatusGet', DeviceStatusGet],
4478
+ ['deviceSettingsGet', DeviceSettingsGet],
4479
+ ])('does not auto-unlock the read-only %s method', (_name, MethodClass) => {
4480
+ const method = new MethodClass({
4481
+ id: 1,
4482
+ payload: { method: _name },
4483
+ } as any);
4484
+ method.init();
4485
+
4486
+ expect(method.unlockPolicy).toBe('none');
4487
+ });
4488
+
4560
4489
  test('unlocks and retries an opted-in Protocol V2 method once', async () => {
4561
4490
  const calls: string[] = [];
4562
4491
  const method = {
@@ -5045,9 +4974,19 @@ describe('Protocol V2 current low-level methods', () => {
5045
4974
  });
5046
4975
  });
5047
4976
 
5048
- test('marks Protocol V2 settings methods for unlock-on-locked retry', () => {
4977
+ test('does not unlock before reading Protocol V2 device settings', () => {
4978
+ const method = new DeviceSettingsGet({
4979
+ id: 1,
4980
+ payload: { method: 'deviceSettingsGet' },
4981
+ });
4982
+
4983
+ method.init();
4984
+
4985
+ expect(method.unlockPolicy).toBe('none');
4986
+ });
4987
+
4988
+ test('marks Protocol V2 settings mutations for unlock-on-locked retry', () => {
5049
4989
  const methods = [
5050
- new DeviceSettingsGet({ id: 1, payload: { method: 'deviceSettingsGet' } }),
5051
4990
  new DeviceSettingsSet({
5052
4991
  id: 2,
5053
4992
  payload: { method: 'deviceSettingsSet', settings: { brightness: 80 } },
@@ -5139,6 +5078,12 @@ describe('Protocol V2 raw device info method', () => {
5139
5078
  });
5140
5079
 
5141
5080
  test('rejects removed target/type names before transport call', () => {
5081
+ expect(() =>
5082
+ buildMethod({
5083
+ targets: { hw: true, status: true },
5084
+ })
5085
+ ).toThrow('targets');
5086
+
5142
5087
  expect(() =>
5143
5088
  buildMethod({
5144
5089
  targets: { hw: true, bt: true },