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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/__tests__/DeviceCommands.test.ts +87 -2
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +1 -1
  5. package/__tests__/device-wallet-session-store.test.ts +111 -7
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  7. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  8. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  9. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  10. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +200 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  12. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  13. package/__tests__/open-wallet-session.test.ts +253 -13
  14. package/__tests__/pro2Wallpaper.test.ts +7 -18
  15. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  16. package/__tests__/protocol-v2.test.ts +415 -145
  17. package/__tests__/public-device-state-api.test.ts +69 -62
  18. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  19. package/dist/api/BaseMethod.d.ts +2 -1
  20. package/dist/api/BaseMethod.d.ts.map +1 -1
  21. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  25. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  31. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  32. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  33. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  34. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  35. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  36. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  37. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  38. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  39. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  40. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  41. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  42. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  43. package/dist/api/firmware/FirmwareArtifactSource.d.ts +26 -0
  44. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  45. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  46. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  47. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  48. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  49. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  50. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +24 -0
  52. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  53. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +25 -0
  54. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  55. package/dist/api/firmware/getBinary.d.ts +7 -0
  56. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  57. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  58. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  59. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  60. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  61. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  62. package/dist/api/index.d.ts +0 -17
  63. package/dist/api/index.d.ts.map +1 -1
  64. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  65. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  66. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  67. package/dist/data-manager/DataManager.d.ts +1 -0
  68. package/dist/data-manager/DataManager.d.ts.map +1 -1
  69. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  70. package/dist/device/Device.d.ts.map +1 -1
  71. package/dist/device/DeviceCommands.d.ts.map +1 -1
  72. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  73. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  74. package/dist/events/logBlockEvent.d.ts.map +1 -1
  75. package/dist/index.d.ts +265 -226
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3045 -1550
  78. package/dist/inject.d.ts.map +1 -1
  79. package/dist/lowLevelInject.d.ts.map +1 -1
  80. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  81. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  82. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  83. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  84. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  85. package/dist/topLevelInject.d.ts.map +1 -1
  86. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  87. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  88. package/dist/types/api/deviceUnlock.d.ts +2 -2
  89. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  90. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  91. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  92. package/dist/types/api/export.d.ts +4 -1
  93. package/dist/types/api/export.d.ts.map +1 -1
  94. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  95. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  96. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  97. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  98. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  99. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  100. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  101. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  102. package/dist/types/api/index.d.ts +10 -19
  103. package/dist/types/api/index.d.ts.map +1 -1
  104. package/dist/types/api/openWalletSession.d.ts +1 -0
  105. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  106. package/dist/types/api/protocolV2.d.ts +3 -76
  107. package/dist/types/api/protocolV2.d.ts.map +1 -1
  108. package/dist/types/api/sessionCache.d.ts +4 -1
  109. package/dist/types/api/sessionCache.d.ts.map +1 -1
  110. package/dist/types/settings.d.ts +13 -0
  111. package/dist/types/settings.d.ts.map +1 -1
  112. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  113. package/dist/utils/patch.d.ts +1 -1
  114. package/dist/utils/patch.d.ts.map +1 -1
  115. package/package.json +4 -4
  116. package/src/api/BaseMethod.ts +8 -3
  117. package/src/api/CheckAllFirmwareRelease.ts +16 -3
  118. package/src/api/ClearSessionCache.ts +4 -0
  119. package/src/api/FirmwareUpdate.ts +8 -1
  120. package/src/api/FirmwareUpdateV2.ts +284 -119
  121. package/src/api/FirmwareUpdateV3.ts +248 -55
  122. package/src/api/FirmwareUpdateV4.ts +816 -328
  123. package/src/api/OpenWalletSession.ts +74 -25
  124. package/src/api/UploadPortfolio.ts +1 -1
  125. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  126. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  127. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  128. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  129. package/src/api/device/DeviceChangePin.ts +1 -1
  130. package/src/api/device/DeviceUnlock.ts +1 -2
  131. package/src/api/device/DeviceUpdateBootloader.ts +114 -6
  132. package/src/api/device/DeviceWipe.ts +1 -1
  133. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  134. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  135. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  136. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  137. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  138. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +378 -0
  139. package/src/api/firmware/updateBootloader.ts +19 -4
  140. package/src/api/firmware/uploadFirmware.ts +140 -22
  141. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  142. package/src/api/index.ts +0 -18
  143. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  144. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  145. package/src/data/messages/messages-protocol-v2.json +77 -4
  146. package/src/data-manager/DataManager.ts +33 -33
  147. package/src/data-manager/connectSettings.ts +11 -0
  148. package/src/device/Device.ts +50 -60
  149. package/src/device/DeviceCommands.ts +58 -14
  150. package/src/device/DeviceStateMapper.ts +18 -1
  151. package/src/device/DeviceStateProjector.ts +7 -1
  152. package/src/events/logBlockEvent.ts +1 -0
  153. package/src/index.ts +5 -0
  154. package/src/inject.ts +22 -25
  155. package/src/lowLevelInject.ts +5 -1
  156. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  157. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -2
  158. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  159. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  160. package/src/protocols/protocol-v2/walletSession.ts +68 -12
  161. package/src/topLevelInject.ts +5 -1
  162. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  163. package/src/types/api/deviceUnlock.ts +2 -5
  164. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  165. package/src/types/api/export.ts +18 -0
  166. package/src/types/api/firmwareUpdate.ts +76 -0
  167. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  168. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  169. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  170. package/src/types/api/index.ts +14 -34
  171. package/src/types/api/openWalletSession.ts +2 -0
  172. package/src/types/api/protocolV2.ts +3 -156
  173. package/src/types/api/sessionCache.ts +9 -4
  174. package/src/types/settings.ts +13 -0
  175. package/src/utils/deviceFeaturesUtils.ts +5 -1
@@ -1,4 +1,4 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import { DeviceRebootType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
@@ -83,6 +83,7 @@ import {
83
83
  isMethodVersionRangeUnsupported,
84
84
  } from '../src/utils';
85
85
  import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
86
+ import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
86
87
  import {
87
88
  getPassphraseState,
88
89
  getPassphraseStateWithRefreshDeviceInfo,
@@ -122,7 +123,7 @@ describe('DeviceUploadWallpaper', () => {
122
123
  const result = await method.run();
123
124
 
124
125
  expect(method.requireProtocolV2).toBe(true);
125
- expect(method.unlockPolicy).toBe('retry-on-locked');
126
+ expect(method.unlockPolicy).toBe('unlock-before-run');
126
127
  expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
127
128
  path: 'vol1:/wallpapers',
128
129
  });
@@ -211,7 +212,7 @@ describe('UploadPortfolio', () => {
211
212
  method.init();
212
213
  const result = await method.run();
213
214
 
214
- expect(method.unlockPolicy).toBe('retry-on-locked');
215
+ expect(method.unlockPolicy).toBe('unlock-before-run');
215
216
  expect(method.protocolV2UiMode).toBe('none');
216
217
  expect(method.protocolV2UiInteraction).toBeUndefined();
217
218
  expect(method.payload.emitProgress).toBe(false);
@@ -711,7 +712,9 @@ describe('Protocol V2 feature adapter', () => {
711
712
  unlockedAttachPin: false,
712
713
  resumed: false,
713
714
  });
714
- expect(typedCall).not.toHaveBeenCalled();
715
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
716
+ eventless_wallet_session: true,
717
+ });
715
718
  expect(device.getInternalState()).toBeUndefined();
716
719
  });
