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

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 (131) hide show
  1. package/__tests__/DeviceCommands.test.ts +35 -0
  2. package/__tests__/connectSettings.test.ts +5 -47
  3. package/__tests__/device-lifecycle-events.test.ts +59 -0
  4. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +30 -39
  5. package/__tests__/open-wallet-session.test.ts +192 -129
  6. package/__tests__/protocol-v2-ping.test.ts +28 -0
  7. package/__tests__/protocol-v2.test.ts +338 -376
  8. package/__tests__/public-pro2-api-boundary.test.ts +22 -0
  9. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  10. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  11. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  12. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  13. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  14. package/dist/api/FirmwareUpdateV4.d.ts +5 -21
  15. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  16. package/dist/api/OpenWalletSession.d.ts +1 -8
  17. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  18. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  19. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  20. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -7
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  22. package/dist/api/firmware/getBinary.d.ts +0 -7
  23. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  24. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  25. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  26. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  27. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  28. package/dist/api/index.d.ts +1 -0
  29. package/dist/api/index.d.ts.map +1 -1
  30. package/dist/api/protocol-v2/Ping.d.ts +2 -0
  31. package/dist/api/protocol-v2/Ping.d.ts.map +1 -1
  32. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/data-manager/DataManager.d.ts +0 -1
  35. package/dist/data-manager/DataManager.d.ts.map +1 -1
  36. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  37. package/dist/device/Device.d.ts.map +1 -1
  38. package/dist/device/DeviceCommands.d.ts.map +1 -1
  39. package/dist/index.d.ts +89 -273
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +783 -2443
  42. package/dist/inject.d.ts.map +1 -1
  43. package/dist/lowLevelInject.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockRetry.d.ts +2 -3
  45. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +12 -0
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/topLevelInject.d.ts.map +1 -1
  49. package/dist/types/api/checkAllFirmwareRelease.d.ts +0 -3
  50. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  51. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  52. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  53. package/dist/types/api/export.d.ts +1 -4
  54. package/dist/types/api/export.d.ts.map +1 -1
  55. package/dist/types/api/firmwareUpdate.d.ts +0 -69
  56. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  57. package/dist/types/api/index.d.ts +4 -10
  58. package/dist/types/api/index.d.ts.map +1 -1
  59. package/dist/types/api/openWalletSession.d.ts +0 -12
  60. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  61. package/dist/types/api/protocolV2.d.ts +4 -0
  62. package/dist/types/api/protocolV2.d.ts.map +1 -1
  63. package/dist/types/settings.d.ts +0 -13
  64. package/dist/types/settings.d.ts.map +1 -1
  65. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  66. package/dist/utils/patch.d.ts +1 -1
  67. package/dist/utils/patch.d.ts.map +1 -1
  68. package/package.json +4 -4
  69. package/src/api/CheckAllFirmwareRelease.ts +3 -23
  70. package/src/api/FirmwareUpdateV2.ts +120 -292
  71. package/src/api/FirmwareUpdateV3.ts +55 -256
  72. package/src/api/FirmwareUpdateV4.ts +297 -757
  73. package/src/api/OpenWalletSession.ts +23 -86
  74. package/src/api/device/DeviceUpdateBootloader.ts +6 -122
  75. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -50
  76. package/src/api/firmware/updateBootloader.ts +4 -19
  77. package/src/api/firmware/uploadFirmware.ts +22 -140
  78. package/src/api/index.ts +1 -0
  79. package/src/api/protocol-v2/Ping.ts +32 -1
  80. package/src/core/index.ts +39 -7
  81. package/src/data/messages/messages-protocol-v2.json +33 -1
  82. package/src/data-manager/DataManager.ts +33 -33
  83. package/src/data-manager/connectSettings.ts +0 -11
  84. package/src/device/Device.ts +7 -9
  85. package/src/device/DeviceCommands.ts +19 -0
  86. package/src/index.ts +0 -5
  87. package/src/inject.ts +4 -22
  88. package/src/lowLevelInject.ts +1 -5
  89. package/src/protocols/protocol-v2/unlockRetry.ts +20 -3
  90. package/src/protocols/protocol-v2/walletSession.ts +26 -8
  91. package/src/topLevelInject.ts +1 -5
  92. package/src/types/api/checkAllFirmwareRelease.ts +0 -3
  93. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  94. package/src/types/api/export.ts +0 -18
  95. package/src/types/api/firmwareUpdate.ts +0 -76
  96. package/src/types/api/index.ts +3 -14
  97. package/src/types/api/openWalletSession.ts +0 -19
  98. package/src/types/api/protocolV2.ts +10 -0
  99. package/src/types/settings.ts +0 -13
  100. package/src/utils/deviceInfoUtils.ts +15 -3
  101. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -86
  102. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  103. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  104. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  105. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -326
  106. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -213
  107. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  108. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  109. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  110. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  111. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  112. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  113. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  114. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -23
  115. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  116. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -30
  117. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  118. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  119. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  120. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -27
  121. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  122. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  123. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  124. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  125. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  126. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  127. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -621
  128. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -392
  129. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  130. package/src/types/api/firmwareUpdatePlan.ts +0 -36
  131. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -1,4 +1,4 @@