717
720
 
@@ -721,7 +724,13 @@ describe('Protocol V2 feature adapter', () => {
721
724
  { ...descriptor, protocolType: 'V2' } as any,
722
725
  { status: { passphrase_enabled: false, unlocked: true } }
723
726
  );
724
- const typedCall = jest.fn();
727
+ const typedCall = jest.fn().mockResolvedValue({
728
+ type: 'DeviceSession',
729
+ message: {
730
+ btc_test_address: 'main-wallet-state',
731
+ session_id: 'main-wallet-session',
732
+ },
733
+ });
725
734
  const promptPassphrase = jest
726
735
  .fn()
727
736
  .mockResolvedValue({ passphrase: 'hidden-wallet-with-disabled-flag' });
@@ -735,7 +744,9 @@ describe('Protocol V2 feature adapter', () => {
735
744
  });
736
745
 
737
746
  expect(promptPassphrase).not.toHaveBeenCalled();
738
- expect(typedCall).not.toHaveBeenCalled();
747
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
748
+ eventless_wallet_session: true,
749
+ });
739
750
  });
740
751
 
741
752
  test('preserves cached settings only while Protocol V2 physical identity stays the same', () => {
@@ -790,13 +801,8 @@ describe('Protocol V2 feature adapter', () => {
790
801
  }
791
802
  );
792
803
  const typedCall = jest.fn().mockImplementation((request: string) => {
793
- if (request === 'DeviceSessionGet') {
794
- return Promise.resolve({
795
- message: {
796
- btc_test_address: 'state-after-unlock',
797
- session_id: 'session-after-unlock',
798
- },
799
- });
804
+ if (request === 'ProtocolInfoRequest') {
805
+ return Promise.resolve({ message: { version: 2 } });
800
806
  }
801
807
  if (request === 'DeviceStatusGet') {
802
808
  return Promise.resolve({
@@ -825,7 +831,9 @@ describe('Protocol V2 feature adapter', () => {
825
831
 
826
832
  expect(unlockDevice).toHaveBeenCalledTimes(1);
827
833
  expect(promptPassphrase).not.toHaveBeenCalled();
828
- expect(typedCall).not.toHaveBeenCalled();
834
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
835
+ eventless_wallet_session: true,
836
+ });
829
837
  });
830
838
 
831
839
  test('onlyMainPin takes precedence over a cached hidden-wallet state', async () => {
@@ -836,14 +844,28 @@ describe('Protocol V2 feature adapter', () => {
836
844
  );
837
845
  device.passphraseState = 'hidden-state';
838
846
  preloadSessionCache('main-wallet-device', 'hidden-state', 'hidden-session');
839
- const typedCall = jest.fn();
847
+ const typedCall = jest.fn().mockResolvedValue({
848
+ type: 'DeviceSession',
849
+ message: {
850
+ btc_test_address: 'main-wallet-state',
851
+ session_id: 'main-wallet-session',
852
+ },
853
+ });
840
854
  (device as any).commands = { typedCall };
841
855
 
842
- await getPassphraseState(device, {
843
- onlyMainPin: true,
856
+ await expect(
857
+ getPassphraseState(device, {
858
+ onlyMainPin: true,
859
+ })
860
+ ).resolves.toMatchObject({
861
+ passphraseState: undefined,
862
+ newSession: undefined,
844
863
  });
845
- expect(typedCall).not.toHaveBeenCalled();
846
- expect(device.getInternalState()).toBe('hidden-session');
864
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
865
+ eventless_wallet_session: true,
866
+ });
867
+ expect(device.passphraseState).toBeUndefined();
868
+ expect(device.getInternalState()).toBeUndefined();
847
869
  });
848
870
 
849
871
  test('gets the wallet session selected by the Protocol V2 device', async () => {
@@ -867,7 +889,7 @@ describe('Protocol V2 feature adapter', () => {
867
889
  type: 'DeviceSession',
868
890
  message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
869
891
  });
870
- const promptPassphrase = jest.fn();
892
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden secret' });
871
893
  (device as any).commands = { typedCall, promptPassphrase };
872
894
 
873
895
  await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
@@ -875,8 +897,10 @@ describe('Protocol V2 feature adapter', () => {
875
897
  passphraseState: 'hidden-state',
876
898
  });
877
899
 
878
- expect(promptPassphrase).not.toHaveBeenCalled();
879
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
900
+ expect(promptPassphrase).toHaveBeenCalled();
901
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
902
+ select: { host_passphrase: { passphrase: 'hidden secret' } },
903
+ });
880
904
  });
881
905
 
882
906
  test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
@@ -980,8 +1004,8 @@ describe('Protocol V2 feature adapter', () => {
980
1004
  newSession: 'session-b',
981
1005
  });
982
1006
 
983
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
984
- session_id: 'session-a',
1007
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
1008
+ resume: { session_id: 'session-a' },
985
1009
  });
986
1010
  expect(device.getInternalState()).toBe('session-b');
987
1011
  });
@@ -999,13 +1023,18 @@ describe('Protocol V2 feature adapter', () => {
999
1023
  preloadSessionCache('stable-device-2', 'state-a', 'session-a');
1000
1024
  const typedCall = jest.fn().mockResolvedValue({
1001
1025
  type: 'DeviceSession',
1002
- message: { session_id: 'main-session' },
1026
+ message: {
1027
+ session_id: 'main-session',
1028
+ btc_test_address: 'main-wallet-state',
1029
+ },
1003
1030
  });
1004
1031
  (device as any).commands = { typedCall };
1005
1032
 
1006
1033
  await getProtocolV2WalletSession(device, { onlyMainPin: true });
1007
1034
 
1008
- expect(typedCall).not.toHaveBeenCalled();
1035
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1036
+ eventless_wallet_session: true,
1037
+ });
1009
1038
  expect(device.getInternalState()).toBeUndefined();
1010
1039
  });
1011
1040
 