1
- import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import { DeviceRebootType, DeviceSessionPinType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
@@ -83,7 +83,6 @@ 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';
87
86
  import {
88
87
  getPassphraseState,
89
88
  getPassphraseStateWithRefreshDeviceInfo,
@@ -98,6 +97,14 @@ jest.mock('../src/data/config', () => ({
98
97
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
99
98
  }));
100
99
 
100
+ const createWalletSessionTypedCall = (implementation = jest.fn()) =>
101
+ jest.fn((request: string, ...args: unknown[]) => {
102
+ if (request === 'ProtocolInfoRequest') {
103
+ return Promise.resolve({ message: { version: 2 } });
104
+ }
105
+ return implementation(request, ...args);
106
+ });
107
+
101
108
  describe('DeviceUploadWallpaper', () => {
102
109
  test('encodes, uploads and applies a Pro2 wallpaper', async () => {
103
110
  const rgba = new Uint8Array(604 * 1024 * 4).fill(255);
@@ -690,13 +697,15 @@ describe('Protocol V2 feature adapter', () => {
690
697
  const features = normalizeProtocolV2Features(descriptor as any);
691
698
  features.firmwareVersion = '1.2.3';
692
699
  features.passphraseProtection = true;
693
- const typedCall = jest.fn().mockResolvedValue({
694
- type: 'DeviceSession',
695
- message: {
696
- btc_test_address: 'state-1',
697
- session_id: 'session-1',
698
- },
699
- });
700
+ const typedCall = createWalletSessionTypedCall(
701
+ jest.fn().mockResolvedValue({
702
+ type: 'DeviceSession',
703
+ message: {
704
+ btc_test_address: 'state-1',
705
+ session_id: 'session-1',
706
+ },
707
+ })
708
+ );
700
709
  const commands = { typedCall } as unknown as DeviceCommands;
701
710
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
702
711
  (device as any).features = features;
@@ -724,13 +733,15 @@ describe('Protocol V2 feature adapter', () => {
724
733
  { ...descriptor, protocolType: 'V2' } as any,
725
734
  { status: { passphrase_enabled: false, unlocked: true } }
726
735
  );
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
- });
736
+ const typedCall = createWalletSessionTypedCall(
737
+ jest.fn().mockResolvedValue({
738
+ type: 'DeviceSession',
739
+ message: {
740
+ btc_test_address: 'main-wallet-state',
741
+ session_id: 'main-wallet-session',
742
+ },
743
+ })
744
+ );
734
745
  const promptPassphrase = jest
735
746
  .fn()
736
747
  .mockResolvedValue({ passphrase: 'hidden-wallet-with-disabled-flag' });
@@ -800,17 +811,16 @@ describe('Protocol V2 feature adapter', () => {
800
811
  },
801
812
  }
802
813
  );
803
- const typedCall = jest.fn().mockImplementation((request: string) => {
804
- if (request === 'ProtocolInfoRequest') {
805
- return Promise.resolve({ message: { version: 2 } });
806
- }
807
- if (request === 'DeviceStatusGet') {
808
- return Promise.resolve({
809
- message: { passphrase_enabled: false, unlocked: true },
810
- });
811
- }
812
- throw new Error(`Unexpected request: ${request}`);
813
- });
814
+ const typedCall = createWalletSessionTypedCall(
815
+ jest.fn().mockImplementation((request: string) => {
816
+ if (request === 'DeviceStatusGet') {
817
+ return Promise.resolve({
818
+ message: { passphrase_enabled: false, unlocked: true },
819
+ });
820
+ }
821
+ throw new Error(`Unexpected request: ${request}`);
822
+ })
823
+ );
814
824
  const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden-after-unlock' });
815
825
  const unlockDevice = jest.spyOn(device, 'unlockDevice').mockImplementation(() => {
816
826
  (device as any).features = {
@@ -844,13 +854,15 @@ describe('Protocol V2 feature adapter', () => {
844
854
  );
845
855
  device.passphraseState = 'hidden-state';
846
856
  preloadSessionCache('main-wallet-device', 'hidden-state', 'hidden-session');
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
- });
857
+ const typedCall = createWalletSessionTypedCall(
858
+ jest.fn().mockResolvedValue({
859
+ type: 'DeviceSession',
860
+ message: {
861
+ btc_test_address: 'main-wallet-state',
862
+ session_id: 'main-wallet-session',
863
+ },
864
+ })
865
+ );
854
866
  (device as any).commands = { typedCall };
855
867
 
856
868
  await expect(
@@ -885,11 +897,13 @@ describe('Protocol V2 feature adapter', () => {
885
897
  },
886
898
  }
887
899
  );
888
- const typedCall = jest.fn().mockResolvedValue({
889
- type: 'DeviceSession',
890
- message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
891
- });
892
- const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden secret' });
900
+ const typedCall = createWalletSessionTypedCall(
901
+ jest.fn().mockResolvedValue({
902
+ type: 'DeviceSession',
903
+ message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
904
+ })
905
+ );
906
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
893
907
  (device as any).commands = { typedCall, promptPassphrase };
894
908
 
895
909
  await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
@@ -898,14 +912,7 @@ describe('Protocol V2 feature adapter', () => {
898
912
  });
899
913
 
900
914
  expect(promptPassphrase).toHaveBeenCalled();
901
- expect(typedCall).toHaveBeenCalledWith(
902
- 'DeviceSessionAskPassphrase',
903
- 'Success',
904
- {},
905
- {
906
- timeoutMs: 120_000,
907
- }
908
- );
915
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {});
909
916
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
910
917
  });
911
918
 
@@ -999,10 +1006,12 @@ describe('Protocol V2 feature adapter', () => {
999
1006
  );
1000
1007
  device.passphraseState = 'state-a';
1001
1008
  preloadSessionCache('stable-device-1', 'state-a', 'session-a');
1002
- const typedCall = jest.fn().mockResolvedValue({
1003
- type: 'DeviceSession',
1004
- message: { session_id: 'session-b', btc_test_address: 'state-a' },
1005
- });
1009
+ const typedCall = createWalletSessionTypedCall(
1010
+ jest.fn().mockResolvedValue({
1011
+ type: 'DeviceSession',
1012
+ message: { session_id: 'session-b', btc_test_address: 'state-a' },
1013
+ })
1014
+ );
1006
1015
  (device as any).commands = { typedCall };
1007
1016
 
1008
1017
  await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
@@ -1027,13 +1036,15 @@ describe('Protocol V2 feature adapter', () => {
1027
1036
  { status: { device_id: 'stable-device-2', unlocked: true } }
1028
1037
  );
1029
1038
  preloadSessionCache('stable-device-2', 'state-a', 'session-a');
1030
- const typedCall = jest.fn().mockResolvedValue({
1031
- type: 'DeviceSession',
1032
- message: {
1033
- session_id: 'main-session',
1034
- btc_test_address: 'main-wallet-state',
1035
- },
1036
- });
1039
+ const typedCall = createWalletSessionTypedCall(
1040
+ jest.fn().mockResolvedValue({
1041
+ type: 'DeviceSession',
1042
+ message: {
1043
+ session_id: 'main-session',
1044
+ btc_test_address: 'main-wallet-state',
1045
+ },
1046
+ })
1047
+ );
1037
1048
  (device as any).commands = { typedCall };
1038
1049
 
1039
1050
  await getProtocolV2WalletSession(device, { onlyMainPin: true });
@@ -1062,14 +1073,13 @@ describe('Protocol V2 feature adapter', () => {
1062
1073
  );
1063
1074
  device.passphraseState = 'state-a';
1064
1075
  preloadSessionCache('stable-device-3', 'state-a', 'session-a');
1065
- const typedCall = jest
1066
- .fn()
1067
- .mockResolvedValueOnce({ message: { version: 2 } })
1068
- .mockRejectedValue(
1076
+ const typedCall = createWalletSessionTypedCall(
1077
+ jest.fn().mockRejectedValue(
1069
1078
  Object.assign(new Error('SE request failed'), {
1070
1079
  errorCode: HardwareErrorCode.RuntimeError,
1071
1080
  })
1072
- );
1081
+ )
1082
+ );
1073
1083
  const promptPassphrase = jest.fn();
1074
1084
  (device as any).commands = { typedCall, promptPassphrase };
1075
1085
 
@@ -1100,17 +1110,16 @@ describe('Protocol V2 feature adapter', () => {
1100
1110
  },
1101
1111
  }
1102
1112
  );
1103
- const typedCall = jest
1104
- .fn()
1105
- .mockResolvedValueOnce({ message: { version: 2 } })
1106
- .mockRejectedValue(
1113
+ const typedCall = createWalletSessionTypedCall(
1114
+ jest.fn().mockRejectedValue(
1107
1115
  Object.assign(new Error('Invalid session'), {
1108
1116
  errorCode: HardwareErrorCode.WalletSessionInvalid,
1109
1117
  })
1110
- );
1118
+ )
1119
+ );
1111
1120
  (device as any).commands = {
1112
1121
  typedCall,
1113
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'new-secret' }),
1122
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1114
1123
  };
1115
1124
 
1116
1125
  await expect(getProtocolV2WalletSession(device)).rejects.toMatchObject({
@@ -1118,7 +1127,7 @@ describe('Protocol V2 feature adapter', () => {
1118
1127
  });
1119
1128
  expect(typedCall.mock.calls).toEqual([
1120
1129
  ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1121
- ['DeviceSessionAskPassphrase', 'Success', {}, { timeoutMs: 120_000 }],
1130
+ ['DeviceSessionAskPassphrase', 'Success', {}],
1122
1131
  ]);
1123
1132
  });
1124
1133
 
@@ -1141,10 +1150,12 @@ describe('Protocol V2 feature adapter', () => {
1141
1150
  device.passphraseState = 'state-a';
1142
1151
  preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a');
1143
1152
  (device as any).commands = {
1144
- typedCall: jest.fn().mockResolvedValue({
1145
- type: 'DeviceSession',
1146
- message: { session_id: 'session-b', btc_test_address: 'state-b' },
1147
- }),
1153
+ typedCall: createWalletSessionTypedCall(
1154
+ jest.fn().mockResolvedValue({
1155
+ type: 'DeviceSession',
1156
+ message: { session_id: 'session-b', btc_test_address: 'state-b' },
1157
+ })
1158
+ ),
1148
1159
  };
1149
1160
 
1150
1161
  await expect(
@@ -1170,20 +1181,21 @@ describe('Protocol V2 feature adapter', () => {
1170
1181
  const lockedError = Object.assign(new Error('Device is locked'), {
1171
1182
  errorCode: HardwareErrorCode.DeviceLocked,
1172
1183
  });
1173
- const typedCall = jest
1174
- .fn()
1175
- .mockResolvedValueOnce({ message: { version: 2 } })
1176
- .mockRejectedValueOnce(lockedError)
1177
- .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1178
- .mockResolvedValueOnce({
1179
- message: {
1180
- session_id: 'session-after-unlock',
1181
- btc_test_address: 'state-after-unlock',
1182
- },
1183
- });
1184
+ const typedCall = createWalletSessionTypedCall(
1185
+ jest
1186
+ .fn()
1187
+ .mockRejectedValueOnce(lockedError)
1188
+ .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1189
+ .mockResolvedValueOnce({
1190
+ message: {
1191
+ session_id: 'session-after-unlock',
1192
+ btc_test_address: 'state-after-unlock',
1193
+ },
1194
+ })
1195
+ );
1184
1196
  (device as any).commands = {
1185
1197
  typedCall,
1186
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'hidden secret' }),
1198
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1187
1199
  };
1188
1200
  const unlockDevice = jest.spyOn(device, 'unlockDevice').mockResolvedValue(undefined as any);
1189
1201
 
@@ -1335,7 +1347,7 @@ describe('Protocol V2 feature adapter', () => {
1335
1347
  };
1336
1348
  const typedCall = jest
1337
1349
  .fn()
1338
- .mockResolvedValueOnce({ message: { eventless_wallet_session: true } })
1350
+ .mockResolvedValueOnce({ message: { version: 2 } })
1339
1351
  .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1340
1352
  .mockResolvedValueOnce({
1341
1353
  type: 'DeviceSession',
@@ -1346,7 +1358,7 @@ describe('Protocol V2 feature adapter', () => {
1346
1358
  });
1347
1359
  const updateInternalState = jest.fn();
1348
1360
  const getFeatures = jest.fn().mockResolvedValue(features);
1349
- const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'legacy app secret' });
1361
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
1350
1362
  const method = new GetPassphraseState({
1351
1363
  payload: {
1352
1364
  method: 'getPassphraseState',
@@ -1370,8 +1382,12 @@ describe('Protocol V2 feature adapter', () => {
1370
1382
 
1371
1383
  await expect(method.run()).resolves.toBe('state-pro2');
1372
1384
  expect(promptPassphrase).toHaveBeenCalledWith(
1373
- expect.objectContaining({ deviceOnly: true }),
1374
- expect.anything()
1385
+ {
1386
+ existsAttachPinUser: false,
1387
+ source: 'wallet-session-coordinator',
1388
+ reason: 'open-wallet',
1389
+ },
1390
+ { cancelDeviceOnReject: false }
1375
1391
  );
1376
1392
  expect(updateInternalState).toHaveBeenCalledWith(
1377
1393
  true,
@@ -1388,7 +1404,7 @@ describe('Protocol V2 feature adapter', () => {
1388
1404
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1389
1405
  const typedCall = jest.fn().mockImplementation((request: string) => {
1390
1406
  if (request === 'ProtocolInfoRequest') {
1391
- return Promise.resolve({ message: { eventless_wallet_session: true } });
1407
+ return Promise.resolve({ message: { version: 2 } });
1392
1408
  }
1393
1409
  if (request === 'DeviceSessionAskPassphrase') {
1394
1410
  return Promise.resolve({ message: { message: 'passphrase prepared' } });
@@ -1451,7 +1467,7 @@ describe('Protocol V2 feature adapter', () => {
1451
1467
  passphraseProtection: false,
1452
1468
  unlocked: true,
1453
1469
  };
1454
- const typedCall = jest.fn();
1470
+ const typedCall = createWalletSessionTypedCall();
1455
1471
  const updateInternalState = jest.fn();
1456
1472
  const method = new GetPassphraseState({
1457
1473
  payload: {
@@ -1465,7 +1481,7 @@ describe('Protocol V2 feature adapter', () => {
1465
1481
  features,
1466
1482
  commands: {
1467
1483
  typedCall,
1468
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'mock-hidden-wallet' }),
1484
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1469
1485
  },
1470
1486
  updateInternalState,
1471
1487
  getCurrentDeviceType: () => 'pro2',
@@ -1491,7 +1507,7 @@ describe('Protocol V2 feature adapter', () => {
1491
1507
  };
1492
1508
  const typedCall = jest
1493
1509
  .fn()
1494
- .mockResolvedValueOnce({ message: { eventless_wallet_session: true } })
1510
+ .mockResolvedValueOnce({ message: { version: 2 } })
1495
1511
  .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1496
1512
  .mockResolvedValueOnce({
1497
1513
  type: 'DeviceSession',
@@ -1515,7 +1531,7 @@ describe('Protocol V2 feature adapter', () => {
1515
1531
  passphraseState: 'state-pro2-old',
1516
1532
  commands: {
1517
1533
  typedCall,
1518
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-new-secret' }),
1534
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1519
1535
  },
1520
1536
  emit: jest.fn(),
1521
1537
  clearInternalState,
@@ -1538,13 +1554,15 @@ describe('Protocol V2 feature adapter', () => {
1538
1554
 
1539
1555
  test('stores Pro2 passphrase session cache without synthetic device id', async () => {
1540
1556
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1541
- const typedCall = jest.fn().mockResolvedValue({
1542
- type: 'DeviceSession',
1543
- message: {
1544
- btc_test_address: 'state-profile',
1545
- session_id: 'session-profile',
1546
- },
1547
- });
1557
+ const typedCall = createWalletSessionTypedCall(
1558
+ jest.fn().mockResolvedValue({
1559
+ type: 'DeviceSession',
1560
+ message: {
1561
+ btc_test_address: 'state-profile',
1562
+ session_id: 'session-profile',
1563
+ },
1564
+ })
1565
+ );
1548
1566
 
1549
1567
  (device as any).features = {
1550
1568
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
@@ -1556,7 +1574,7 @@ describe('Protocol V2 feature adapter', () => {
1556
1574
  };
1557
1575
  (device as any).commands = {
1558
1576
  typedCall,
1559
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-profile-secret' }),
1577
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1560
1578
  };
1561
1579
 
1562
1580
  await getPassphraseStateWithRefreshDeviceInfo(device);
@@ -1568,13 +1586,15 @@ describe('Protocol V2 feature adapter', () => {
1568
1586
 
1569
1587
  test('stores Pro2 passphrase sessions without selecting them implicitly', async () => {
1570
1588
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1571
- const typedCall = jest.fn().mockResolvedValue({
1572
- type: 'DeviceSession',
1573
- message: {
1574
- btc_test_address: 'state-auto',
1575
- session_id: 'session-auto',
1576
- },
1577
- });
1589
+ const typedCall = createWalletSessionTypedCall(
1590
+ jest.fn().mockResolvedValue({
1591
+ type: 'DeviceSession',
1592
+ message: {
1593
+ btc_test_address: 'state-auto',
1594
+ session_id: 'session-auto',
1595
+ },
1596
+ })
1597
+ );
1578
1598
 
1579
1599
  (device as any).features = normalizeProtocolV2Features(
1580
1600
  {
@@ -1594,7 +1614,7 @@ describe('Protocol V2 feature adapter', () => {
1594
1614
  );
1595
1615
  (device as any).commands = {
1596
1616
  typedCall,
1597
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-auto-secret' }),
1617
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1598
1618
  };
1599
1619
 
1600
1620
  await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({
@@ -1613,13 +1633,15 @@ describe('Protocol V2 feature adapter', () => {
1613
1633
 
1614
1634
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
1615
1635
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1616
- const typedCall = jest.fn().mockResolvedValue({
1617
- type: 'DeviceSession',
1618
- message: {
1619
- session_id: 'main-pin-session',
1620
- btc_test_address: 'main-wallet-state',
1621
- },
1622
- });
1636
+ const typedCall = createWalletSessionTypedCall(
1637
+ jest.fn().mockResolvedValue({
1638
+ type: 'DeviceSession',
1639
+ message: {
1640
+ session_id: 'main-pin-session',
1641
+ btc_test_address: 'main-wallet-state',
1642
+ },
1643
+ })
1644
+ );
1623
1645
 
1624
1646
  (device as any).features = normalizeProtocolV2Features(
1625
1647
  {
@@ -1654,13 +1676,15 @@ describe('Protocol V2 feature adapter', () => {
1654
1676
 
1655
1677
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
1656
1678
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1657
- const typedCall = jest.fn().mockResolvedValue({
1658
- type: 'DeviceSession',
1659
- message: {
1660
- btc_test_address: 'wrong-state',
1661
- session_id: 'wrong-session',
1662
- },
1663
- });
1679
+ const typedCall = createWalletSessionTypedCall(
1680
+ jest.fn().mockResolvedValue({
1681
+ type: 'DeviceSession',
1682
+ message: {
1683
+ btc_test_address: 'wrong-state',
1684
+ session_id: 'wrong-session',
1685
+ },
1686
+ })
1687
+ );
1664
1688
 
1665
1689
  (device as any).features = normalizeProtocolV2Features({
1666
1690
  ...descriptor,
@@ -1671,7 +1695,7 @@ describe('Protocol V2 feature adapter', () => {
1671
1695
  (device as any).features.unlocked = true;
1672
1696
  (device as any).commands = {
1673
1697
  typedCall,
1674
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'expected-secret' }),
1698
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1675
1699
  };
1676
1700
 
1677
1701
  await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
@@ -1681,26 +1705,20 @@ describe('Protocol V2 feature adapter', () => {
1681
1705
  );
1682
1706
 
1683
1707
  expect(device.getInternalState()).toBeUndefined();
1684
- expect(typedCall).toHaveBeenNthCalledWith(
1685
- 2,
1686
- 'DeviceSessionAskPassphrase',
1687
- 'Success',
1688
- {},
1689
- {
1690
- timeoutMs: 120_000,
1691
- }
1692
- );
1708
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {});
1693
1709
  });
1694
1710
 
1695
1711
  test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
1696
1712
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1697
- const typedCall = jest.fn().mockResolvedValue({
1698
- type: 'DeviceSession',
1699
- message: {
1700
- session_id: 'main-wallet-session',
1701
- btc_test_address: 'main-wallet-state',
1702
- },
1703
- });
1713
+ const typedCall = createWalletSessionTypedCall(
1714
+ jest.fn().mockResolvedValue({
1715
+ type: 'DeviceSession',
1716
+ message: {
1717
+ session_id: 'main-wallet-session',
1718
+ btc_test_address: 'main-wallet-state',
1719
+ },
1720
+ })
1721
+ );
1704
1722
 
1705
1723
  (device as any).features = normalizeProtocolV2Features(
1706
1724
  { ...descriptor, protocolType: 'V2' } as any,
@@ -1800,7 +1818,7 @@ describe('Protocol V2 feature adapter', () => {
1800
1818
  expect(device.hasUsePassphrase()).toBe(true);
1801
1819
  });
1802
1820
 
1803
- test('keeps an empty Protocol V2 label distinct from the BLE name', () => {
1821
+ test('uses the BLE name as the Protocol V2 display label while preserving the raw label', () => {
1804
1822
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1805
1823
  (device as any).features = {
1806
1824
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
@@ -1813,8 +1831,9 @@ describe('Protocol V2 feature adapter', () => {
1813
1831
  serialNo: 'PR9999999999',
1814
1832
  bleName: 'Pro2 6136',
1815
1833
  name: 'Pro2 6136',
1816
- label: '',
1834
+ label: 'Pro2 6136',
1817
1835
  });
1836
+ expect(device.features?.label).toBeNull();
1818
1837
  });
1819
1838
 
1820
1839
  test('prefers the real Protocol V2 label over the BLE name for display', () => {
@@ -2333,12 +2352,7 @@ describe('Protocol V2 feature adapter', () => {
2333
2352
  const features = await device.unlockDevice();
2334
2353
 
2335
2354
  expect(typedCall.mock.calls).toEqual([
2336
- [
2337
- 'DeviceSessionAskPin',
2338
- 'Success',
2339
- { type: DeviceSessionPinType.Main },
2340
- { timeoutMs: 120_000 },
2341
- ],
2355
+ ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
2342
2356
  ['DeviceStatusGet', 'DeviceStatus', {}],
2343
2357
  ]);
2344
2358
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
@@ -2386,12 +2400,7 @@ describe('Protocol V2 feature adapter', () => {
2386
2400
  await device.unlockDevice();
2387
2401
 
2388
2402
  expect(typedCall.mock.calls).toEqual([
2389
- [
2390
- 'DeviceSessionAskPin',
2391
- 'Success',
2392
- { type: DeviceSessionPinType.Main },
2393
- { timeoutMs: 120_000 },
2394
- ],
2403
+ ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
2395
2404
  ['DeviceStatusGet', 'DeviceStatus', {}],
2396
2405
  ]);
2397
2406
  expect((device as any).profile).toBeUndefined();
@@ -2421,12 +2430,7 @@ describe('Protocol V2 feature adapter', () => {
2421
2430
  );
2422
2431
  expect(typedCall).toHaveBeenCalledTimes(1);
2423
2432
  expect(typedCall.mock.calls).toEqual([
2424
- [
2425
- 'DeviceSessionAskPin',
2426
- 'Success',
2427
- { type: DeviceSessionPinType.Main },
2428
- { timeoutMs: 120_000 },
2429
- ],
2433
+ ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
2430
2434
  ]);
2431
2435
  expect(typedCall).not.toHaveBeenCalledWith(
2432
2436
  'DeviceSessionGet',
@@ -2942,7 +2946,7 @@ describe('Protocol V2 firmware update targets', () => {
2942
2946
  });
2943
2947
  });
2944
2948
 
2945
- test('delegates Protocol V2 mode transition to the phase executor', async () => {
2949
+ test('enters Protocol V2 bootloader before upload', async () => {
2946
2950
  const method = new FirmwareUpdateV4({
2947
2951
  id: 1,
2948
2952
  payload: {
@@ -2977,7 +2981,7 @@ describe('Protocol V2 firmware update targets', () => {
2977
2981
 
2978
2982
  await method.run();
2979
2983
 
2980
- expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
2984
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
2981
2985
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
2982
2986
  fwBinaryMap: [
2983
2987
  {
@@ -3296,9 +3300,9 @@ describe('Protocol V2 firmware update targets', () => {
3296
3300
  const typedCall = jest
3297
3301
  .fn()
3298
3302
  .mockRejectedValue(
3299
- Object.assign(
3300
- new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
3301
- { name: 'NotFoundError' }
3303
+ new DOMException(
3304
+ "Failed to execute 'open' on 'USBDevice': The device was disconnected",
3305
+ 'NotFoundError'
3302
3306
  )
3303
3307
  );
3304
3308
 
@@ -3386,7 +3390,7 @@ describe('Protocol V2 firmware update targets', () => {
3386
3390
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
3387
3391
  });
3388
3392
 
3389
- test('does not treat a missing firmware status handler as install completion', async () => {
3393
+ test('accepts a missing firmware status handler as an app reboot signal', async () => {
3390
3394
  const method = new FirmwareUpdateV4({
3391
3395
  id: 1,
3392
3396
  payload: {
@@ -3405,62 +3409,12 @@ describe('Protocol V2 firmware update targets', () => {
3405
3409
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3406
3410
  method.postProgressMessage = jest.fn();
3407
3411
 
3408
- await expect(
3409
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
3410
- { target_id: 4, path: 'vol0:/application_p1.bin' },
3411
- ])
3412
- ).rejects.toThrow('Protocol V2 firmware install status is unavailable');
3413
-
3414
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
3415
- expect(typedCall.mock.calls.map(call => call[0])).toEqual([
3416
- 'DeviceFirmwareUpdateStatusGet',
3417
- 'DeviceInfoGet',
3412
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
3413
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
3418
3414
  ]);
3419
- });
3420
3415
 
3421
- test('rejects a finished target whose payload version conflicts with the plan', () => {
3422
- const method = new FirmwareUpdateV4({
3423
- id: 1,
3424
- payload: {
3425
- method: 'firmwareUpdateV4',
3426
- platform: 'desktop',
3427
- expectedTargetVersions: {
3428
- app_v1: '2.0.0',
3429
- },
3430
- },
3431
- });
3432
- method.init();
3433
- method.postProgressMessage = jest.fn();
3434
-
3435
- expect(() =>
3436
- (method as any).assertProtocolV2TargetStatus(
3437
- [{ target_id: 4, status: 2, payload_version: 0x010000 }],
3438
- new Set([4])
3439
- )
3440
- ).toThrow('expected 131072');
3441
- });
3442
-
3443
- test('does not fail a completed multi-app install when final app versions are unobservable', () => {
3444
- const method = new FirmwareUpdateV4({
3445
- id: 1,
3446
- payload: {
3447
- method: 'firmwareUpdateV4',
3448
- platform: 'desktop',
3449
- targetsToUpdate: ['app_v1', 'app_v2'],
3450
- expectedTargetVersions: {
3451
- app_v1: '1.0.0',
3452
- app_v2: '2.0.0',
3453
- },
3454
- },
3455
- });
3456
- method.init();
3457
- (method as any).protocolV2LatestFinalFeatures = {
3458
- major_version: 3,
3459
- minor_version: 0,
3460
- patch_version: 0,
3461
- };
3462
-
3463
- expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
3416
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
3417
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
3464
3418
  });
3465
3419
 
3466
3420
  test('polls target status after update ACK and finishes when all targets complete', async () => {
@@ -3639,14 +3593,10 @@ describe('Protocol V2 firmware update targets', () => {
3639
3593
  const writtenPaths: string[] = [];
3640
3594
  method.postTipMessage = jest.fn();
3641
3595
  method.postProgressMessage = jest.fn();
3642
- (method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
3596
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
3643
3597
  writtenPaths.push(params.filePath);
3644
- return Number(params.processedSize ?? 0) + Number(params.source.size);
3598
+ return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
3645
3599
  });
3646
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3647
- (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3648
- (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
3649
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3650
3600
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3651
3601
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3652
3602
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3686,12 +3636,10 @@ describe('Protocol V2 firmware update targets', () => {
3686
3636
  'vol0:/coprocessor.bin',
3687
3637
  1
3688
3638
  );
3689
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
3690
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
3691
- targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
3692
- });
3693
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
3639
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
3640
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
3694
3641
  targets: [
3642
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
3695
3643
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
3696
3644
  { target_id: 7, path: 'vol0:/se01.bin' },
3697
3645
  { target_id: 4, path: 'vol0:/application_p1.bin' },
@@ -3701,7 +3649,7 @@ describe('Protocol V2 firmware update targets', () => {
3701
3649
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
3702
3650
  });
3703
3651
 
3704
- test('announces separate resource and component transfer phases', async () => {
3652
+ test('announces one transfer when syncing resource bundles and staging firmware', async () => {
3705
3653
  const method = new FirmwareUpdateV4({
3706
3654
  id: 1,
3707
3655
  payload: {
@@ -3711,14 +3659,11 @@ describe('Protocol V2 firmware update targets', () => {
3711
3659
 
3712
3660
  method.postTipMessage = jest.fn();
3713
3661
  method.postProgressMessage = jest.fn();
3714
- (method as any).protocolV2SourceUpdateProcess = jest
3662
+ (method as any).protocolV2CommonUpdateProcess = jest
3715
3663
  .fn()
3716
3664
  .mockImplementation(params =>
3717
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
3665
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
3718
3666
  );
3719
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3720
- (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3721
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3722
3667
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3723
3668
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3724
3669
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3738,22 +3683,21 @@ describe('Protocol V2 firmware update targets', () => {
3738
3683
  {
3739
3684
  fileName: 'application_p1.bin',
3740
3685
  binary: new Uint8Array([3]).buffer,
3741
- targetId: 4,
3686
+ targetId: 3,
3742
3687
  },
3743
3688
  ],
3744
3689
  });
3745
3690
 
3746
- expect(method.postTipMessage).toHaveBeenCalledTimes(3);
3691
+ expect(method.postTipMessage).toHaveBeenCalledTimes(2);
3747
3692
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
3748
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
3749
- expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
3750
- expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
3693
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
3694
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
3751
3695
  1,
3752
- expect.objectContaining({ processedSize: 0, totalSize: 2 })
3696
+ expect.objectContaining({ processedSize: 0, totalSize: 3 })
3753
3697
  );
3754
- expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
3698
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
3755
3699
  2,
3756
- expect.objectContaining({ processedSize: 0, totalSize: 1 })
3700
+ expect.objectContaining({ processedSize: 2, totalSize: 3 })
3757
3701
  );
3758
3702
  });
3759
3703
 
@@ -3767,8 +3711,7 @@ describe('Protocol V2 firmware update targets', () => {
3767
3711
 
3768
3712
  method.postTipMessage = jest.fn();
3769
3713
  method.postProgressMessage = jest.fn();
3770
- (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
3771
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3714
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
3772
3715
  (method as any).device = stubDevice({
3773
3716
  getCommands: () => ({
3774
3717
  typedCall: jest.fn().mockResolvedValue({
@@ -3814,14 +3757,11 @@ describe('Protocol V2 firmware update targets', () => {
3814
3757
 
3815
3758
  method.postTipMessage = jest.fn();
3816
3759
  method.postProgressMessage = jest.fn();
3817
- (method as any).protocolV2SourceUpdateProcess = jest
3760
+ (method as any).protocolV2CommonUpdateProcess = jest
3818
3761
  .fn()
3819
3762
  .mockImplementation(params =>
3820
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
3763
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
3821
3764
  );
3822
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3823
- (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3824
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3825
3765
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3826
3766
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3827
3767
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -4088,58 +4028,6 @@ describe('Protocol V2 firmware update targets', () => {
4088
4028
  getFirmwareLatestReleaseSpy.mockRestore();
4089
4029
  });
4090
4030
 
4091
- test('rejects prepared RESC bundle paths before opening an artifact source', async () => {
4092
- const method = new FirmwareUpdateV4({
4093
- id: 1,
4094
- payload: {
4095
- method: 'firmwareUpdateV4',
4096
- },
4097
- });
4098
- const openPreparedSource = jest.fn();
4099
- (method as any).params = {
4100
- targetsToUpdate: ['resource'],
4101
- resourceBundleArtifacts: [
4102
- {
4103
- name: 'images',
4104
- artifact: {
4105
- artifactRef: `fw:${'a'.repeat(64)}`,
4106
- size: 1,
4107
- sha256: 'a'.repeat(64),
4108
- },
4109
- },
4110
- ],
4111
- };
4112
- (method as any).openProtocolV2PreparedSource = openPreparedSource;
4113
- const getFirmwareLatestReleaseSpy = jest
4114
- .spyOn(DataManager, 'getFirmwareLatestRelease')
4115
- .mockReturnValue({
4116
- required: false,
4117
- version: [1, 0, 0],
4118
- url: '',
4119
- resourceBundles: [
4120
- {
4121
- name: 'images',
4122
- url: 'https://example.com/images.okpkg',
4123
- devicePath: 'vol0:/resource/../images.okpkg',
4124
- },
4125
- ],
4126
- fingerprint: '',
4127
- changelog: {
4128
- 'zh-CN': '',
4129
- 'en-US': '',
4130
- },
4131
- });
4132
-
4133
- await expect(
4134
- (method as any).prepareProtocolV2ResourceSources('universal', {
4135
- deviceType: 'pro2',
4136
- })
4137
- ).rejects.toThrow('resourceBundles[].devicePath');
4138
- expect(openPreparedSource).not.toHaveBeenCalled();
4139
-
4140
- getFirmwareLatestReleaseSpy.mockRestore();
4141
- });
4142
-
4143
4031
  test('downloads and validates remote RESC bundles before entering bootloader', async () => {
4144
4032
  const method = new FirmwareUpdateV4({
4145
4033
  id: 1,
@@ -4192,9 +4080,7 @@ describe('Protocol V2 firmware update targets', () => {
4192
4080
  order.push('bootloader');
4193
4081
  return Promise.resolve(true);
4194
4082
  });
4195
- const executeProtocolV2UpdateSpy = jest.spyOn(method as any, 'executeProtocolV2Update');
4196
- (method as any).executeProtocolV2TransferPhase = jest.fn().mockResolvedValue(undefined);
4197
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue(undefined);
4083
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
4198
4084
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
4199
4085
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
4200
4086
  bootloaderVersion: '1.0.0',
@@ -4206,7 +4092,7 @@ describe('Protocol V2 firmware update targets', () => {
4206
4092
  await method.run();
4207
4093
 
4208
4094
  expect(order).toEqual(['download', 'bootloader']);
4209
- expect(executeProtocolV2UpdateSpy).toHaveBeenCalledWith({
4095
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
4210
4096
  bootloaderBinary: null,
4211
4097
  fwBinaryMap: [],
4212
4098
  resourceBundles: [
@@ -4219,7 +4105,6 @@ describe('Protocol V2 firmware update targets', () => {
4219
4105
  ],
4220
4106
  });
4221
4107
 
4222
- executeProtocolV2UpdateSpy.mockRestore();
4223
4108
  getSysResourceBinarySpy.mockRestore();
4224
4109
  getFirmwareLatestReleaseSpy.mockRestore();
4225
4110
  });
@@ -4508,10 +4393,7 @@ describe('Protocol V2 firmware update targets', () => {
4508
4393
 
4509
4394
  method.postTipMessage = jest.fn();
4510
4395
  method.postProgressMessage = jest.fn();
4511
- (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
4512
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
4513
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
4514
- (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
4396
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
4515
4397
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
4516
4398
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
4517
4399
 
@@ -4527,7 +4409,7 @@ describe('Protocol V2 firmware update targets', () => {
4527
4409
  fwBinaryMap: [],
4528
4410
  });
4529
4411
 
4530
- expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
4412
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
4531
4413
  expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
4532
4414
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
4533
4415
  });
@@ -4558,19 +4440,12 @@ describe('Protocol V2 firmware update targets', () => {
4558
4440
  });
4559
4441
  method.postProgressMessage = jest.fn();
4560
4442
 
4561
- const source = await openFirmwareByteSource({
4562
- binary: new Uint8Array(4097).buffer,
4443
+ await (method as any).protocolV2CommonUpdateProcess({
4444
+ payload: new Uint8Array(4097).buffer,
4445
+ filePath: 'vol1:firmware.bin',
4446
+ processedSize: 0,
4447
+ totalSize: 4097,
4563
4448
  });
4564
- try {
4565
- await (method as any).protocolV2SourceUpdateProcess({
4566
- source,
4567
- filePath: 'vol1:firmware.bin',
4568
- processedSize: 0,
4569
- totalSize: 4097,
4570
- });
4571
- } finally {
4572
- await source?.close();
4573
- }
4574
4449
 
4575
4450
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
4576
4451
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
@@ -4621,16 +4496,12 @@ describe('Protocol V2 firmware update targets', () => {
4621
4496
  method.postProgressMessage = jest.fn();
4622
4497
 
4623
4498
  try {
4624
- const source = await openFirmwareByteSource({
4625
- binary: new Uint8Array(8001).buffer,
4626
- });
4627
- await (method as any).protocolV2SourceUpdateProcess({
4628
- source,
4499
+ await (method as any).protocolV2CommonUpdateProcess({
4500
+ payload: new Uint8Array(8001).buffer,
4629
4501
  filePath: 'vol0:/firmware.bin',
4630
4502
  processedSize: 0,
4631
4503
  totalSize: 8001,
4632
4504
  });
4633
- await source?.close();
4634
4505
  } finally {
4635
4506
  setTimeoutSpy.mockRestore();
4636
4507
  }
@@ -4662,21 +4533,16 @@ describe('Protocol V2 firmware update targets', () => {
4662
4533
  });
4663
4534
  method.postProgressMessage = jest.fn();
4664
4535
  method.postTipMessage = jest.fn();
4665
- (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
4666
4536
 
4667
- const source = await openFirmwareByteSource({
4668
- binary: new Uint8Array(4097).buffer,
4669
- });
4670
4537
  await expect(
4671
- (method as any).protocolV2SourceUpdateProcess({
4672
- source,
4538
+ (method as any).protocolV2WriteWholeFile({
4539
+ payload: new Uint8Array(4097).buffer,
4673
4540
  filePath: 'vol0:/firmware.bin',
4674
4541
  processedSize: 0,
4675
4542
  totalSize: 4097,
4676
4543
  })
4677
4544
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
4678
- await source?.close();
4679
- expect(typedCall).toHaveBeenCalledTimes(6);
4545
+ expect(typedCall).toHaveBeenCalledTimes(2);
4680
4546
  });
4681
4547
 
4682
4548
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -4709,19 +4575,12 @@ describe('Protocol V2 firmware update targets', () => {
4709
4575
  });
4710
4576
  method.postProgressMessage = jest.fn();
4711
4577
 
4712
- const source = await openFirmwareByteSource({
4713
- binary: new Uint8Array(1801).buffer,
4578
+ await (method as any).protocolV2CommonUpdateProcess({
4579
+ payload: new Uint8Array(1801).buffer,
4580
+ filePath: 'vol1:ble-firmware.bin',
4581
+ processedSize: 0,
4582
+ totalSize: 1801,
4714
4583
  });
4715
- try {
4716
- await (method as any).protocolV2SourceUpdateProcess({
4717
- source,
4718
- filePath: 'vol1:ble-firmware.bin',
4719
- processedSize: 0,
4720
- totalSize: 1801,
4721
- });
4722
- } finally {
4723
- await source?.close();
4724
- }
4725
4584
 
4726
4585
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
4727
4586
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
@@ -4838,9 +4697,9 @@ describe('Protocol V2 firmware update method', () => {
4838
4697
  const typedCall = jest
4839
4698
  .fn()
4840
4699
  .mockRejectedValue(
4841
- Object.assign(
4842
- new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
4843
- { name: 'NotFoundError' }
4700
+ new DOMException(
4701
+ "Failed to execute 'open' on 'USBDevice': The device was disconnected",
4702
+ 'NotFoundError'
4844
4703
  )
4845
4704
  );
4846
4705
 
@@ -5005,21 +4864,6 @@ describe('Protocol V2 firmware update method', () => {
5005
4864
  });
5006
4865
 
5007
4866
  describe('Protocol V2 firmware reconnect identity', () => {
5008
- test('keeps the host plan identity as the V4 reconnect anchor', () => {
5009
- const method = new FirmwareUpdateV4({
5010
- id: 1,
5011
- payload: {
5012
- method: 'firmwareUpdateV4',
5013
- platform: 'native',
5014
- expectedDeviceId: 'persisted-device-id',
5015
- },
5016
- });
5017
-
5018
- method.init();
5019
-
5020
- expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
5021
- });
5022
-
5023
4867
  test('rejects a different physical serial before firmware transfer resumes', () => {
5024
4868
  expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
5025
4869
  'identity mismatch'
@@ -5029,10 +4873,16 @@ describe('Protocol V2 firmware reconnect identity', () => {
5029
4873
  ).not.toThrow();
5030
4874
  });
5031
4875
 
5032
- test('allows firmware update when the optional physical serial is unavailable', () => {
5033
- expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).not.toThrow();
5034
- expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).not.toThrow();
5035
- expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).not.toThrow();
4876
+ test('fails closed when the physical serial is unavailable', () => {
4877
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).toThrow(
4878
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
4879
+ );
4880
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).toThrow(
4881
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
4882
+ );
4883
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).toThrow(
4884
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
4885
+ );
5036
4886
  });
5037
4887
 
5038
4888
  test('captures physical identity from active DeviceInfo instead of wallet deviceId', async () => {
@@ -5064,6 +4914,54 @@ describe('Protocol V2 firmware reconnect identity', () => {
5064
4914
  { timeoutMs: 5000 }
5065
4915
  );
5066
4916
  });
4917
+
4918
+ test('rejects firmware update startup when DeviceInfo has no physical serial', async () => {
4919
+ const method = new FirmwareUpdateV4({
4920
+ id: 1,
4921
+ payload: { method: 'firmwareUpdateV4' },
4922
+ });
4923
+ const typedCall = jest.fn().mockResolvedValue({
4924
+ type: 'DeviceInfo',
4925
+ message: { protocol_version: 1, hw: {} },
4926
+ });
4927
+ (method as any).device = stubDevice({
4928
+ getCommands: () => ({ typedCall }),
4929
+ });
4930
+
4931
+ await expect((method as any).captureProtocolV2PhysicalIdentity()).rejects.toMatchObject({
4932
+ errorCode: HardwareErrorCode.DeviceNotFound,
4933
+ });
4934
+ });
4935
+
4936
+ test('uses the BLE read limit when reading an existing firmware bundle header', async () => {
4937
+ const method = new FirmwareUpdateV4({
4938
+ id: 1,
4939
+ payload: {
4940
+ method: 'firmwareUpdateV4',
4941
+ platform: 'native',
4942
+ chunkSize: 1800,
4943
+ },
4944
+ });
4945
+ method.init();
4946
+ const readChunkLengths: number[] = [];
4947
+ const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
4948
+ if (requestType === 'FilesystemPathInfoQuery') {
4949
+ return Promise.resolve({ message: { exist: true, directory: false, size: 0x52a0 } });
4950
+ }
4951
+ if (requestType === 'FilesystemFileRead') {
4952
+ readChunkLengths.push(request.chunk_len);
4953
+ return Promise.resolve({ message: { data: new Uint8Array(request.chunk_len) } });
4954
+ }
4955
+ throw new Error(`Unexpected request: ${requestType}`);
4956
+ });
4957
+ (method as any).device = stubDevice({
4958
+ getCommands: () => ({ typedCall }),
4959
+ });
4960
+
4961
+ await expect((method as any).readProtocolV2DeviceFileHeader('bundle.okpp')).resolves.toBeNull();
4962
+ expect(readChunkLengths.length).toBeGreaterThan(1);
4963
+ expect(Math.max(...readChunkLengths)).toBe(900);
4964
+ });
5067
4965
  });
5068
4966
 
5069
4967
  describe('Protocol V2 explicit USB device selection', () => {
@@ -5245,6 +5143,70 @@ describe('Protocol V2 protected method execution', () => {
5245
5143
  ]);
5246
5144
  });
5247
5145
 
5146
+ test('restores the expected hidden-wallet session before retrying after unlock', async () => {
5147
+ const calls: string[] = [];
5148
+ const method = {
5149
+ name: 'evmSignMessage',
5150
+ payload: { passphraseState: 'hidden-state' },
5151
+ useDevicePassphraseState: true,
5152
+ unlockPolicy: 'retry-on-locked',
5153
+ run: jest
5154
+ .fn()
5155
+ .mockImplementationOnce(() => {
5156
+ calls.push('run-1');
5157
+ return Promise.reject(deviceLockedError());
5158
+ })
5159
+ .mockImplementationOnce(() => {
5160
+ calls.push('run-2');
5161
+ return Promise.resolve({ message: 'ok' });
5162
+ }),
5163
+ };
5164
+ const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
5165
+ if (requestType === 'ProtocolInfoRequest') {
5166
+ calls.push('negotiate-session');
5167
+ return Promise.resolve({ message: { version: 2 } });
5168
+ }
5169
+ if (requestType === 'DeviceSessionGet') {
5170
+ calls.push('resume-hidden-session');
5171
+ expect(request).toEqual({ session_id: 'hidden-session' });
5172
+ return Promise.resolve({
5173
+ message: {
5174
+ session_id: 'hidden-session',
5175
+ btc_test_address: 'hidden-state',
5176
+ },
5177
+ });
5178
+ }
5179
+ throw new Error(`Unexpected request: ${requestType}`);
5180
+ });
5181
+ const device = {
5182
+ features: { unlocked: true, passphraseProtection: true },
5183
+ passphraseState: 'hidden-state',
5184
+ commands: { typedCall },
5185
+ isProtocolV2: () => true,
5186
+ unlockDevice: jest.fn(() => {
5187
+ calls.push('unlock');
5188
+ return Promise.resolve();
5189
+ }),
5190
+ getCurrentPassphraseProtection: () => true,
5191
+ getInternalState: () => 'hidden-session',
5192
+ clearInternalState: jest.fn(),
5193
+ getCurrentDeviceId: () => 'wallet-device-id',
5194
+ updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
5195
+ };
5196
+
5197
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
5198
+ message: 'ok',
5199
+ });
5200
+ expect(calls).toEqual([
5201
+ 'run-1',
5202
+ 'unlock',
5203
+ 'negotiate-session',
5204
+ 'resume-hidden-session',
5205
+ 'validate-hidden-session',
5206
+ 'run-2',
5207
+ ]);
5208
+ });
5209
+
5248
5210
  test('unlocks before showing the method interaction when cached status is locked', async () => {
5249
5211
  const calls: string[] = [];
5250
5212
  const method = {