@@ -1027,11 +1056,14 @@ describe('Protocol V2 feature adapter', () => {
1027
1056
  );
1028
1057
  device.passphraseState = 'state-a';
1029
1058
  preloadSessionCache('stable-device-3', 'state-a', 'session-a');
1030
- const typedCall = jest.fn().mockRejectedValue(
1031
- Object.assign(new Error('SE request failed'), {
1032
- errorCode: HardwareErrorCode.RuntimeError,
1033
- })
1034
- );
1059
+ const typedCall = jest
1060
+ .fn()
1061
+ .mockResolvedValueOnce({ message: { version: 2 } })
1062
+ .mockRejectedValue(
1063
+ Object.assign(new Error('SE request failed'), {
1064
+ errorCode: HardwareErrorCode.RuntimeError,
1065
+ })
1066
+ );
1035
1067
  const promptPassphrase = jest.fn();
1036
1068
  (device as any).commands = { typedCall, promptPassphrase };
1037
1069
 
@@ -1039,7 +1071,8 @@ describe('Protocol V2 feature adapter', () => {
1039
1071
  errorCode: HardwareErrorCode.RuntimeError,
1040
1072
  });
1041
1073
  expect(typedCall.mock.calls).toEqual([
1042
- ['DeviceSessionGet', 'DeviceSession', { session_id: 'session-a' }],
1074
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1075
+ ['DeviceSessionOpen', 'DeviceSession', { resume: { session_id: 'session-a' } }],
1043
1076
  ]);
1044
1077
  expect(promptPassphrase).not.toHaveBeenCalled();
1045
1078
  expect(device.getInternalState()).toBeUndefined();
@@ -1061,11 +1094,14 @@ describe('Protocol V2 feature adapter', () => {
1061
1094
  },
1062
1095
  }
1063
1096
  );
1064
- const typedCall = jest.fn().mockRejectedValue(
1065
- Object.assign(new Error('Invalid session'), {
1066
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1067
- })
1068
- );
1097
+ const typedCall = jest
1098
+ .fn()
1099
+ .mockResolvedValueOnce({ message: { version: 2 } })
1100
+ .mockRejectedValue(
1101
+ Object.assign(new Error('Invalid session'), {
1102
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
1103
+ })
1104
+ );
1069
1105
  (device as any).commands = {
1070
1106
  typedCall,
1071
1107
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'new-secret' }),
@@ -1074,7 +1110,14 @@ describe('Protocol V2 feature adapter', () => {
1074
1110
  await expect(getProtocolV2WalletSession(device)).rejects.toMatchObject({
1075
1111
  errorCode: HardwareErrorCode.WalletSessionInvalid,
1076
1112
  });
1077
- expect(typedCall.mock.calls).toEqual([['DeviceSessionGet', 'DeviceSession', {}]]);
1113
+ expect(typedCall.mock.calls).toEqual([
1114
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1115
+ [
1116
+ 'DeviceSessionOpen',
1117
+ 'DeviceSession',
1118
+ { select: { host_passphrase: { passphrase: 'new-secret' } } },
1119
+ ],
1120
+ ]);
1078
1121
  });
1079
1122
 
1080
1123
  test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
@@ -1112,7 +1155,7 @@ describe('Protocol V2 feature adapter', () => {
1112
1155
  expect(device.getInternalState()).toBeUndefined();
1113
1156
  });
1114
1157
 
1115
- test('unlocks and retries DeviceSessionGet when wallet selection is locked', async () => {
1158
+ test('unlocks and retries DeviceSessionOpen when wallet selection is locked', async () => {
1116
1159
  const device = Device.fromDescriptor({
1117
1160
  id: 'cache-device-4',
1118
1161
  path: 'cache-path-4',
@@ -1127,6 +1170,7 @@ describe('Protocol V2 feature adapter', () => {
1127
1170
  });
1128
1171
  const typedCall = jest
1129
1172
  .fn()
1173
+ .mockResolvedValueOnce({ message: { version: 2 } })
1130
1174
  .mockRejectedValueOnce(lockedError)
1131
1175
  .mockResolvedValueOnce({
1132
1176
  message: {
@@ -1145,7 +1189,7 @@ describe('Protocol V2 feature adapter', () => {
1145
1189
  passphraseState: 'state-after-unlock',
1146
1190
  });
1147
1191
  expect(unlockDevice).toHaveBeenCalledTimes(1);
1148
- expect(typedCall).toHaveBeenCalledTimes(2);
1192
+ expect(typedCall).toHaveBeenCalledTimes(3);
1149
1193
  });
1150
1194
 
1151
1195
  test('does not request a Pro2 wallet session before features are initialized', async () => {
@@ -1262,6 +1306,7 @@ describe('Protocol V2 feature adapter', () => {
1262
1306
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-secret' }),
1263
1307
  },
1264
1308
  updateInternalState,
1309
+ getInternalState: () => 'feature-session',
1265
1310
  getCurrentDeviceId: () => 'pro-device-id',
1266
1311
  getCurrentPassphraseProtection: () => true,
1267
1312
  }) as any;
@@ -1472,7 +1517,9 @@ describe('Protocol V2 feature adapter', () => {
1472
1517
  expect(device.getInternalState()).toBeUndefined();
1473
1518
  device.passphraseState = 'state-auto';
1474
1519
  expect(device.getInternalState()).toBe('session-auto');
1475
- expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
1520
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionOpen', 'DeviceSession', {
1521
+ select: { host_passphrase: { passphrase: 'state-auto-secret' } },
1522
+ });
1476
1523
  });
1477
1524
 
1478
1525
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
@@ -1481,6 +1528,7 @@ describe('Protocol V2 feature adapter', () => {
1481
1528
  type: 'DeviceSession',
1482
1529
  message: {
1483
1530
  session_id: 'main-pin-session',
1531
+ btc_test_address: 'main-wallet-state',
1484
1532
  },
1485
1533
  });
1486
1534
 
@@ -1510,7 +1558,9 @@ describe('Protocol V2 feature adapter', () => {
1510
1558
  expect(device.features?.passphraseProtection).toBe(false);
1511
1559
  expect(device.features?.sessionId).toBeNull();
1512
1560
  expect(device.getInternalState()).toBeUndefined();
1513
- expect(typedCall).not.toHaveBeenCalled();
1561
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1562
+ eventless_wallet_session: true,
1563
+ });
1514
1564
  });
1515
1565
 
1516
1566
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
@@ -1542,12 +1592,20 @@ describe('Protocol V2 feature adapter', () => {
1542
1592
  );
1543
1593
 
1544
1594
  expect(device.getInternalState()).toBeUndefined();
1545
- expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {});
1595
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionOpen', 'DeviceSession', {
1596
+ select: { host_passphrase: { passphrase: 'expected-secret' } },
1597
+ });
1546
1598
  });
1547
1599
 
1548
1600
  test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
1549
1601
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1550
- const typedCall = jest.fn();
1602
+ const typedCall = jest.fn().mockResolvedValue({
1603
+ type: 'DeviceSession',
1604
+ message: {
1605
+ session_id: 'main-wallet-session',
1606
+ btc_test_address: 'main-wallet-state',
1607
+ },
1608
+ });
1551
1609
 
1552
1610
  (device as any).features = normalizeProtocolV2Features(
1553
1611
  { ...descriptor, protocolType: 'V2' } as any,
@@ -1567,7 +1625,10 @@ describe('Protocol V2 feature adapter', () => {
1567
1625
  await expect(
1568
1626
  device.checkPassphraseStateSafety('stale-hidden-state', true, false)
1569
1627
  ).resolves.toBe(true);
1570
- expect(typedCall).not.toHaveBeenCalled();
1628
+ expect(typedCall).toHaveBeenCalledTimes(1);
1629
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1630
+ eventless_wallet_session: true,
1631
+ });
1571
1632
  });
1572
1633
 
1573
1634
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
@@ -2226,7 +2287,7 @@ describe('Protocol V2 feature adapter', () => {
2226
2287
  ['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
2227
2288
  ]);
2228
2289
  expect(typedCall).not.toHaveBeenCalledWith(
2229
- 'DeviceSessionGet',
2290
+ 'DeviceSessionOpen',
2230
2291
  'DeviceSession',
2231
2292
  expect.anything()
2232
2293
  );
@@ -2669,6 +2730,7 @@ describe('Protocol V2 firmware update targets', () => {
2669
2730
  return Promise.resolve({
2670
2731
  type: 'DeviceInfo',
2671
2732
  message: {
2733
+ hw: { serial_no: 'BLE-PRO2-SERIAL' },
2672
2734
  fw: {
2673
2735
  bootloader: { version: '0.0.0' },
2674
2736
  application: { version: '0.0.0' },
@@ -2689,6 +2751,7 @@ describe('Protocol V2 firmware update targets', () => {
2689
2751
  });
2690
2752
  const commands = { typedCall };
2691
2753
 
2754
+ (method as any).protocolV2ExpectedSerialNumber = 'BLE-PRO2-SERIAL';
2692
2755
  (method as any).isBleReconnect = jest.fn(() => true);
2693
2756
  (method as any).device = stubDevice({
2694
2757
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
@@ -2737,7 +2800,7 @@ describe('Protocol V2 firmware update targets', () => {
2737
2800
  });
2738
2801
  });
2739
2802
 
2740
- test('enters Protocol V2 bootloader before upload', async () => {
2803
+ test('delegates Protocol V2 mode transition to the phase executor', async () => {
2741
2804
  const method = new FirmwareUpdateV4({
2742
2805
  id: 1,
2743
2806
  payload: {
@@ -2750,6 +2813,7 @@ describe('Protocol V2 firmware update targets', () => {
2750
2813
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
2751
2814
  features: { capabilities: [] },
2752
2815
  });
2816
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
2753
2817
  (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
2754
2818
  (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
2755
2819
  {
@@ -2771,7 +2835,7 @@ describe('Protocol V2 firmware update targets', () => {
2771
2835
 
2772
2836
  await method.run();
2773
2837
 
2774
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
2838
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
2775
2839
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
2776
2840
  fwBinaryMap: [
2777
2841
  {
@@ -2815,6 +2879,7 @@ describe('Protocol V2 firmware update targets', () => {
2815
2879
  });
2816
2880
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2817
2881
  (method as any).device.getCommands = () => ({ typedCall });
2882
+ (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
2818
2883
  method.postTipMessage = jest.fn();
2819
2884
 
2820
2885
  await (method as any).enterProtocolV2BootloaderMode();
@@ -2880,7 +2945,7 @@ describe('Protocol V2 firmware update targets', () => {
2880
2945
  });
2881
2946
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2882
2947
  (method as any).device.getCommands = () => ({ typedCall });
2883
- (method as any).protocolV2ExpectedDeviceId = '8693920D7CA90CEC4A88353D';
2948
+ (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
2884
2949
 
2885
2950
  await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
2886
2951
 
@@ -2999,7 +3064,7 @@ describe('Protocol V2 firmware update targets', () => {
2999
3064
  fw: { application: { version: '1.0.0' } },
3000
3065
  },
3001
3066
  });
3002
- (method as any).protocolV2ExpectedDeviceId = 'expected-device';
3067
+ (method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
3003
3068
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3004
3069
  (method as any).device = stubDevice({
3005
3070
  getCommands: () => ({ typedCall }),
@@ -3025,6 +3090,7 @@ describe('Protocol V2 firmware update targets', () => {
3025
3090
  });
3026
3091
  method.postTipMessage = jest.fn();
3027
3092
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
3093
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3028
3094
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
3029
3095
  message: 'Device rebooted successfully',
3030
3096
  });
@@ -3088,9 +3154,9 @@ describe('Protocol V2 firmware update targets', () => {
3088
3154
  const typedCall = jest
3089
3155
  .fn()
3090
3156
  .mockRejectedValue(
3091
- new DOMException(
3092
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
3093
- 'NotFoundError'
3157
+ Object.assign(
3158
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
3159
+ { name: 'NotFoundError' }
3094
3160
  )
3095
3161
  );
3096
3162
 
@@ -3161,6 +3227,7 @@ describe('Protocol V2 firmware update targets', () => {
3161
3227
  getCommands: () => ({ typedCall }),
3162
3228
  });
3163
3229
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3230
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3164
3231
  method.postProgressMessage = jest.fn();
3165
3232
 
3166
3233
  await (method as any).waitForProtocolV2FirmwareUpdateComplete([
@@ -3177,7 +3244,7 @@ describe('Protocol V2 firmware update targets', () => {
3177
3244
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
3178
3245
  });
3179
3246
 
3180
- test('accepts a missing firmware status handler as an app reboot signal', async () => {
3247
+ test('does not treat a missing firmware status handler as install completion', async () => {
3181
3248
  const method = new FirmwareUpdateV4({
3182
3249
  id: 1,
3183
3250
  payload: {
@@ -3193,14 +3260,42 @@ describe('Protocol V2 firmware update targets', () => {
3193
3260
  getCommands: () => ({ typedCall }),
3194
3261
  });
3195
3262
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3263
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3196
3264
  method.postProgressMessage = jest.fn();
3197
3265
 
3198
- await (method as any).waitForProtocolV2FirmwareUpdateComplete([
3199
- { target_id: 4, path: 'vol0:/application_p1.bin' },
3200
- ]);
3266
+ await expect(
3267
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
3268
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
3269
+ ])
3270
+ ).rejects.toThrow('Protocol V2 firmware install status is unavailable');
3201
3271
 
3202
3272
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
3203
- expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
3273
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
3274
+ 'DeviceFirmwareUpdateStatusGet',
3275
+ 'DeviceInfoGet',
3276
+ ]);
3277
+ });
3278
+
3279
+ test('rejects a finished target whose payload version conflicts with the plan', () => {
3280
+ const method = new FirmwareUpdateV4({
3281
+ id: 1,
3282
+ payload: {
3283
+ method: 'firmwareUpdateV4',
3284
+ platform: 'desktop',
3285
+ expectedTargetVersions: {
3286
+ app_v1: '2.0.0',
3287
+ },
3288
+ },
3289
+ });
3290
+ method.init();
3291
+ method.postProgressMessage = jest.fn();
3292
+
3293
+ expect(() =>
3294
+ (method as any).assertProtocolV2TargetStatus(
3295
+ [{ target_id: 4, status: 2, payload_version: 0x010000 }],
3296
+ new Set([4])
3297
+ )
3298
+ ).toThrow('expected 131072');
3204
3299
  });
3205
3300
 
3206
3301
  test('polls target status after update ACK and finishes when all targets complete', async () => {
@@ -3225,6 +3320,7 @@ describe('Protocol V2 firmware update targets', () => {
3225
3320
  getCommands: () => ({ typedCall }),
3226
3321
  });
3227
3322
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3323
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3228
3324
  method.postProgressMessage = jest.fn();
3229
3325
 
3230
3326
  await expect(
@@ -3342,6 +3438,7 @@ describe('Protocol V2 firmware update targets', () => {
3342
3438
  getCommands: () => ({ typedCall }),
3343
3439
  });
3344
3440
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
3441
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3345
3442
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(false);
3346
3443
 
3347
3444
  await expect(
@@ -3377,10 +3474,14 @@ describe('Protocol V2 firmware update targets', () => {
3377
3474
  const writtenPaths: string[] = [];
3378
3475
  method.postTipMessage = jest.fn();
3379
3476
  method.postProgressMessage = jest.fn();
3380
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
3477
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
3381
3478
  writtenPaths.push(params.filePath);
3382
- return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
3479
+ return Number(params.processedSize ?? 0) + Number(params.source.size);
3383
3480
  });
3481
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3482
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3483
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
3484
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3384
3485
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3385
3486
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3386
3487
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3420,10 +3521,12 @@ describe('Protocol V2 firmware update targets', () => {
3420
3521
  'vol0:/coprocessor.bin',
3421
3522
  1
3422
3523
  );
3423
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
3424
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
3524
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
3525
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
3526
+ targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
3527
+ });
3528
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
3425
3529
  targets: [
3426
- { target_id: 3, path: 'vol0:/bootloader.bin' },
3427
3530
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
3428
3531
  { target_id: 7, path: 'vol0:/se01.bin' },
3429
3532
  { target_id: 4, path: 'vol0:/application_p1.bin' },
@@ -3433,7 +3536,7 @@ describe('Protocol V2 firmware update targets', () => {
3433
3536
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
3434
3537
  });
3435
3538
 
3436
- test('announces one transfer when syncing resource bundles and staging firmware', async () => {
3539
+ test('announces separate resource and component transfer phases', async () => {
3437
3540
  const method = new FirmwareUpdateV4({
3438
3541
  id: 1,
3439
3542
  payload: {
@@ -3443,11 +3546,14 @@ describe('Protocol V2 firmware update targets', () => {
3443
3546
 
3444
3547
  method.postTipMessage = jest.fn();
3445
3548
  method.postProgressMessage = jest.fn();
3446
- (method as any).protocolV2CommonUpdateProcess = jest
3549
+ (method as any).protocolV2SourceUpdateProcess = jest
3447
3550
  .fn()
3448
3551
  .mockImplementation(params =>
3449
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
3552
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
3450
3553
  );
3554
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3555
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3556
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3451
3557
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3452
3558
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3453
3559
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3467,21 +3573,22 @@ describe('Protocol V2 firmware update targets', () => {
3467
3573
  {
3468
3574
  fileName: 'application_p1.bin',
3469
3575
  binary: new Uint8Array([3]).buffer,
3470
- targetId: 3,
3576
+ targetId: 4,
3471
3577
  },
3472
3578
  ],
3473
3579
  });
3474
3580
 
3475
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
3581
+ expect(method.postTipMessage).toHaveBeenCalledTimes(3);
3476
3582
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
3477
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
3478
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
3583
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
3584
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
3585
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
3479
3586
  1,
3480
- expect.objectContaining({ processedSize: 0, totalSize: 3 })
3587
+ expect.objectContaining({ processedSize: 0, totalSize: 2 })
3481
3588
  );
3482
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
3589
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
3483
3590
  2,
3484
- expect.objectContaining({ processedSize: 2, totalSize: 3 })
3591
+ expect.objectContaining({ processedSize: 0, totalSize: 1 })
3485
3592
  );
3486
3593
  });
3487
3594
 
@@ -3495,7 +3602,8 @@ describe('Protocol V2 firmware update targets', () => {
3495
3602
 
3496
3603
  method.postTipMessage = jest.fn();
3497
3604
  method.postProgressMessage = jest.fn();
3498
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
3605
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
3606
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3499
3607
  (method as any).device = stubDevice({
3500
3608
  getCommands: () => ({
3501
3609
  typedCall: jest.fn().mockResolvedValue({
@@ -3541,11 +3649,14 @@ describe('Protocol V2 firmware update targets', () => {
3541
3649
 
3542
3650
  method.postTipMessage = jest.fn();
3543
3651
  method.postProgressMessage = jest.fn();
3544
- (method as any).protocolV2CommonUpdateProcess = jest
3652
+ (method as any).protocolV2SourceUpdateProcess = jest
3545
3653
  .fn()
3546
3654
  .mockImplementation(params =>
3547
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
3655
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
3548
3656
  );
3657
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3658
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3659
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3549
3660
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3550
3661
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3551
3662
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3725,7 +3836,7 @@ describe('Protocol V2 firmware update targets', () => {
3725
3836
  {
3726
3837
  name: 'images',
3727
3838
  url: 'https://example.com/images.okpkg',
3728
- devicePath: 'vol0:/resource/images/images.okpkg',
3839
+ devicePath: 'VOL0:/resource/images/images.okpkg',
3729
3840
  version: [2, 0, 0],
3730
3841
  payloadHash: 'ab'.repeat(64),
3731
3842
  headerHash: 'cd'.repeat(64),
@@ -3760,6 +3871,58 @@ describe('Protocol V2 firmware update targets', () => {
3760
3871
  getFirmwareLatestReleaseSpy.mockRestore();
3761
3872
  });
3762
3873
 
3874
+ test('rejects remote RESC bundle paths before download or bootloader entry', async () => {
3875
+ const method = new FirmwareUpdateV4({
3876
+ id: 1,
3877
+ payload: {
3878
+ method: 'firmwareUpdateV4',
3879
+ platform: 'web',
3880
+ targetsToUpdate: ['resource'],
3881
+ },
3882
+ });
3883
+ method.init();
3884
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3885
+
3886
+ const getSysResourceBinarySpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
3887
+ const getFirmwareLatestReleaseSpy = jest
3888
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
3889
+ .mockReturnValue({
3890
+ required: false,
3891
+ version: [1, 0, 0],
3892
+ url: '',
3893
+ resourceBundles: [
3894
+ {
3895
+ name: 'images',
3896
+ url: 'https://example.com/images.okpkg',
3897
+ devicePath: 'vol0:/resource/../images.okpkg',
3898
+ },
3899
+ ],
3900
+ fingerprint: '',
3901
+ changelog: {
3902
+ 'zh-CN': '',
3903
+ 'en-US': '',
3904
+ },
3905
+ });
3906
+
3907
+ (method as any).device = stubDevice({
3908
+ originalDescriptor: { protocolType: 'V2' },
3909
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
3910
+ });
3911
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
3912
+ bootloaderBinary: null,
3913
+ fwBinaryMap: [],
3914
+ });
3915
+ (method as any).enterProtocolV2BootloaderMode = jest.fn();
3916
+ method.postTipMessage = jest.fn();
3917
+
3918
+ await expect(method.run()).rejects.toThrow('resourceBundles[0].devicePath');
3919
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
3920
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
3921
+
3922
+ getSysResourceBinarySpy.mockRestore();
3923
+ getFirmwareLatestReleaseSpy.mockRestore();
3924
+ });
3925
+
3763
3926
  test('downloads and validates remote RESC bundles before entering bootloader', async () => {
3764
3927
  const method = new FirmwareUpdateV4({
3765
3928
  id: 1,
@@ -3770,6 +3933,7 @@ describe('Protocol V2 firmware update targets', () => {
3770
3933
  },
3771
3934
  });
3772
3935
  method.init();
3936
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3773
3937
 
3774
3938
  const order: string[] = [];
3775
3939
  const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
@@ -3789,7 +3953,7 @@ describe('Protocol V2 firmware update targets', () => {
3789
3953
  {
3790
3954
  name: 'images',
3791
3955
  url: 'https://example.com/images.okpkg',
3792
- devicePath: 'vol0:/resource/images/images.okpkg',
3956
+ devicePath: ' VOL0:/resource/images/images.okpkg ',
3793
3957
  },
3794
3958
  ],
3795
3959
  fingerprint: '',
@@ -3811,7 +3975,9 @@ describe('Protocol V2 firmware update targets', () => {
3811
3975
  order.push('bootloader');
3812
3976
  return Promise.resolve(true);
3813
3977
  });
3814
- (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3978
+ const executeProtocolV2UpdateSpy = jest.spyOn(method as any, 'executeProtocolV2Update');
3979
+ (method as any).executeProtocolV2TransferPhase = jest.fn().mockResolvedValue(undefined);
3980
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue(undefined);
3815
3981
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3816
3982
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3817
3983
  bootloaderVersion: '1.0.0',
@@ -3823,7 +3989,7 @@ describe('Protocol V2 firmware update targets', () => {
3823
3989
  await method.run();
3824
3990
 
3825
3991
  expect(order).toEqual(['download', 'bootloader']);
3826
- expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3992
+ expect(executeProtocolV2UpdateSpy).toHaveBeenCalledWith({
3827
3993
  bootloaderBinary: null,
3828
3994
  fwBinaryMap: [],
3829
3995
  resourceBundles: [
@@ -3836,6 +4002,7 @@ describe('Protocol V2 firmware update targets', () => {
3836
4002
  ],
3837
4003
  });
3838
4004
 
4005
+ executeProtocolV2UpdateSpy.mockRestore();
3839
4006
  getSysResourceBinarySpy.mockRestore();
3840
4007
  getFirmwareLatestReleaseSpy.mockRestore();
3841
4008
  });
@@ -3850,6 +4017,7 @@ describe('Protocol V2 firmware update targets', () => {
3850
4017
  },
3851
4018
  });
3852
4019
  method.init();
4020
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3853
4021
 
3854
4022
  const getSysResourceBinarySpy = jest
3855
4023
  .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
@@ -3961,6 +4129,7 @@ describe('Protocol V2 firmware update targets', () => {
3961
4129
  },
3962
4130
  });
3963
4131
  method.init();
4132
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3964
4133
 
3965
4134
  const remoteBinaries = {
3966
4135
  bootloaderBinary: new Uint8Array([1]).buffer,
@@ -4007,6 +4176,7 @@ describe('Protocol V2 firmware update targets', () => {
4007
4176
  },
4008
4177
  });
4009
4178
  method.init();
4179
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4010
4180
 
4011
4181
  (method as any).device = stubDevice({
4012
4182
  originalDescriptor: { protocolType: 'V2' },
@@ -4042,12 +4212,13 @@ describe('Protocol V2 firmware update targets', () => {
4042
4212
  resourceBundleFiles: [
4043
4213
  {
4044
4214
  binary: resourceBundle,
4045
- devicePath: 'vol0:/resource/images/images.okpkg',
4215
+ devicePath: ' VOL0:/resource/images/images.okpkg ',
4046
4216
  },
4047
4217
  ],
4048
4218
  },
4049
4219
  });
4050
4220
  method.init();
4221
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4051
4222
 
4052
4223
  (method as any).device = stubDevice({
4053
4224
  originalDescriptor: { protocolType: 'V2' },
@@ -4080,6 +4251,36 @@ describe('Protocol V2 firmware update targets', () => {
4080
4251
  );
4081
4252
  });
4082
4253
 
4254
+ test('rejects manual RESC bundle paths before bootloader entry', async () => {
4255
+ const method = new FirmwareUpdateV4({
4256
+ id: 1,
4257
+ payload: {
4258
+ method: 'firmwareUpdateV4',
4259
+ platform: 'web',
4260
+ resourceBundleFiles: [
4261
+ {
4262
+ binary: new Uint8Array([1, 2, 3]).buffer,
4263
+ devicePath: 'vol2:/resource/images/images.okpkg',
4264
+ },
4265
+ ],
4266
+ },
4267
+ });
4268
+ method.init();
4269
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4270
+
4271
+ (method as any).device = stubDevice({
4272
+ originalDescriptor: { protocolType: 'V2' },
4273
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
4274
+ });
4275
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
4276
+ (method as any).enterProtocolV2BootloaderMode = jest.fn();
4277
+ method.postTipMessage = jest.fn();
4278
+
4279
+ await expect(method.run()).rejects.toThrow('resourceBundleFiles[0].devicePath');
4280
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
4281
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
4282
+ });
4283
+
4083
4284
  test('syncs resource bundles without sending a firmware install request', async () => {
4084
4285
  const method = new FirmwareUpdateV4({
4085
4286
  id: 1,
@@ -4090,7 +4291,10 @@ describe('Protocol V2 firmware update targets', () => {
4090
4291
 
4091
4292
  method.postTipMessage = jest.fn();
4092
4293
  method.postProgressMessage = jest.fn();
4093
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
4294
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
4295
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
4296
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
4297
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
4094
4298
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
4095
4299
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
4096
4300
 
@@ -4106,7 +4310,7 @@ describe('Protocol V2 firmware update targets', () => {
4106
4310
  fwBinaryMap: [],
4107
4311
  });
4108
4312
 
4109
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
4313
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
4110
4314
  expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
4111
4315
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
4112
4316
  });
@@ -4137,12 +4341,19 @@ describe('Protocol V2 firmware update targets', () => {
4137
4341
  });
4138
4342
  method.postProgressMessage = jest.fn();
4139
4343
 
4140
- await (method as any).protocolV2CommonUpdateProcess({
4141
- payload: new Uint8Array(4097).buffer,
4142
- filePath: 'vol1:firmware.bin',
4143
- processedSize: 0,
4144
- totalSize: 4097,
4344
+ const source = await openFirmwareByteSource({
4345
+ binary: new Uint8Array(4097).buffer,
4145
4346
  });
4347
+ try {
4348
+ await (method as any).protocolV2SourceUpdateProcess({
4349
+ source,
4350
+ filePath: 'vol1:firmware.bin',
4351
+ processedSize: 0,
4352
+ totalSize: 4097,
4353
+ });
4354
+ } finally {
4355
+ await source?.close();
4356
+ }
4146
4357
 
4147
4358
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
4148
4359
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
@@ -4193,12 +4404,16 @@ describe('Protocol V2 firmware update targets', () => {
4193
4404
  method.postProgressMessage = jest.fn();
4194
4405
 
4195
4406
  try {
4196
- await (method as any).protocolV2CommonUpdateProcess({
4197
- payload: new Uint8Array(8001).buffer,
4407
+ const source = await openFirmwareByteSource({
4408
+ binary: new Uint8Array(8001).buffer,
4409
+ });
4410
+ await (method as any).protocolV2SourceUpdateProcess({
4411
+ source,
4198
4412
  filePath: 'vol0:/firmware.bin',
4199
4413
  processedSize: 0,
4200
4414
  totalSize: 8001,
4201
4415
  });
4416
+ await source?.close();
4202
4417
  } finally {
4203
4418
  setTimeoutSpy.mockRestore();
4204
4419
  }
@@ -4230,16 +4445,21 @@ describe('Protocol V2 firmware update targets', () => {
4230
4445
  });
4231
4446
  method.postProgressMessage = jest.fn();
4232
4447
  method.postTipMessage = jest.fn();
4448
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
4233
4449
 
4450
+ const source = await openFirmwareByteSource({
4451
+ binary: new Uint8Array(4097).buffer,
4452
+ });
4234
4453
  await expect(
4235
- (method as any).protocolV2WriteWholeFile({
4236
- payload: new Uint8Array(4097).buffer,
4454
+ (method as any).protocolV2SourceUpdateProcess({
4455
+ source,
4237
4456
  filePath: 'vol0:/firmware.bin',
4238
4457
  processedSize: 0,
4239
4458
  totalSize: 4097,
4240
4459
  })
4241
4460
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
4242
- expect(typedCall).toHaveBeenCalledTimes(2);
4461
+ await source?.close();
4462
+ expect(typedCall).toHaveBeenCalledTimes(6);
4243
4463
  });
4244
4464
 
4245
4465
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -4272,12 +4492,19 @@ describe('Protocol V2 firmware update targets', () => {
4272
4492
  });
4273
4493
  method.postProgressMessage = jest.fn();
4274
4494
 
4275
- await (method as any).protocolV2CommonUpdateProcess({
4276
- payload: new Uint8Array(1801).buffer,
4277
- filePath: 'vol1:ble-firmware.bin',
4278
- processedSize: 0,
4279
- totalSize: 1801,
4495
+ const source = await openFirmwareByteSource({
4496
+ binary: new Uint8Array(1801).buffer,
4280
4497
  });
4498
+ try {
4499
+ await (method as any).protocolV2SourceUpdateProcess({
4500
+ source,
4501
+ filePath: 'vol1:ble-firmware.bin',
4502
+ processedSize: 0,
4503
+ totalSize: 1801,
4504
+ });
4505
+ } finally {
4506
+ await source?.close();
4507
+ }
4281
4508
 
4282
4509
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
4283
4510
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
@@ -4394,9 +4621,9 @@ describe('Protocol V2 firmware update method', () => {
4394
4621
  const typedCall = jest
4395
4622
  .fn()
4396
4623
  .mockRejectedValue(
4397
- new DOMException(
4398
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
4399
- 'NotFoundError'
4624
+ Object.assign(
4625
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
4626
+ { name: 'NotFoundError' }
4400
4627
  )
4401
4628
  );
4402
4629
 
@@ -4561,29 +4788,64 @@ describe('Protocol V2 firmware update method', () => {
4561
4788
  });
4562
4789
 
4563
4790
  describe('Protocol V2 firmware reconnect identity', () => {
4564
- test('rejects a different device before firmware transfer resumes', () => {
4565
- expect(() => assertProtocolV2ReconnectIdentity('expected-device', 'other-device')).toThrow(
4791
+ test('keeps the host plan identity as the V4 reconnect anchor', () => {
4792
+ const method = new FirmwareUpdateV4({
4793
+ id: 1,
4794
+ payload: {
4795
+ method: 'firmwareUpdateV4',
4796
+ platform: 'native',
4797
+ expectedDeviceId: 'persisted-device-id',
4798
+ },
4799
+ });
4800
+
4801
+ method.init();
4802
+
4803
+ expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
4804
+ });
4805
+
4806
+ test('requires the same physical serial before firmware transfer resumes', () => {
4807
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
4566
4808
  'identity mismatch'
4567
4809
  );
4568
- expect(() => assertProtocolV2ReconnectIdentity('expected-device', undefined)).toThrow(
4810
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).toThrow(
4569
4811
  'identity unavailable'
4570
4812
  );
4571
4813
  expect(() =>
4572
- assertProtocolV2ReconnectIdentity('expected-device', 'expected-device')
4814
+ assertProtocolV2ReconnectIdentity('expected-serial', 'expected-serial')
4573
4815
  ).not.toThrow();
4816
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).toThrow(
4817
+ 'identity unavailable'
4818
+ );
4574
4819
  });
4575
4820
 
4576
- test('allows bootloader reconnect without wallet device id after USB identity is selected', () => {
4577
- expect(() =>
4578
- assertProtocolV2ReconnectIdentity('expected-device', undefined, {
4579
- allowMissingActual: true,
4580
- })
4581
- ).not.toThrow();
4582
- expect(() =>
4583
- assertProtocolV2ReconnectIdentity('expected-device', 'other-device', {
4584
- allowMissingActual: true,
4585
- })
4586
- ).toThrow('identity mismatch');
4821
+ test('captures physical identity from active DeviceInfo instead of wallet deviceId', async () => {
4822
+ const method = new FirmwareUpdateV4({
4823
+ id: 1,
4824
+ payload: {
4825
+ method: 'firmwareUpdateV4',
4826
+ },
4827
+ });
4828
+ const typedCall = jest.fn().mockResolvedValue({
4829
+ type: 'DeviceInfo',
4830
+ message: {
4831
+ protocol_version: 1,
4832
+ hw: { serial_no: 'PRO2-PHYSICAL-1' },
4833
+ },
4834
+ });
4835
+ (method as any).device = stubDevice({
4836
+ features: { deviceId: 'wallet-derived-id' },
4837
+ getCommands: () => ({ typedCall }),
4838
+ });
4839
+
4840
+ await (method as any).captureProtocolV2PhysicalIdentity();
4841
+
4842
+ expect((method as any).protocolV2ExpectedSerialNumber).toBe('PRO2-PHYSICAL-1');
4843
+ expect(typedCall).toHaveBeenCalledWith(
4844
+ 'DeviceInfoGet',
4845
+ 'DeviceInfo',
4846
+ expect.objectContaining({ targets: expect.objectContaining({ hw: true }) }),
4847
+ { timeoutMs: 5000 }
4848
+ );
4587
4849
  });
4588
4850
  });
4589
4851
 
@@ -4661,7 +4923,7 @@ describe('Protocol V2 protected method execution', () => {
4661
4923
  errorCode: HardwareErrorCode.DeviceLocked,
4662
4924
  });
4663
4925
 
4664
- test('uses locked retry by default for SDK business methods', () => {
4926
+ test('does not replay SDK methods unless they are explicitly classified as retry-safe', () => {
4665
4927
  class TestBusinessMethod extends BaseMethod {
4666
4928
  init() {}
4667
4929
 
@@ -4675,7 +4937,7 @@ describe('Protocol V2 protected method execution', () => {
4675
4937
  payload: { method: 'testBusinessMethod' },
4676
4938
  } as any);
4677
4939
 
4678
- expect(method.unlockPolicy).toBe('retry-on-locked');
4940
+ expect(method.unlockPolicy).toBe('none');
4679
4941
  });
4680
4942
 
4681
4943
  test.each([
@@ -4770,7 +5032,7 @@ describe('Protocol V2 protected method execution', () => {
4770
5032
  const calls: string[] = [];
4771
5033
  const method = {
4772
5034
  name: 'deviceSettingsPageShow',
4773
- unlockPolicy: 'retry-on-locked',
5035
+ unlockPolicy: 'unlock-before-run',
4774
5036
  protocolV2UiInteraction: { reason: 'settings-page' },
4775
5037
  run: jest.fn(() => {
4776
5038
  calls.push('run');
@@ -4854,14 +5116,12 @@ describe('Protocol V2 protected method execution', () => {
4854
5116
  test('keeps auto unlock but suppresses all synthesized UI for eventless methods', async () => {
4855
5117
  const method = {
4856
5118
  name: 'uploadPortfolio',
4857
- unlockPolicy: 'retry-on-locked',
5119
+ unlockPolicy: 'unlock-before-run',
4858
5120
  protocolV2UiMode: 'none',
4859
- run: jest
4860
- .fn()
4861
- .mockRejectedValueOnce(deviceLockedError())
4862
- .mockResolvedValueOnce({ message: 'ok' }),
5121
+ run: jest.fn().mockResolvedValue({ message: 'ok' }),
4863
5122
  };
4864
5123
  const device = {
5124
+ features: { unlocked: false },
4865
5125
  isProtocolV2: () => true,
4866
5126
  unlockDevice: jest.fn().mockResolvedValue(undefined),
4867
5127
  };
@@ -4880,6 +5140,24 @@ describe('Protocol V2 protected method execution', () => {
4880
5140
  expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
4881
5141
  });
4882
5142
 
5143
+ test('does not replay a state-changing method after a locked response', async () => {
5144
+ const error = deviceLockedError();
5145
+ const method = {
5146
+ name: 'uploadPortfolio',
5147
+ unlockPolicy: 'unlock-before-run',
5148
+ run: jest.fn().mockRejectedValue(error),
5149
+ };
5150
+ const device = {
5151
+ features: { unlocked: true },
5152
+ isProtocolV2: () => true,
5153
+ unlockDevice: jest.fn(),
5154
+ };
5155
+
5156
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
5157
+ expect(method.run).toHaveBeenCalledTimes(1);
5158
+ expect(device.unlockDevice).not.toHaveBeenCalled();
5159
+ });
5160
+
4883
5161
  test('does not retry when unlock fails or when the retry is still locked', async () => {
4884
5162
  const initialError = deviceLockedError();
4885
5163
  const unlockError = new Error('PIN cancelled');
@@ -4959,7 +5237,7 @@ describe('Protocol V2 current low-level methods', () => {
4959
5237
  });
4960
5238
 
4961
5239
  await expect(v2Method.run()).resolves.toEqual({ message: 'accepted' });
4962
- expect(v2Method.unlockPolicy).toBe('retry-on-locked');
5240
+ expect(v2Method.unlockPolicy).toBe('unlock-before-run');
4963
5241
  expect(v2TypedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
4964
5242
  page: DeviceSettingsPage.DeviceReset,
4965
5243
  });
@@ -5002,7 +5280,7 @@ describe('Protocol V2 current low-level methods', () => {
5002
5280
  });
5003
5281
 
5004
5282
  await expect(v2Method.run()).resolves.toEqual({ message: 'accepted' });
5005
- expect(v2Method.unlockPolicy).toBe('retry-on-locked');
5283
+ expect(v2Method.unlockPolicy).toBe('unlock-before-run');
5006
5284
  expect(v2TypedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
5007
5285
  page: DeviceSettingsPage.DevicePinChange,
5008
5286
  });
@@ -5103,32 +5381,24 @@ describe('Protocol V2 current low-level methods', () => {
5103
5381
  });
5104
5382
  });
5105
5383
 
5106
- test('returns canonical public state after explicit device unlock', async () => {
5384
+ test('preserves the legacy Features response after explicit device unlock', async () => {
5107
5385
  const method = new DeviceUnlock({
5108
5386
  id: 1,
5109
5387
  payload: { method: 'deviceUnlock' },
5110
5388
  });
5111
5389
  method.init();
5112
5390
 
5113
- const state = {
5114
- schemaVersion: 1,
5115
- revision: 2,
5116
- updatedAt: 1,
5391
+ const features = {
5117
5392
  protocol: 'V2',
5118
- protocolVersion: 1,
5119
- identity: {},
5120
- status: { unlocked: true },
5121
- settings: {},
5122
- versions: {},
5123
- capabilities: [],
5393
+ unlocked: true,
5394
+ sessionId: null,
5395
+ session_id: null,
5124
5396
  };
5125
- const unlockDevice = jest.fn().mockResolvedValue({ raw: { protocolV2DeviceStatus: {} } });
5126
- const getDeviceState = jest.fn().mockResolvedValue(state);
5127
- (method as any).device = { unlockDevice, getDeviceState };
5397
+ const unlockDevice = jest.fn().mockResolvedValue(features);
5398
+ (method as any).device = { unlockDevice };
5128
5399
 
5129
- await expect(method.run()).resolves.toBe(state);
5400
+ await expect(method.run()).resolves.toBe(features);
5130
5401
  expect(unlockDevice).toHaveBeenCalledTimes(1);
5131
- expect(getDeviceState).toHaveBeenCalledWith();
5132
5402
  });
5133
5403
 
5134
5404
  test.each([
@@ -5169,7 +5439,7 @@ describe('Protocol V2 current low-level methods', () => {
5169
5439
 
5170
5440
  method.init();
5171
5441
 
5172
- expect(method.unlockPolicy).toBe('retry-on-locked');
5442
+ expect(method.unlockPolicy).toBe('unlock-before-run');
5173
5443
  });
5174
5444
 
5175
5445
  test('unlocks before opening Protocol V2 settings pages through the unified method', () => {
@@ -5180,7 +5450,7 @@ describe('Protocol V2 current low-level methods', () => {
5180
5450
 
5181
5451
  method.init();
5182
5452
 
5183
- expect(method.unlockPolicy).toBe('retry-on-locked');
5453
+ expect(method.unlockPolicy).toBe('unlock-before-run');
5184
5454
  });
5185
5455
 
5186
5456
  test('keeps firmwareUpdateV4 unlock-before-reboot behavior explicit', () => {
@@ -5191,7 +5461,7 @@ describe('Protocol V2 current low-level methods', () => {
5191
5461
 
5192
5462
  method.init();
5193
5463
 
5194
- expect(method.unlockPolicy).toBe('retry-on-locked');
5464
+ expect(method.unlockPolicy).toBe('unlock-before-run');
5195
5465
  });
5196
5466
 
5197
5467
  test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {