@onekeyfe/hd-core 1.2.0-alpha.0 → 1.2.0-alpha.10

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 (192) hide show
  1. package/__tests__/connectSettings.test.ts +19 -0
  2. package/__tests__/device-wallet-session-store.test.ts +131 -0
  3. package/__tests__/preInitialize.test.ts +33 -0
  4. package/__tests__/protocol-v2-firmware-targets.test.ts +19 -0
  5. package/__tests__/protocol-v2.test.ts +1930 -765
  6. package/dist/api/ClearSessionCache.d.ts +9 -0
  7. package/dist/api/ClearSessionCache.d.ts.map +1 -0
  8. package/dist/api/FileRead.d.ts.map +1 -1
  9. package/dist/api/FileWrite.d.ts.map +1 -1
  10. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  12. package/dist/api/FirmwareUpdateV4.d.ts +26 -5
  13. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  14. package/dist/api/GetDeviceInfo.d.ts.map +1 -1
  15. package/dist/api/GetFeatures.d.ts +1 -1
  16. package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
  17. package/dist/api/GetPassphraseState.d.ts +4 -4
  18. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  19. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  20. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +1 -0
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  22. package/dist/api/firmware/bootloaderHelper.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +4 -0
  24. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  25. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  26. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  27. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  28. package/dist/api/index.d.ts +8 -10
  29. package/dist/api/index.d.ts.map +1 -1
  30. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts +6 -0
  31. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts.map +1 -0
  32. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts +7 -0
  33. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts.map +1 -0
  34. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +1 -1
  35. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  36. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +3 -2
  37. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  38. package/dist/api/protocol-v2/{DeviceGetDeviceInfo.d.ts → DeviceInfoGet.d.ts} +10 -10
  39. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -0
  40. package/dist/api/protocol-v2/DeviceSessionGet.d.ts +9 -0
  41. package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -0
  42. package/dist/api/protocol-v2/DeviceStatusGet.d.ts +6 -0
  43. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -0
  44. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -1
  45. package/dist/api/protocol-v2/{FilesystemFixPermission.d.ts → FilesystemPermissionFix.d.ts} +2 -2
  46. package/dist/api/protocol-v2/FilesystemPermissionFix.d.ts.map +1 -0
  47. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts +6 -0
  48. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -0
  49. package/dist/api/protocol-v2/helpers.d.ts +27 -19
  50. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  51. package/dist/api/ton/TonSignData.d.ts +1 -5
  52. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  53. package/dist/api/tron/TronSignMessage.d.ts +0 -1
  54. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  55. package/dist/api/tron/TronSignTransaction.d.ts +0 -1
  56. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  57. package/dist/constants/index.d.ts +2 -1
  58. package/dist/constants/index.d.ts.map +1 -1
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/data-manager/DataManager.d.ts +1 -1
  61. package/dist/data-manager/DataManager.d.ts.map +1 -1
  62. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  63. package/dist/device/Device.d.ts +17 -18
  64. package/dist/device/Device.d.ts.map +1 -1
  65. package/dist/device/DeviceWalletSessionStore.d.ts +15 -0
  66. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -0
  67. package/dist/deviceProfile/buildDeviceFeatures.d.ts +6 -0
  68. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -0
  69. package/dist/deviceProfile/buildDeviceProfile.d.ts +3 -4
  70. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  71. package/dist/deviceProfile/index.d.ts +1 -1
  72. package/dist/deviceProfile/index.d.ts.map +1 -1
  73. package/dist/index.d.ts +657 -539
  74. package/dist/index.js +2579 -2161
  75. package/dist/inject.d.ts.map +1 -1
  76. package/dist/protocols/protocol-v2/features.d.ts +12 -14
  77. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  78. package/dist/protocols/protocol-v2/firmware.d.ts +11 -9
  79. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  80. package/dist/protocols/protocol-v2/index.d.ts +1 -0
  81. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  82. package/dist/protocols/protocol-v2/walletSession.d.ts +13 -0
  83. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -0
  84. package/dist/types/api/firmwareUpdate.d.ts +4 -6
  85. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  86. package/dist/types/api/getDeviceInfo.d.ts +4 -2
  87. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  88. package/dist/types/api/getPassphraseState.d.ts +4 -4
  89. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  90. package/dist/types/api/index.d.ts +11 -11
  91. package/dist/types/api/index.d.ts.map +1 -1
  92. package/dist/types/api/protocolV2.d.ts +16 -17
  93. package/dist/types/api/protocolV2.d.ts.map +1 -1
  94. package/dist/types/api/sessionCache.d.ts +10 -0
  95. package/dist/types/api/sessionCache.d.ts.map +1 -0
  96. package/dist/types/device.d.ts +88 -8
  97. package/dist/types/device.d.ts.map +1 -1
  98. package/dist/types/params.d.ts +1 -0
  99. package/dist/types/params.d.ts.map +1 -1
  100. package/dist/types/settings.d.ts +21 -1
  101. package/dist/types/settings.d.ts.map +1 -1
  102. package/dist/utils/capabilitieUtils.d.ts.map +1 -1
  103. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  104. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  105. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  106. package/dist/utils/deviceVersionUtils.d.ts.map +1 -1
  107. package/dist/utils/findDefectiveBatchDevice.d.ts +1 -1
  108. package/dist/utils/patch.d.ts +1 -1
  109. package/dist/utils/patch.d.ts.map +1 -1
  110. package/package.json +4 -4
  111. package/src/api/ClearSessionCache.ts +28 -0
  112. package/src/api/FileRead.ts +16 -3
  113. package/src/api/FileWrite.ts +14 -1
  114. package/src/api/FirmwareUpdateV2.ts +9 -2
  115. package/src/api/FirmwareUpdateV3.ts +2 -1
  116. package/src/api/FirmwareUpdateV4.ts +1089 -285
  117. package/src/api/GetDeviceInfo.ts +6 -13
  118. package/src/api/GetOnekeyFeatures.ts +3 -14
  119. package/src/api/GetPassphraseState.ts +7 -4
  120. package/src/api/device/DeviceRebootToBoardloader.ts +4 -4
  121. package/src/api/device/DeviceRebootToBootloader.ts +4 -4
  122. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +18 -4
  123. package/src/api/firmware/bootloaderHelper.ts +3 -2
  124. package/src/api/firmware/getBinary.ts +1 -1
  125. package/src/api/firmware/progressThrottle.ts +44 -0
  126. package/src/api/firmware/releaseHelper.ts +3 -3
  127. package/src/api/firmware/uploadFirmware.ts +5 -2
  128. package/src/api/index.ts +8 -10
  129. package/src/api/protocol-v2/{FactoryGetDeviceInfo.ts → DeviceFactoryInfoGet.ts} +3 -3
  130. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +32 -0
  131. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +3 -5
  132. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +39 -5
  133. package/src/api/protocol-v2/{DeviceGetDeviceInfo.ts → DeviceInfoGet.ts} +52 -22
  134. package/src/api/protocol-v2/DeviceReboot.ts +2 -2
  135. package/src/api/protocol-v2/DeviceSessionGet.ts +26 -0
  136. package/src/api/protocol-v2/DeviceStatusGet.ts +14 -0
  137. package/src/api/protocol-v2/FilesystemFormat.ts +4 -1
  138. package/src/api/protocol-v2/{FilesystemFixPermission.ts → FilesystemPermissionFix.ts} +2 -2
  139. package/src/api/protocol-v2/{GetProtoVersion.ts → ProtocolInfoRequest.ts} +2 -2
  140. package/src/api/protocol-v2/helpers.ts +58 -59
  141. package/src/api/ton/TonSignData.ts +1 -5
  142. package/src/api/tron/TronSignMessage.ts +0 -1
  143. package/src/api/tron/TronSignTransaction.ts +0 -1
  144. package/src/constants/index.ts +10 -1
  145. package/src/core/index.ts +22 -10
  146. package/src/data/messages/messages-protocol-v2.json +992 -1420
  147. package/src/data/messages/messages.json +8 -0
  148. package/src/data-manager/DataManager.ts +11 -4
  149. package/src/data-manager/connectSettings.ts +4 -0
  150. package/src/device/Device.ts +158 -294
  151. package/src/device/DevicePool.ts +9 -9
  152. package/src/device/DeviceWalletSessionStore.ts +78 -0
  153. package/src/deviceProfile/buildDeviceFeatures.ts +365 -0
  154. package/src/deviceProfile/buildDeviceProfile.ts +165 -200
  155. package/src/deviceProfile/index.ts +4 -1
  156. package/src/inject.ts +13 -16
  157. package/src/protocols/protocol-v2/features.ts +28 -67
  158. package/src/protocols/protocol-v2/firmware.ts +12 -36
  159. package/src/protocols/protocol-v2/index.ts +1 -0
  160. package/src/protocols/protocol-v2/walletSession.ts +83 -0
  161. package/src/types/api/firmwareUpdate.ts +16 -20
  162. package/src/types/api/getDeviceInfo.ts +4 -2
  163. package/src/types/api/getPassphraseState.ts +4 -4
  164. package/src/types/api/index.ts +17 -20
  165. package/src/types/api/protocolV2.ts +35 -40
  166. package/src/types/api/sessionCache.ts +14 -0
  167. package/src/types/device.ts +98 -34
  168. package/src/types/params.ts +4 -0
  169. package/src/types/settings.ts +42 -30
  170. package/src/utils/capabilitieUtils.ts +1 -2
  171. package/src/utils/deviceFeaturesUtils.ts +54 -29
  172. package/src/utils/deviceInfoUtils.ts +11 -29
  173. package/src/utils/deviceVersionUtils.ts +7 -25
  174. package/src/utils/findDefectiveBatchDevice.ts +6 -6
  175. package/dist/api/protocol-v2/DevReboot.d.ts +0 -7
  176. package/dist/api/protocol-v2/DevReboot.d.ts.map +0 -1
  177. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +0 -1
  178. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +0 -6
  179. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +0 -1
  180. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +0 -7
  181. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +0 -1
  182. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +0 -6
  183. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +0 -1
  184. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +0 -1
  185. package/dist/api/protocol-v2/GetProtoVersion.d.ts +0 -6
  186. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +0 -1
  187. package/dist/deviceProfile/legacyFeaturesView.d.ts +0 -5
  188. package/dist/deviceProfile/legacyFeaturesView.d.ts.map +0 -1
  189. package/src/api/protocol-v2/DevReboot.ts +0 -24
  190. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +0 -16
  191. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +0 -29
  192. package/src/deviceProfile/legacyFeaturesView.ts +0 -123
@@ -1,17 +1,23 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DevRebootType } from '@onekeyfe/hd-transport';
2
+ import { DeviceRebootType } from '@onekeyfe/hd-transport';
3
3
 
4
- import ConfluxSignTransaction from '../src/api/conflux/ConfluxSignTransaction';
4
+ import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
5
5
  import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
6
6
  import DnxSignTransaction from '../src/api/dynex/DnxSignTransaction';
7
7
  import DirList from '../src/api/DirList';
8
8
  import FileRead from '../src/api/FileRead';
9
9
  import FileWrite from '../src/api/FileWrite';
10
- import DevReboot from '../src/api/protocol-v2/DevReboot';
10
+ import DeviceFactoryInfoGet from '../src/api/protocol-v2/DeviceFactoryInfoGet';
11
+ import DeviceFactoryInfoSet from '../src/api/protocol-v2/DeviceFactoryInfoSet';
11
12
  import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
12
- import DeviceGetDeviceInfo from '../src/api/protocol-v2/DeviceGetDeviceInfo';
13
- import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus';
13
+ import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus';
14
+ import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
14
15
  import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
16
+ import DeviceSessionGet from '../src/api/protocol-v2/DeviceSessionGet';
17
+ import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet';
18
+ import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat';
19
+ import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
20
+ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
15
21
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
16
22
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
17
23
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
@@ -35,27 +41,27 @@ import TonSignData from '../src/api/ton/TonSignData';
35
41
  import TonSignMessage from '../src/api/ton/TonSignMessage';
36
42
  import TonSignProof from '../src/api/ton/TonSignProof';
37
43
  import TronGetAddress from '../src/api/tron/TronGetAddress';
38
- import TronSignMessage from '../src/api/tron/TronSignMessage';
39
- import XrpSignTransaction from '../src/api/xrp/XrpSignTransaction';
40
44
  import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
41
45
  import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
42
46
  import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit';
43
47
  import { DataManager } from '../src/data-manager';
44
- import { Device } from '../src/device/Device';
48
+ import { createCoreApi } from '../src/inject';
49
+ import { Device, preloadSessionCache } from '../src/device/Device';
45
50
  import { UI_REQUEST } from '../src/events/ui-request';
46
51
  import {
47
52
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
48
- buildMockProtocolV2DeviceInfo,
49
53
  requestProtocolV2DeviceInfo,
50
- setProtocolV2DeviceInfoMock,
51
54
  } from '../src/protocols/protocol-v2/features';
52
55
  import {
53
- buildProfileFromProtocolV2,
54
- buildProtocolV2GetFeaturesPayload,
55
- } from '../src/deviceProfile';
56
+ getProtocolV2WalletSession,
57
+ refreshProtocolV2DeviceStatus,
58
+ } from '../src/protocols/protocol-v2/walletSession';
59
+ import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
56
60
  import {
57
61
  getDeviceType,
58
62
  getFirmwareType,
63
+ getFirmwareUpdateField,
64
+ getFirmwareUpdateFieldArray,
59
65
  getMethodVersionRange,
60
66
  isMethodVersionRangeUnsupported,
61
67
  } from '../src/utils';
@@ -74,12 +80,6 @@ jest.mock('../src/data/config', () => ({
74
80
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
75
81
  }));
76
82
 
77
- // DeviceGetDeviceInfo mock 开关默认开启(早期工程板固件没有该消息)。
78
- // 本测试文件验证的是真实 wire 调用行为,统一关闭;mock 行为单独有用例覆盖。
79
- beforeAll(() => {
80
- setProtocolV2DeviceInfoMock(false);
81
- });
82
-
83
83
  const descriptor = {
84
84
  id: 'ble-id',
85
85
  path: 'usb-path',
@@ -91,47 +91,48 @@ const descriptor = {
91
91
  */
92
92
  function stubDevice<T extends Record<string, any>>(device: T): T {
93
93
  const d = device as any;
94
- d.isProtocolV2 ??= () =>
95
- d.profile?.protocol === 'V2' || d.originalDescriptor?.protocolType === 'V2';
94
+ d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
96
95
  d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1');
97
- d.getCurrentDeviceType ??= () => d.profile?.deviceType ?? getDeviceType(d.features);
98
- d.getCurrentFirmwareType ??= () => d.profile?.firmwareType ?? getFirmwareType(d.features);
96
+ d.getCurrentDeviceType ??= () => getDeviceType(d.features);
97
+ d.getCurrentFirmwareType ??= () => getFirmwareType(d.features);
99
98
  d.getCurrentFirmwareVersionString ??= () =>
100
- d.profile?.versions?.firmware ??
101
- (d.features ? getDeviceFirmwareVersion(d.features).join('.') : undefined);
102
- d.getCurrentBLEFirmwareVersionString ??= () => d.profile?.versions?.ble ?? undefined;
103
- d.getCurrentSafetyChecks ??= () => d.features?.safety_checks;
104
- d.getCurrentDeviceId ??= () => d.profile?.deviceId || d.features?.device_id || undefined;
105
- d.getCurrentSerialNo ??= () => d.profile?.serialNo || '';
106
- d.getCurrentPassphraseProtection ??= () =>
107
- d.profile ? d.profile.status?.passphraseProtection : d.features?.passphrase_protection;
99
+ d.features ? getDeviceFirmwareVersion(d.features).join('.') : undefined;
100
+ d.getCurrentBLEFirmwareVersionString ??= () => d.features?.bleVersion;
101
+ d.getCurrentSafetyChecks ??= () => d.features?.safetyChecks;
102
+ d.getCurrentDeviceId ??= () => d.features?.deviceId || undefined;
103
+ d.getCurrentSerialNo ??= () => d.features?.serialNo || '';
104
+ d.getCurrentPassphraseProtection ??= () => d.features?.passphraseProtection;
108
105
  d.getCurrentMethodVersionRange ??= (fn: (model: any) => any) => {
109
106
  const deviceType = d.getCurrentDeviceType();
110
107
  const range = fn(deviceType);
111
108
  if (range) return range;
112
109
  return getMethodVersionRange(d.features, fn);
113
110
  };
114
- d.updateProfile ??= (p: any) => {
115
- d.profile = p;
116
- };
117
- d.applyProfileUpdate ??= (p: any) => {
118
- d.profile = p;
119
- return p;
111
+ d.updateProtocolV2Features ??= (deviceInfo?: ProtocolV2DeviceInfo) => {
112
+ d.features = buildProtocolV2FeaturesPayload(deviceInfo, d.features);
113
+ return d.features;
120
114
  };
121
115
  return device;
122
116
  }
123
117
 
124
118
  // 直接复用生产映射函数,避免测试内副本与实现漂移(之前的手抄副本已缺失 se boot 字段)
125
119
  function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: ProtocolV2DeviceInfo) {
126
- const profile = buildProfileFromProtocolV2({
127
- deviceInfo,
128
- sources: ['deviceInfo'],
129
- scope: 'verify',
130
- });
131
- return buildProtocolV2GetFeaturesPayload(profile, deviceInfo);
120
+ return buildProtocolV2FeaturesPayload(deviceInfo);
132
121
  }
133
122
 
134
- async function requestProtocolV2LegacyFeatures({
123
+ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
124
+ protocol_version: 1,
125
+ hw: {
126
+ serial_no: 'PR9999999999',
127
+ },
128
+ fw: {
129
+ bootloader: {
130
+ version: '0.0.1',
131
+ },
132
+ },
133
+ };
134
+
135
+ async function requestProtocolV2Features({
135
136
  commands,
136
137
  descriptor: inputDescriptor,
137
138
  }: {
@@ -143,32 +144,6 @@ async function requestProtocolV2LegacyFeatures({
143
144
  }
144
145
 
145
146
  describe('Protocol V2 feature adapter', () => {
146
- test('skips DeviceGetDeviceInfo wire call and returns mock when mock is enabled', async () => {
147
- const typedCall = jest.fn();
148
- setProtocolV2DeviceInfoMock(true);
149
- try {
150
- const deviceInfo = await requestProtocolV2DeviceInfo({
151
- commands: { typedCall } as unknown as DeviceCommands,
152
- });
153
- expect(typedCall).not.toHaveBeenCalled();
154
- expect(deviceInfo).toEqual(buildMockProtocolV2DeviceInfo());
155
- // 空 serial_no + fallbackSerialNo:身份字段回退 transport path
156
- const profile = buildProfileFromProtocolV2({
157
- deviceInfo,
158
- fallbackSerialNo: 'usb-path',
159
- });
160
- expect(profile).toMatchObject({
161
- protocol: 'V2',
162
- deviceId: 'usb-path',
163
- serialNo: 'usb-path',
164
- status: { initialized: true },
165
- versions: { firmware: '0.1.0' },
166
- });
167
- } finally {
168
- setProtocolV2DeviceInfoMock(false);
169
- }
170
- });
171
-
172
147
  test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
173
148
  const features = normalizeProtocolV2Features(descriptor as any, {
174
149
  protocol_version: 1,
@@ -176,31 +151,31 @@ describe('Protocol V2 feature adapter', () => {
176
151
  serial_no: 'PR2SERIAL',
177
152
  },
178
153
  fw: {
179
- board: {
154
+ application_data: {
180
155
  version: '0.1.0',
181
156
  hash: [1, 2, 255],
182
157
  },
183
- boot: {
158
+ bootloader: {
184
159
  version: '0.2.0',
185
160
  build_id: 'boot-build',
186
161
  hash: new Uint8Array([10, 11]),
187
162
  },
188
- app: {
163
+ application: {
189
164
  version: '1.2.3',
190
165
  build_id: 'app-build',
191
166
  hash: 'abc123',
192
167
  },
193
168
  },
194
- bt: {
195
- app: {
169
+ coprocessor: {
170
+ application: {
196
171
  version: '4.5.6',
197
172
  build_id: 'bt-build',
198
173
  hash: [12, 13],
199
174
  },
200
- adv_name: 'Pro2 BLE',
175
+ bt_adv_name: 'Pro2 BLE',
201
176
  },
202
177
  se1: {
203
- app: {
178
+ application: {
204
179
  version: '7.8.9',
205
180
  build_id: 'se-build',
206
181
  hash: [14, 15],
@@ -208,61 +183,80 @@ describe('Protocol V2 feature adapter', () => {
208
183
  state: 85,
209
184
  },
210
185
  se2: {
211
- app: {
186
+ application: {
212
187
  version: '8.0.0',
213
188
  },
214
189
  state: 0,
215
190
  },
216
191
  status: {
217
- label: 'My Pro2',
218
- language: 'en-US',
219
- bt_enable: true,
192
+ device_id: 'PRO2-DEVICE-ID',
193
+ unlocked: true,
220
194
  init_states: false,
221
195
  backup_required: true,
222
- passphrase_protection: true,
196
+ passphrase_enabled: true,
197
+ attach_to_pin_enabled: true,
198
+ unlocked_by_attach_to_pin: true,
223
199
  },
224
200
  });
225
201
 
226
- expect(features.device_id).toBe('PR2SERIAL');
227
- expect(features.serial_no).toBe('PR2SERIAL');
228
- expect(features.onekey_serial_no).toBe('PR2SERIAL');
229
- expect(features.onekey_device_type).toBe('pro2');
230
- expect(features.protocol_version).toBe(1);
231
- expect(features.major_version).toBe(1);
232
- expect(features.minor_version).toBe(2);
233
- expect(features.patch_version).toBe(3);
234
- expect(features.onekey_firmware_version).toBe('1.2.3');
235
- expect(features.onekey_firmware_build_id).toBe('app-build');
236
- expect(features.onekey_firmware_hash).toBe('abc123');
237
- expect(features.bootloader_version).toBe('0.2.0');
238
- expect(features.onekey_boot_build_id).toBe('boot-build');
239
- expect(features.onekey_boot_hash).toBe('0a0b');
240
- expect(features.onekey_board_hash).toBe('0102ff');
241
- expect(features.ble_name).toBe('Pro2 BLE');
242
- expect(features.onekey_ble_version).toBe('4.5.6');
243
- expect(features.onekey_ble_hash).toBe('0c0d');
244
- expect(features.onekey_se01_version).toBe('7.8.9');
245
- expect(features.onekey_se01_hash).toBe('0e0f');
246
- expect(features.onekey_se01_state).toBe('APP');
247
- expect(features.onekey_se02_state).toBe('BOOT');
248
- expect(features.label).toBe('My Pro2');
249
- expect(features.language).toBe('en-US');
202
+ expect(features.deviceId).toBe('PRO2-DEVICE-ID');
203
+ expect(features.serialNo).toBe('PR2SERIAL');
204
+ expect(features.deviceType).toBe('pro2');
205
+ expect(features.protocolVersion).toBe(1);
206
+ expect(features.firmwareVersion).toBe('1.2.3');
207
+ expect(features.verify?.firmwareBuildId).toBe('app-build');
208
+ expect(features.verify?.firmwareHash).toBe('abc123');
209
+ expect(features.bootloaderVersion).toBe('0.2.0');
210
+ expect(features.verify?.bootloaderBuildId).toBe('boot-build');
211
+ expect(features.verify?.bootloaderHash).toBe('0a0b');
212
+ expect(features.verify?.boardHash).toBe('0102ff');
213
+ expect(features.bleName).toBe('Pro2 BLE');
214
+ expect(features.bleVersion).toBe('4.5.6');
215
+ expect(features.verify?.bleHash).toBe('0c0d');
216
+ expect(features.se01Version).toBe('7.8.9');
217
+ expect(features.verify?.se01Hash).toBe('0e0f');
218
+ expect(features.label).toBeNull();
219
+ expect(features.language).toBeNull();
250
220
  expect(features.initialized).toBe(false);
251
- expect(features.needs_backup).toBe(true);
252
- expect(features.passphrase_protection).toBe(true);
253
- expect(features.ble_enable).toBe(true);
221
+ expect(features.unlocked).toBe(true);
222
+ expect(features.backupRequired).toBe(true);
223
+ expect(features.passphraseProtection).toBe(true);
224
+ expect(features.attachToPinEnabled).toBe(true);
225
+ expect(features.unlockedAttachPin).toBe(true);
226
+ expect(features.bleEnabled).toBeNull();
227
+ });
228
+
229
+ test('marks Protocol V2 DeviceInfo without status as bootloader mode', () => {
230
+ const deviceInfo = {
231
+ protocol_version: 1,
232
+ hw: {
233
+ serial_no: 'PR2BOOT',
234
+ },
235
+ fw: {
236
+ bootloader: {
237
+ version: '0.2.0',
238
+ },
239
+ },
240
+ };
241
+ const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
242
+ const profile = buildProfileFromProtocolV2({ deviceInfo });
243
+
244
+ expect(features.mode).toBe('bootloader');
245
+ expect(features.bootloaderMode).toBe(true);
246
+ expect(features.initialized).toBeNull();
247
+ expect(profile.status.mode).toBe('bootloader');
248
+ expect(profile.status.bootloaderMode).toBe(true);
254
249
  });
255
250
 
256
- test('uses GetPassphraseState payloads compatible with Pro series passphrase flow', async () => {
251
+ test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => {
257
252
  const features = normalizeProtocolV2Features(descriptor as any);
258
- // Pro2 版本线独立于 Pro 系列:V2 设备无论固件版本都支持 GetPassphraseState
259
- features.onekey_firmware_version = '1.2.3';
253
+ features.firmwareVersion = '1.2.3';
254
+ features.passphraseProtection = true;
260
255
  const typedCall = jest.fn().mockResolvedValue({
261
- type: 'PassphraseState',
256
+ type: 'DeviceSession',
262
257
  message: {
263
- passphrase_state: 'state-1',
258
+ btc_test_address: 'state-1',
264
259
  session_id: 'session-1',
265
- unlocked_attach_pin: false,
266
260
  },
267
261
  });
268
262
  const commands = { typedCall } as unknown as DeviceCommands;
@@ -273,35 +267,294 @@ describe('Protocol V2 feature adapter', () => {
273
267
  await getPassphraseState(device, {
274
268
  expectPassphraseState: 'state-1',
275
269
  });
276
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
277
- passphrase_state: 'state-1',
278
- });
270
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
271
+
272
+ await expect(
273
+ getPassphraseState(device, {
274
+ expectPassphraseState: 'state-2',
275
+ allowCreateAttachPin: true,
276
+ })
277
+ ).rejects.toEqual(
278
+ expect.objectContaining({
279
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
280
+ })
281
+ );
282
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
279
283
 
280
284
  await getPassphraseState(device, {
281
- expectPassphraseState: 'state-2',
282
- allowCreateAttachPin: true,
285
+ onlyMainPin: true,
286
+ });
287
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
288
+ });
289
+
290
+ test('deviceStatusGet returns raw DeviceStatus without updating features', async () => {
291
+ const typedCall = jest.fn().mockResolvedValue({
292
+ type: 'DeviceStatus',
293
+ message: { device_id: 'device-1', unlocked: true },
283
294
  });
284
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
285
- passphrase_state: 'state-2',
286
- allow_create_attach_pin: true,
295
+ const updateProtocolV2Features = jest.fn();
296
+ const method = new DeviceStatusGet({
297
+ payload: { method: 'deviceStatusGet', connectId: 'connect-id' },
287
298
  });
299
+ method.init();
300
+ method.device = stubDevice({
301
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
302
+ commands: { typedCall },
303
+ updateProtocolV2Features,
304
+ }) as any;
288
305
 
289
- await getPassphraseState(device, {
290
- onlyMainPin: true,
306
+ await expect(method.run()).resolves.toEqual({ device_id: 'device-1', unlocked: true });
307
+ expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
308
+ expect(updateProtocolV2Features).not.toHaveBeenCalled();
309
+ });
310
+
311
+ test('deviceSessionGet maps sessionId and does not mutate wallet cache', async () => {
312
+ const typedCall = jest.fn().mockResolvedValue({
313
+ type: 'DeviceSession',
314
+ message: { session_id: 'new-session', btc_test_address: 'state-a' },
315
+ });
316
+ const updateInternalState = jest.fn();
317
+ const method = new DeviceSessionGet({
318
+ payload: {
319
+ method: 'deviceSessionGet',
320
+ connectId: 'connect-id',
321
+ sessionId: 'cached-session',
322
+ },
323
+ });
324
+ method.init();
325
+ method.device = stubDevice({
326
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
327
+ commands: { typedCall },
328
+ updateInternalState,
329
+ }) as any;
330
+
331
+ await expect(method.run()).resolves.toEqual({
332
+ session_id: 'new-session',
333
+ btc_test_address: 'state-a',
334
+ });
335
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
336
+ session_id: 'cached-session',
337
+ });
338
+ expect(updateInternalState).not.toHaveBeenCalled();
339
+ });
340
+
341
+ test('routes raw status and session methods through CoreApi', async () => {
342
+ const call = jest.fn().mockResolvedValue({ success: true, payload: {} });
343
+ const api = createCoreApi(call as any);
344
+
345
+ await api.deviceStatusGet('connect-id', { retryCount: 1 });
346
+ await api.deviceSessionGet('connect-id', { sessionId: 'cached-session' });
347
+
348
+ expect(call).toHaveBeenNthCalledWith(1, {
349
+ method: 'deviceStatusGet',
350
+ connectId: 'connect-id',
351
+ retryCount: 1,
352
+ });
353
+ expect(call).toHaveBeenNthCalledWith(2, {
354
+ method: 'deviceSessionGet',
355
+ connectId: 'connect-id',
356
+ sessionId: 'cached-session',
357
+ });
358
+ });
359
+
360
+ test('reuses the cached session id for the selected Pro2 wallet', async () => {
361
+ const device = Device.fromDescriptor({
362
+ id: 'cache-device-1',
363
+ path: 'cache-path-1',
364
+ protocolType: 'V2',
365
+ } as any);
366
+ (device as any).features = normalizeProtocolV2Features(
367
+ { ...descriptor, protocolType: 'V2' } as any,
368
+ {
369
+ status: {
370
+ device_id: 'stable-device-1',
371
+ unlocked: true,
372
+ passphrase_enabled: true,
373
+ },
374
+ }
375
+ );
376
+ device.passphraseState = 'state-a';
377
+ preloadSessionCache('stable-device-1', 'state-a', 'session-a');
378
+ const typedCall = jest.fn().mockResolvedValue({
379
+ type: 'DeviceSession',
380
+ message: { session_id: 'session-b', btc_test_address: 'state-a' },
381
+ });
382
+ (device as any).commands = { typedCall };
383
+
384
+ await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
385
+ passphraseState: 'state-a',
386
+ newSession: 'session-b',
387
+ });
388
+
389
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
390
+ session_id: 'session-a',
391
+ });
392
+ expect(device.getInternalState()).toBe('session-b');
393
+ });
394
+
395
+ test('does not reuse another Pro2 wallet session without passphraseState', async () => {
396
+ const device = Device.fromDescriptor({
397
+ id: 'cache-device-2',
398
+ path: 'cache-path-2',
399
+ protocolType: 'V2',
400
+ } as any);
401
+ (device as any).features = normalizeProtocolV2Features(
402
+ { ...descriptor, protocolType: 'V2' } as any,
403
+ { status: { device_id: 'stable-device-2', unlocked: true } }
404
+ );
405
+ preloadSessionCache('stable-device-2', 'state-a', 'session-a');
406
+ const typedCall = jest.fn().mockResolvedValue({
407
+ type: 'DeviceSession',
408
+ message: { session_id: 'main-session' },
409
+ });
410
+ (device as any).commands = { typedCall };
411
+
412
+ await getProtocolV2WalletSession(device);
413
+
414
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
415
+ expect(device.getInternalState()).toBeUndefined();
416
+ });
417
+
418
+ test('clears the selected Pro2 cache entry after invalid session rejection', async () => {
419
+ const device = Device.fromDescriptor({
420
+ id: 'cache-device-3',
421
+ path: 'cache-path-3',
422
+ protocolType: 'V2',
423
+ } as any);
424
+ (device as any).features = normalizeProtocolV2Features(
425
+ { ...descriptor, protocolType: 'V2' } as any,
426
+ {
427
+ status: {
428
+ device_id: 'stable-device-3',
429
+ unlocked: true,
430
+ passphrase_enabled: true,
431
+ },
432
+ }
433
+ );
434
+ device.passphraseState = 'state-a';
435
+ preloadSessionCache('stable-device-3', 'state-a', 'session-a');
436
+ (device as any).commands = {
437
+ typedCall: jest.fn().mockRejectedValue(new Error('Failure_InvalidSession,no error message')),
438
+ };
439
+
440
+ await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Failure_InvalidSession');
441
+ expect(device.getInternalState()).toBeUndefined();
442
+ });
443
+
444
+ test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
445
+ const device = Device.fromDescriptor({
446
+ id: 'cache-device-mismatch',
447
+ path: 'cache-path-mismatch',
448
+ protocolType: 'V2',
449
+ } as any);
450
+ (device as any).features = normalizeProtocolV2Features(
451
+ { ...descriptor, protocolType: 'V2' } as any,
452
+ {
453
+ status: {
454
+ device_id: 'stable-device-mismatch',
455
+ unlocked: true,
456
+ passphrase_enabled: true,
457
+ },
458
+ }
459
+ );
460
+ device.passphraseState = 'state-a';
461
+ preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a');
462
+ (device as any).commands = {
463
+ typedCall: jest.fn().mockResolvedValue({
464
+ type: 'DeviceSession',
465
+ message: { session_id: 'session-b', btc_test_address: 'state-b' },
466
+ }),
467
+ };
468
+
469
+ await expect(
470
+ getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
471
+ ).rejects.toEqual(
472
+ expect.objectContaining({
473
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
474
+ })
475
+ );
476
+ expect(device.getInternalState()).toBeUndefined();
477
+ });
478
+
479
+ test('rejects DeviceSessionGet while cached Pro2 status is locked', async () => {
480
+ const device = Device.fromDescriptor({
481
+ id: 'cache-device-4',
482
+ path: 'cache-path-4',
483
+ protocolType: 'V2',
484
+ } as any);
485
+ (device as any).features = normalizeProtocolV2Features(
486
+ { ...descriptor, protocolType: 'V2' } as any,
487
+ { status: { device_id: 'stable-device-4', unlocked: false } }
488
+ );
489
+ const typedCall = jest.fn();
490
+ (device as any).commands = { typedCall };
491
+
492
+ await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Device is locked');
493
+ expect(typedCall).not.toHaveBeenCalled();
494
+ });
495
+
496
+ test('does not request a Pro2 wallet session before features are initialized', async () => {
497
+ const device = Device.fromDescriptor({
498
+ id: 'cache-device-5',
499
+ path: 'cache-path-5',
500
+ protocolType: 'V2',
501
+ } as any);
502
+ const typedCall = jest.fn();
503
+ (device as any).commands = { typedCall };
504
+
505
+ await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toEqual({
506
+ passphraseState: undefined,
507
+ newSession: undefined,
508
+ unlockedAttachPin: undefined,
509
+ });
510
+ expect(typedCall).not.toHaveBeenCalled();
511
+ });
512
+
513
+ test('merges DeviceStatus into existing Protocol V2 features', async () => {
514
+ const device = Device.fromDescriptor({
515
+ id: 'status-device',
516
+ path: 'status-path',
517
+ protocolType: 'V2',
518
+ } as any);
519
+ (device as any).features = normalizeProtocolV2Features(
520
+ { ...descriptor, protocolType: 'V2' } as any,
521
+ {
522
+ protocol_version: 2,
523
+ fw: { application: { version: '1.2.3' } },
524
+ se1: { application: { version: '4.5.6' } },
525
+ status: { unlocked: false, passphrase_enabled: false },
526
+ }
527
+ );
528
+ const typedCall = jest.fn().mockResolvedValue({
529
+ type: 'DeviceStatus',
530
+ message: {
531
+ device_id: 'stable-status-device',
532
+ unlocked: true,
533
+ passphrase_enabled: true,
534
+ },
291
535
  });
292
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
293
- _only_main_pin: true,
536
+ (device as any).commands = { typedCall };
537
+
538
+ const features = await refreshProtocolV2DeviceStatus(device);
539
+
540
+ expect(features).toMatchObject({
541
+ deviceId: 'stable-status-device',
542
+ unlocked: true,
543
+ passphraseProtection: true,
544
+ firmwareVersion: '1.2.3',
545
+ se01Version: '4.5.6',
294
546
  });
547
+ expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
295
548
  });
296
549
 
297
550
  test('returns unified GetPassphraseState object payload for existing Pro devices', async () => {
298
551
  const features = {
299
- device_id: 'pro-device-id',
300
- onekey_device_type: 'PRO',
301
- onekey_firmware_version: '4.15.0',
302
- passphrase_protection: true,
303
- session_id: 'feature-session',
304
- unlocked_attach_pin: true,
552
+ deviceId: 'pro-device-id',
553
+ deviceType: 'pro',
554
+ firmwareVersion: '4.15.0',
555
+ passphraseProtection: true,
556
+ sessionId: 'feature-session',
557
+ unlockedAttachPin: true,
305
558
  };
306
559
  const typedCall = jest.fn().mockResolvedValue({
307
560
  type: 'PassphraseState',
@@ -319,6 +572,7 @@ describe('Protocol V2 feature adapter', () => {
319
572
  },
320
573
  });
321
574
  method.device = stubDevice({
575
+ originalDescriptor: { ...descriptor, protocolType: 'V1' },
322
576
  features,
323
577
  commands: { typedCall },
324
578
  updateInternalState,
@@ -327,10 +581,10 @@ describe('Protocol V2 feature adapter', () => {
327
581
  }) as any;
328
582
 
329
583
  await expect(method.run()).resolves.toEqual({
330
- passphrase_state: 'state-pro',
331
- session_id: 'session-pro',
332
- unlocked_attach_pin: false,
333
- passphrase_protection: true,
584
+ passphraseState: 'state-pro',
585
+ sessionId: 'session-pro',
586
+ unlockedAttachPin: false,
587
+ passphraseProtection: true,
334
588
  });
335
589
  expect(updateInternalState).toHaveBeenCalledWith(
336
590
  true,
@@ -341,21 +595,20 @@ describe('Protocol V2 feature adapter', () => {
341
595
  );
342
596
  });
343
597
 
344
- test('prefers DeviceProfile for GetPassphraseState response metadata', async () => {
598
+ test('uses features for GetPassphraseState response metadata', async () => {
345
599
  const features = {
346
- device_id: 'legacy-pro-device-id',
347
- onekey_device_type: 'PRO',
348
- onekey_firmware_version: '4.15.0',
349
- passphrase_protection: false,
350
- session_id: 'feature-session',
351
- unlocked_attach_pin: true,
600
+ deviceId: null,
601
+ deviceType: 'pro2',
602
+ firmwareVersion: '4.15.0',
603
+ passphraseProtection: true,
604
+ sessionId: 'feature-session',
605
+ unlockedAttachPin: true,
352
606
  };
353
607
  const typedCall = jest.fn().mockResolvedValue({
354
- type: 'PassphraseState',
608
+ type: 'DeviceSession',
355
609
  message: {
356
- passphrase_state: 'state-pro2',
610
+ btc_test_address: 'state-pro2',
357
611
  session_id: 'session-pro2',
358
- unlocked_attach_pin: false,
359
612
  },
360
613
  });
361
614
  const updateInternalState = jest.fn();
@@ -367,66 +620,90 @@ describe('Protocol V2 feature adapter', () => {
367
620
  },
368
621
  });
369
622
  method.device = stubDevice({
623
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
370
624
  features,
371
- profile: {
372
- protocol: 'V2',
373
- sources: ['deviceInfo'],
374
- deviceType: 'pro2',
375
- firmwareType: 'universal',
376
- deviceId: 'PR2SERIAL',
377
- serialNo: 'PR2SERIAL',
378
- label: null,
379
- bleName: null,
380
- status: {
381
- mode: 'normal',
382
- initialized: true,
383
- bootloaderMode: false,
384
- unlocked: null,
385
- passphraseProtection: true,
386
- backupRequired: false,
387
- noBackup: null,
388
- language: null,
389
- },
390
- versions: {
391
- firmware: '4.15.0',
392
- bootloader: null,
393
- board: null,
394
- ble: null,
395
- },
396
- },
397
625
  commands: { typedCall },
398
626
  updateInternalState,
399
627
  getFeatures,
400
628
  getCurrentDeviceType: () => 'pro2',
401
- getCurrentDeviceId: () => 'PR2SERIAL',
629
+ getCurrentDeviceId: () => undefined,
402
630
  getCurrentPassphraseProtection: () => true,
403
631
  }) as any;
404
632
 
405
633
  await expect(method.run()).resolves.toEqual({
406
- passphrase_state: 'state-pro2',
407
- session_id: 'session-pro2',
408
- unlocked_attach_pin: false,
409
- passphrase_protection: true,
634
+ passphraseState: 'state-pro2',
635
+ sessionId: 'session-pro2',
636
+ unlockedAttachPin: true,
637
+ passphraseProtection: true,
410
638
  });
411
639
  expect(getFeatures).not.toHaveBeenCalled();
412
640
  });
413
641
 
414
- test('uses DeviceProfile identity for Pro2 passphrase session cache', async () => {
642
+ test('honors initSession when getting Pro2 passphrase state', async () => {
643
+ const features = {
644
+ deviceId: 'pro2-device-id',
645
+ deviceType: 'pro2',
646
+ firmwareVersion: '9.9.9',
647
+ passphraseProtection: true,
648
+ sessionId: 'old-feature-session',
649
+ unlockedAttachPin: false,
650
+ };
651
+ const typedCall = jest.fn().mockResolvedValue({
652
+ type: 'DeviceSession',
653
+ message: {
654
+ btc_test_address: 'state-pro2-new',
655
+ },
656
+ });
657
+ const clearInternalState = jest.fn();
658
+ const updateInternalState = jest.fn();
659
+ const method = new GetPassphraseState({
660
+ payload: {
661
+ method: 'getPassphraseState',
662
+ connectId: 'connect-id',
663
+ initSession: true,
664
+ },
665
+ });
666
+ method.device = stubDevice({
667
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
668
+ features,
669
+ commands: { typedCall },
670
+ clearInternalState,
671
+ updateInternalState,
672
+ getCurrentDeviceId: () => 'pro2-device-id',
673
+ getCurrentPassphraseProtection: () => true,
674
+ }) as any;
675
+
676
+ await expect(method.run()).resolves.toEqual({
677
+ passphraseState: 'state-pro2-new',
678
+ sessionId: undefined,
679
+ unlockedAttachPin: false,
680
+ passphraseProtection: true,
681
+ });
682
+ expect(clearInternalState).toHaveBeenCalledTimes(1);
683
+ expect(updateInternalState).toHaveBeenCalledWith(
684
+ true,
685
+ 'state-pro2-new',
686
+ 'pro2-device-id',
687
+ undefined,
688
+ null
689
+ );
690
+ });
691
+
692
+ test('stores Pro2 passphrase session cache without synthetic device id', async () => {
415
693
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
416
694
  const typedCall = jest.fn().mockResolvedValue({
417
- type: 'PassphraseState',
695
+ type: 'DeviceSession',
418
696
  message: {
419
- passphrase_state: 'state-profile',
697
+ btc_test_address: 'state-profile',
420
698
  session_id: 'session-profile',
421
- unlocked_attach_pin: false,
422
699
  },
423
700
  });
424
701
 
425
702
  (device as any).features = {
426
703
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
427
704
  hw: { serial_no: 'PR2SERIAL' },
428
- fw: { app: { version: '4.15.0' } },
429
- status: { passphrase_protection: true },
705
+ fw: { application: { version: '4.15.0' } },
706
+ status: { passphrase_enabled: true },
430
707
  }),
431
708
  unlocked: true,
432
709
  };
@@ -436,17 +713,16 @@ describe('Protocol V2 feature adapter', () => {
436
713
 
437
714
  device.passphraseState = 'state-profile';
438
715
  expect(device.getInternalState()).toBe('session-profile');
439
- expect(device.getInternalState('LEGACY-ID')).toBeUndefined();
716
+ expect(device.getInternalState('CACHED-ID')).toBeUndefined();
440
717
  });
441
718
 
442
719
  test('stores Pro2 passphrase sessions without selecting them implicitly', async () => {
443
720
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
444
721
  const typedCall = jest.fn().mockResolvedValue({
445
- type: 'PassphraseState',
722
+ type: 'DeviceSession',
446
723
  message: {
447
- passphrase_state: 'state-auto',
724
+ btc_test_address: 'state-auto',
448
725
  session_id: 'session-auto',
449
- unlocked_attach_pin: false,
450
726
  },
451
727
  });
452
728
 
@@ -459,8 +735,8 @@ describe('Protocol V2 feature adapter', () => {
459
735
  hw: { serial_no: 'PR2SERIAL' },
460
736
  }
461
737
  );
462
- (device as any).features.onekey_firmware_version = '4.15.0';
463
- (device as any).features.passphrase_protection = true;
738
+ (device as any).features.firmwareVersion = '4.15.0';
739
+ (device as any).features.passphraseProtection = true;
464
740
  (device as any).features.unlocked = true;
465
741
  (device as any).commands = { typedCall };
466
742
 
@@ -470,23 +746,20 @@ describe('Protocol V2 feature adapter', () => {
470
746
  });
471
747
 
472
748
  expect(device.passphraseState).toBeUndefined();
473
- expect(device.features?.passphrase_protection).toBe(true);
474
- expect(device.features?.session_id).toBeNull();
749
+ expect(device.features?.passphraseProtection).toBe(true);
750
+ expect(device.features?.sessionId).toBeNull();
475
751
  expect(device.getInternalState()).toBeUndefined();
476
752
  device.passphraseState = 'state-auto';
477
753
  expect(device.getInternalState()).toBe('session-auto');
478
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
479
- passphrase_state: undefined,
480
- });
754
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
481
755
  });
482
756
 
483
757
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
484
758
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
485
759
  const typedCall = jest.fn().mockResolvedValue({
486
- type: 'PassphraseState',
760
+ type: 'DeviceSession',
487
761
  message: {
488
762
  session_id: 'main-pin-session',
489
- unlocked_attach_pin: false,
490
763
  },
491
764
  });
492
765
 
@@ -497,11 +770,11 @@ describe('Protocol V2 feature adapter', () => {
497
770
  } as any,
498
771
  {
499
772
  status: {
500
- passphrase_protection: false,
773
+ passphrase_enabled: false,
501
774
  },
502
775
  }
503
776
  );
504
- (device as any).features.onekey_firmware_version = '4.15.0';
777
+ (device as any).features.firmwareVersion = '4.15.0';
505
778
  (device as any).features.unlocked = true;
506
779
  (device as any).commands = { typedCall };
507
780
 
@@ -512,22 +785,19 @@ describe('Protocol V2 feature adapter', () => {
512
785
  newSession: 'main-pin-session',
513
786
  });
514
787
 
515
- expect(device.features?.passphrase_protection).toBe(false);
516
- expect(device.features?.session_id).toBeNull();
788
+ expect(device.features?.passphraseProtection).toBe(false);
789
+ expect(device.features?.sessionId).toBeNull();
517
790
  expect(device.getInternalState()).toBeUndefined();
518
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
519
- _only_main_pin: true,
520
- });
791
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
521
792
  });
522
793
 
523
794
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
524
795
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
525
796
  const typedCall = jest.fn().mockResolvedValue({
526
- type: 'PassphraseState',
797
+ type: 'DeviceSession',
527
798
  message: {
528
- passphrase_state: 'wrong-state',
799
+ btc_test_address: 'wrong-state',
529
800
  session_id: 'wrong-session',
530
- unlocked_attach_pin: false,
531
801
  },
532
802
  });
533
803
 
@@ -535,32 +805,52 @@ describe('Protocol V2 feature adapter', () => {
535
805
  ...descriptor,
536
806
  protocolType: 'V2',
537
807
  } as any);
538
- (device as any).features.onekey_firmware_version = '4.15.0';
539
- (device as any).features.passphrase_protection = true;
808
+ (device as any).features.firmwareVersion = '4.15.0';
809
+ (device as any).features.passphraseProtection = true;
540
810
  (device as any).commands = { typedCall };
541
811
 
542
- await expect(device.checkPassphraseStateSafety('expected-state', false, true)).resolves.toBe(
543
- false
812
+ await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
813
+ expect.objectContaining({
814
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
815
+ })
544
816
  );
545
817
 
546
818
  expect(device.getInternalState()).toBeUndefined();
547
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetPassphraseState', 'PassphraseState', {
548
- passphrase_state: 'expected-state',
549
- });
819
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {});
550
820
  });
551
821
 
552
822
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
553
823
  const features = normalizeProtocolV2Features(descriptor as any);
554
824
 
555
- expect(features.device_id).toBe('');
556
- expect(features.serial_no).toBe('');
557
- expect(features.onekey_serial_no).toBe('');
558
- expect(features.initialized).toBe(false);
559
- expect(features.unlocked).toBe(false);
560
- expect(features.firmware_present).toBe(false);
825
+ expect(features.deviceId).toBeNull();
826
+ expect(features.serialNo).toBe('');
827
+ expect(features.initialized).toBeNull();
828
+ expect(features.unlocked).toBeNull();
829
+ expect(features.firmwarePresent).toBeNull();
830
+ });
831
+
832
+ test('uses Protocol V2 status.device_id and does not fall back to serial_no', () => {
833
+ const features = normalizeProtocolV2Features(
834
+ {
835
+ id: 'PR2000000000',
836
+ path: 'PR2000000000',
837
+ protocolType: 'V2',
838
+ } as any,
839
+ {
840
+ hw: {
841
+ serial_no: 'PR9999999999',
842
+ },
843
+ status: {
844
+ device_id: 'DEVICE-ID-9999',
845
+ },
846
+ }
847
+ );
848
+
849
+ expect(features.deviceId).toBe('DEVICE-ID-9999');
850
+ expect(features.serialNo).toBe('PR9999999999');
561
851
  });
562
852
 
563
- test('uses Protocol V2 DeviceInfo serial_no as device_id instead of descriptor id', () => {
853
+ test('does not use Protocol V2 serial_no as deviceId when hw.device_id is absent', () => {
564
854
  const features = normalizeProtocolV2Features(
565
855
  {
566
856
  id: 'PR2000000000',
@@ -574,140 +864,83 @@ describe('Protocol V2 feature adapter', () => {
574
864
  }
575
865
  );
576
866
 
577
- expect(features.device_id).toBe('PR9999999999');
578
- expect(features.onekey_serial_no).toBe('PR9999999999');
579
- expect(features.serial_no).toBe('PR9999999999');
867
+ expect(features.deviceId).toBeNull();
868
+ expect(features.serialNo).toBe('PR9999999999');
580
869
  });
581
870
 
582
- test('does not expose legacy feature ids when Protocol V2 profile is incomplete', () => {
871
+ test('uses Protocol V2 features directly when profile is absent', () => {
583
872
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
584
873
  (device as any).features = {
585
874
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
586
- device_id: 'LEGACY-ID',
587
- serial_no: 'LEGACY-SERIAL',
588
- label: 'Legacy Label',
589
- ble_name: 'Legacy BLE',
590
- passphrase_protection: true,
875
+ serialNo: 'CACHED-SERIAL',
876
+ label: 'Cached Label',
877
+ bleName: 'Cached BLE',
878
+ passphraseProtection: true,
591
879
  };
592
- device.updateProfile({
593
- protocol: 'V2',
594
- sources: ['deviceInfo'],
595
- deviceType: 'pro2',
596
- firmwareType: 'universal',
597
- deviceId: '',
598
- serialNo: '',
599
- label: null,
600
- bleName: null,
601
- status: {
602
- mode: 'normal',
603
- initialized: true,
604
- bootloaderMode: false,
605
- unlocked: null,
606
- passphraseProtection: null,
607
- backupRequired: false,
608
- noBackup: null,
609
- language: null,
610
- },
611
- versions: {
612
- firmware: null,
613
- bootloader: null,
614
- board: null,
615
- ble: null,
616
- },
617
- });
618
880
 
619
881
  expect(device.toMessageObject()).toMatchObject({
620
- uuid: '',
882
+ uuid: 'CACHED-SERIAL',
621
883
  deviceId: null,
622
- bleName: null,
623
- label: 'OneKey Pro2',
884
+ bleName: 'Cached BLE',
885
+ label: 'Cached Label',
624
886
  deviceType: 'pro2',
625
887
  });
626
- expect(device.getCurrentPassphraseProtection()).toBeNull();
888
+ expect(device.getCurrentPassphraseProtection()).toBe(true);
627
889
  expect(device.hasUsePassphrase()).toBe(true);
628
- expect(device.isInitialized()).toBe(true);
629
- expect(device.getMode()).toBe('normal');
630
- expect(device.getFirmwareVersion()).toBeNull();
631
890
  });
632
891
 
633
- test('keeps Protocol V2 cached profile as identity source when syncing legacy features', () => {
892
+ test('syncs Protocol V2 cached features without cached profile', () => {
634
893
  const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
635
894
  (cached as any).features = {
636
895
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
637
- device_id: 'LEGACY-ID',
638
- serial_no: 'LEGACY-SERIAL',
639
- onekey_serial_no: 'LEGACY-SERIAL',
896
+ deviceId: null,
897
+ serialNo: 'CACHED-SERIAL',
640
898
  };
641
- cached.updateProfile({
642
- protocol: 'V2',
643
- sources: ['deviceInfo'],
644
- deviceType: 'pro2',
645
- firmwareType: 'universal',
646
- deviceId: '',
647
- serialNo: '',
648
- label: null,
649
- bleName: null,
650
- status: {
651
- mode: 'normal',
652
- initialized: true,
653
- bootloaderMode: false,
654
- unlocked: null,
655
- passphraseProtection: null,
656
- backupRequired: false,
657
- noBackup: null,
658
- language: null,
659
- },
660
- versions: {
661
- firmware: null,
662
- bootloader: null,
663
- board: null,
664
- ble: null,
665
- },
666
- });
667
899
 
668
900
  const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
669
901
  current.updateFromCache(cached);
670
902
 
671
903
  expect(current.getCurrentDeviceId()).toBeUndefined();
672
- expect(current.getCurrentSerialNo()).toBe('');
904
+ expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
673
905
  expect(current.toMessageObject()).toMatchObject({
674
- uuid: '',
906
+ uuid: 'CACHED-SERIAL',
675
907
  deviceId: null,
676
908
  });
677
909
  });
678
910
 
679
- test('initializes Protocol V2 features from lightweight DeviceGetDeviceInfo', async () => {
911
+ test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => {
680
912
  const commands = {
681
913
  typedCall: jest.fn().mockResolvedValueOnce({
682
914
  type: 'DeviceInfo',
683
915
  message: {
684
916
  hw: { serial_no: 'PR2SERIAL' },
685
917
  status: {
918
+ device_id: 'PRO2-STATUS-ID',
686
919
  init_states: true,
687
- passphrase_protection: true,
920
+ passphrase_enabled: true,
688
921
  },
689
922
  },
690
923
  }),
691
924
  };
692
925
 
693
- const features = await requestProtocolV2LegacyFeatures({
926
+ const features = await requestProtocolV2Features({
694
927
  commands: commands as unknown as DeviceCommands,
695
928
  descriptor: descriptor as any,
696
929
  });
697
930
 
698
- expect(features.device_id).toBe('PR2SERIAL');
931
+ expect(features.deviceId).toBe('PRO2-STATUS-ID');
699
932
  expect(features.initialized).toBe(true);
700
- expect(features.passphrase_protection).toBe(true);
933
+ expect(features.passphraseProtection).toBe(true);
701
934
  expect(commands.typedCall).toHaveBeenCalledTimes(1);
702
935
  expect(commands.typedCall).toHaveBeenNthCalledWith(
703
936
  1,
704
- 'DevGetDeviceInfo',
937
+ 'DeviceInfoGet',
705
938
  'DeviceInfo',
706
939
  {
707
940
  targets: {
708
941
  hw: true,
709
942
  fw: true,
710
- bt: true,
943
+ coprocessor: true,
711
944
  status: true,
712
945
  },
713
946
  types: {
@@ -719,13 +952,13 @@ describe('Protocol V2 feature adapter', () => {
719
952
  );
720
953
  });
721
954
 
722
- test('fails initialization when Protocol V2 DeviceGetDeviceInfo fails', async () => {
955
+ test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => {
723
956
  const commands = {
724
957
  typedCall: jest.fn().mockRejectedValueOnce(new Error('DeviceInfo not supported')),
725
958
  };
726
959
 
727
960
  await expect(
728
- requestProtocolV2LegacyFeatures({
961
+ requestProtocolV2Features({
729
962
  commands: commands as unknown as DeviceCommands,
730
963
  descriptor: descriptor as any,
731
964
  })
@@ -738,20 +971,21 @@ describe('Protocol V2 feature adapter', () => {
738
971
  message: {
739
972
  hw: { serial_no: 'PR2SERIAL' },
740
973
  fw: {
741
- app: {
974
+ application: {
742
975
  version: '5.6.7',
743
976
  },
744
977
  },
745
- bt: {
746
- app: {
978
+ coprocessor: {
979
+ application: {
747
980
  version: '8.9.10',
748
981
  },
749
- adv_name: 'Raw Pro2 BLE',
982
+ bt_adv_name: 'Raw Pro2 BLE',
750
983
  },
751
984
  status: {
985
+ device_id: 'PRO2-BASIC-ID',
986
+ unlocked_by_attach_to_pin: true,
752
987
  init_states: true,
753
- label: 'Raw Pro2',
754
- passphrase_protection: true,
988
+ passphrase_enabled: true,
755
989
  },
756
990
  },
757
991
  });
@@ -768,27 +1002,26 @@ describe('Protocol V2 feature adapter', () => {
768
1002
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
769
1003
  features: {
770
1004
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
771
- device_id: 'LEGACY-ID',
772
- serial_no: 'LEGACY-SERIAL',
773
- label: 'Legacy Pro2',
1005
+ device_id: 'CACHED-ID',
1006
+ serial_no: 'CACHED-SERIAL',
1007
+ label: 'Cached Pro2',
774
1008
  onekey_firmware_version: '1.2.3',
775
1009
  onekey_ble_version: '2.3.4',
776
1010
  },
777
1011
  commands: { typedCall },
778
1012
  _updateFeatures: jest.fn(),
779
- updateProfile: jest.fn(),
780
1013
  });
781
1014
 
782
1015
  const result = await method.run();
783
1016
 
784
1017
  expect(typedCall).toHaveBeenCalledWith(
785
- 'DevGetDeviceInfo',
1018
+ 'DeviceInfoGet',
786
1019
  'DeviceInfo',
787
1020
  {
788
1021
  targets: {
789
1022
  hw: true,
790
1023
  fw: true,
791
- bt: true,
1024
+ coprocessor: true,
792
1025
  status: true,
793
1026
  },
794
1027
  types: {
@@ -801,12 +1034,13 @@ describe('Protocol V2 feature adapter', () => {
801
1034
  expect(result).toMatchObject({
802
1035
  protocol: 'V2',
803
1036
  deviceType: 'pro2',
804
- deviceId: 'PR2SERIAL',
1037
+ deviceId: 'PRO2-BASIC-ID',
805
1038
  serialNo: 'PR2SERIAL',
806
- label: 'Raw Pro2',
1039
+ label: null,
807
1040
  bleName: 'Raw Pro2 BLE',
808
1041
  status: {
809
1042
  initialized: true,
1043
+ unlockedAttachPin: true,
810
1044
  passphraseProtection: true,
811
1045
  },
812
1046
  versions: {
@@ -816,7 +1050,7 @@ describe('Protocol V2 feature adapter', () => {
816
1050
  });
817
1051
  });
818
1052
 
819
- test('does not fill Protocol V2 DeviceProfile from legacy features', async () => {
1053
+ test('does not fill Protocol V2 DeviceProfile from cached V1-shaped fields', async () => {
820
1054
  const typedCall = jest.fn().mockResolvedValueOnce({
821
1055
  type: 'DeviceInfo',
822
1056
  message: {
@@ -837,26 +1071,25 @@ describe('Protocol V2 feature adapter', () => {
837
1071
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
838
1072
  features: {
839
1073
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
840
- device_id: 'LEGACY-ID',
841
- serial_no: 'LEGACY-SERIAL',
842
- label: 'Legacy Pro2',
843
- ble_name: 'Legacy BLE',
1074
+ device_id: 'STALE-ID',
1075
+ serial_no: 'STALE-SERIAL',
1076
+ label: 'Stale Pro2',
1077
+ ble_name: 'Stale BLE',
844
1078
  passphrase_protection: true,
845
1079
  onekey_firmware_version: '1.2.3',
846
1080
  onekey_ble_version: '2.3.4',
847
1081
  },
848
1082
  commands: { typedCall },
849
- updateProfile: jest.fn(),
850
1083
  });
851
1084
 
852
1085
  const result = await method.run();
853
1086
 
854
1087
  expect(result).toMatchObject({
855
1088
  protocol: 'V2',
856
- // 身份字段不得取自 legacy features(LEGACY-ID / LEGACY-SERIAL);
857
- // hw.serial_no 缺失时回退 descriptor.path(早期工程板无 serial 的 mock 身份)。
858
- deviceId: 'usb-path',
859
- serialNo: 'usb-path',
1089
+ // 身份字段不得取自缓存的 V1-shaped features(STALE-ID / STALE-SERIAL),
1090
+ // hw.serial_no 缺失时也不回退 descriptor.path
1091
+ deviceId: '',
1092
+ serialNo: '',
860
1093
  label: null,
861
1094
  bleName: null,
862
1095
  status: {
@@ -896,13 +1129,13 @@ describe('Protocol V2 feature adapter', () => {
896
1129
  await method.run();
897
1130
 
898
1131
  expect(typedCall).toHaveBeenCalledWith(
899
- 'DevGetDeviceInfo',
1132
+ 'DeviceInfoGet',
900
1133
  'DeviceInfo',
901
1134
  {
902
1135
  targets: {
903
1136
  hw: true,
904
1137
  fw: true,
905
- bt: true,
1138
+ coprocessor: true,
906
1139
  se1: true,
907
1140
  se2: true,
908
1141
  se3: true,
@@ -926,12 +1159,12 @@ describe('Protocol V2 feature adapter', () => {
926
1159
  message: {
927
1160
  hw: { serial_no: 'PR2SERIAL' },
928
1161
  se1: {
929
- app: { version: '1.0.1' },
930
- boot: { version: '1.0.0' },
1162
+ application: { version: '1.0.1' },
1163
+ bootloader: { version: '1.0.0' },
931
1164
  },
932
1165
  se2: {
933
- app: { version: '2.0.1' },
934
- boot: { version: '2.0.0' },
1166
+ application: { version: '2.0.1' },
1167
+ bootloader: { version: '2.0.0' },
935
1168
  },
936
1169
  status: { init_states: true },
937
1170
  },
@@ -948,19 +1181,18 @@ describe('Protocol V2 feature adapter', () => {
948
1181
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
949
1182
  features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
950
1183
  commands: { typedCall },
951
- updateProfile: jest.fn(),
952
1184
  });
953
1185
 
954
1186
  const result = await method.run();
955
1187
 
956
1188
  expect(typedCall).toHaveBeenCalledWith(
957
- 'DevGetDeviceInfo',
1189
+ 'DeviceInfoGet',
958
1190
  'DeviceInfo',
959
1191
  {
960
1192
  targets: {
961
1193
  hw: true,
962
1194
  fw: true,
963
- bt: true,
1195
+ coprocessor: true,
964
1196
  se1: true,
965
1197
  se2: true,
966
1198
  se3: true,
@@ -999,10 +1231,23 @@ describe('Protocol V2 feature adapter', () => {
999
1231
  expect(checkedTypes).toEqual(['pro2']);
1000
1232
  });
1001
1233
 
1234
+ test('uses firmware-v1 as the Pro2 remote firmware config field', () => {
1235
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
1236
+
1237
+ expect(
1238
+ getFirmwareUpdateField({
1239
+ features,
1240
+ updateType: 'firmware',
1241
+ firmwareType: 'universal',
1242
+ })
1243
+ ).toBe('firmware-v1');
1244
+ expect(getFirmwareUpdateFieldArray(features, 'firmware')).toEqual(['firmware-v1']);
1245
+ });
1246
+
1002
1247
  test('marks known unsupported public-chain methods as unsupported on Protocol V2', () => {
1003
1248
  const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
1004
1249
  fw: {
1005
- app: {
1250
+ application: {
1006
1251
  version: '9.9.9',
1007
1252
  },
1008
1253
  },
@@ -1052,7 +1297,7 @@ describe('Protocol V2 feature adapter', () => {
1052
1297
  expect(isMethodVersionRangeUnsupported(neuraiRange)).toBe(true);
1053
1298
  });
1054
1299
 
1055
- test('does not block legacy batch public key support checks on Protocol V2', async () => {
1300
+ test('does not block batch public key support checks on Protocol V2', async () => {
1056
1301
  const paths = [{ address_n: [0x8000002c, 0x80000000, 0x80000000] }] as any;
1057
1302
  const typedCall = jest.fn().mockResolvedValue({
1058
1303
  type: 'EcdsaPublicKeys',
@@ -1066,7 +1311,7 @@ describe('Protocol V2 feature adapter', () => {
1066
1311
  originalDescriptor: { protocolType: 'V2' },
1067
1312
  features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
1068
1313
  fw: {
1069
- app: {
1314
+ application: {
1070
1315
  version: '4.14.0',
1071
1316
  },
1072
1317
  },
@@ -1088,7 +1333,7 @@ describe('Protocol V2 feature adapter', () => {
1088
1333
  });
1089
1334
  });
1090
1335
 
1091
- test('returns Protocol V2 oneKey fields without calling legacy OnekeyGetFeatures', async () => {
1336
+ test('does not call V1 OnekeyGetFeatures for Protocol V2 devices', async () => {
1092
1337
  const method = new GetOnekeyFeatures({
1093
1338
  id: 1,
1094
1339
  payload: {
@@ -1100,9 +1345,9 @@ describe('Protocol V2 feature adapter', () => {
1100
1345
  message: {
1101
1346
  protocol_version: 1,
1102
1347
  hw: { serial_no: 'PR2SERIAL' },
1103
- fw: { app: { version: '1.2.3', build_id: 'app-build' } },
1104
- bt: { adv_name: 'Pro2 BLE' },
1105
- status: { label: 'ignored label' },
1348
+ fw: { application: { version: '1.2.3', build_id: 'app-build' } },
1349
+ coprocessor: { bt_adv_name: 'Pro2 BLE' },
1350
+ status: { init_states: true },
1106
1351
  },
1107
1352
  });
1108
1353
 
@@ -1113,10 +1358,10 @@ describe('Protocol V2 feature adapter', () => {
1113
1358
 
1114
1359
  const message = await method.run();
1115
1360
 
1116
- // V2 走 DeviceGetDeviceInfo 完整请求(含 SE 与 hash/build_id),而不是 legacy OnekeyGetFeatures
1361
+ // V2 走 DeviceInfoGet 完整请求(含 SE 与 hash/build_id),而不是 V1 OnekeyGetFeatures
1117
1362
  expect(typedCall).toHaveBeenCalledTimes(1);
1118
1363
  expect(typedCall).toHaveBeenCalledWith(
1119
- 'DevGetDeviceInfo',
1364
+ 'DeviceInfoGet',
1120
1365
  'DeviceInfo',
1121
1366
  expect.objectContaining({
1122
1367
  targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }),
@@ -1124,17 +1369,11 @@ describe('Protocol V2 feature adapter', () => {
1124
1369
  }),
1125
1370
  expect.anything()
1126
1371
  );
1127
- expect(message).toMatchObject({
1128
- onekey_device_type: 'pro2',
1129
- onekey_firmware_version: '1.2.3',
1130
- onekey_firmware_build_id: 'app-build',
1131
- onekey_serial_no: 'PR2SERIAL',
1132
- onekey_ble_name: 'Pro2 BLE',
1133
- });
1372
+ expect(message).toEqual({});
1134
1373
  expect(message).not.toHaveProperty('label');
1135
1374
  });
1136
1375
 
1137
- test('refreshes cached Protocol V2 profile with a lightweight status request on later runs', async () => {
1376
+ test('refreshes cached Protocol V2 features with a lightweight status request on later runs', async () => {
1138
1377
  const device = Device.fromDescriptor({
1139
1378
  path: 'usb-path',
1140
1379
  protocolType: 'V2',
@@ -1145,9 +1384,10 @@ describe('Protocol V2 feature adapter', () => {
1145
1384
  type: 'DeviceInfo',
1146
1385
  message: {
1147
1386
  hw: { serial_no: 'PR2SERIAL' },
1148
- fw: { app: { version: '1.2.3' } },
1387
+ fw: { application: { version: '1.2.3' } },
1149
1388
  status: {
1150
- passphrase_protection: true,
1389
+ device_id: 'PRO2-REFRESH-ID',
1390
+ passphrase_enabled: true,
1151
1391
  },
1152
1392
  },
1153
1393
  })
@@ -1157,8 +1397,8 @@ describe('Protocol V2 feature adapter', () => {
1157
1397
  message: {
1158
1398
  hw: { serial_no: 'PR2SERIAL' },
1159
1399
  status: {
1160
- passphrase_protection: false,
1161
- label: 'renamed',
1400
+ device_id: 'PRO2-REFRESH-ID',
1401
+ passphrase_enabled: false,
1162
1402
  },
1163
1403
  },
1164
1404
  });
@@ -1169,27 +1409,27 @@ describe('Protocol V2 feature adapter', () => {
1169
1409
  await device.initialize();
1170
1410
 
1171
1411
  expect(device.features).toMatchObject({
1172
- device_id: 'PR2SERIAL',
1173
- onekey_firmware_version: '1.2.3',
1174
- passphrase_protection: false,
1175
- label: 'renamed',
1412
+ deviceId: 'PRO2-REFRESH-ID',
1413
+ firmwareVersion: '1.2.3',
1414
+ passphraseProtection: false,
1415
+ label: null,
1176
1416
  });
1177
- expect(device.profile?.deviceId).toBe('PR2SERIAL');
1417
+ expect((device as any).profile).toBeUndefined();
1178
1418
  // status 字段被第二次刷新更新
1179
- expect(device.profile?.status.passphraseProtection).toBe(false);
1180
- expect(device.profile?.label).toBe('renamed');
1419
+ expect(device.features?.passphraseProtection).toBe(false);
1420
+ expect(device.features?.label).toBeNull();
1181
1421
  // 轻量刷新不含 fw target,已有版本信息按字段级合并保留
1182
- expect(device.profile?.versions.firmware).toBe('1.2.3');
1422
+ expect(device.features?.firmwareVersion).toBe('1.2.3');
1183
1423
  expect(typedCall).toHaveBeenCalledTimes(2);
1184
1424
  expect(typedCall).toHaveBeenNthCalledWith(
1185
1425
  1,
1186
- 'DevGetDeviceInfo',
1426
+ 'DeviceInfoGet',
1187
1427
  'DeviceInfo',
1188
1428
  {
1189
1429
  targets: {
1190
1430
  hw: true,
1191
1431
  fw: true,
1192
- bt: true,
1432
+ coprocessor: true,
1193
1433
  status: true,
1194
1434
  },
1195
1435
  types: {
@@ -1201,15 +1441,15 @@ describe('Protocol V2 feature adapter', () => {
1201
1441
  timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
1202
1442
  }
1203
1443
  );
1204
- // 第二次为 status-only 轻量请求(hw/bt 仅用于身份字段,避免顶层覆盖清空)
1444
+ // 第二次为 status-only 轻量请求(hw/coprocessor 仅用于身份字段,避免顶层覆盖清空)
1205
1445
  expect(typedCall).toHaveBeenNthCalledWith(
1206
1446
  2,
1207
- 'DevGetDeviceInfo',
1447
+ 'DeviceInfoGet',
1208
1448
  'DeviceInfo',
1209
1449
  {
1210
1450
  targets: {
1211
1451
  hw: true,
1212
- bt: true,
1452
+ coprocessor: true,
1213
1453
  status: true,
1214
1454
  },
1215
1455
  types: {
@@ -1223,7 +1463,7 @@ describe('Protocol V2 feature adapter', () => {
1223
1463
  );
1224
1464
  });
1225
1465
 
1226
- test('refreshes Protocol V2 features without falling back to legacy GetFeatures', async () => {
1466
+ test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
1227
1467
  const device = Device.fromDescriptor({
1228
1468
  path: 'usb-path',
1229
1469
  protocolType: 'V2',
@@ -1234,7 +1474,7 @@ describe('Protocol V2 feature adapter', () => {
1234
1474
  type: 'DeviceInfo',
1235
1475
  message: {
1236
1476
  hw: { serial_no: 'PR2SERIAL' },
1237
- fw: { app: { version: '1.2.3' } },
1477
+ fw: { application: { version: '1.2.3' } },
1238
1478
  status: { init_states: true },
1239
1479
  },
1240
1480
  })
@@ -1242,8 +1482,8 @@ describe('Protocol V2 feature adapter', () => {
1242
1482
  type: 'DeviceInfo',
1243
1483
  message: {
1244
1484
  hw: { serial_no: 'PR2SERIAL' },
1245
- fw: { app: { version: '1.2.4' } },
1246
- status: { init_states: true, passphrase_protection: true },
1485
+ fw: { application: { version: '1.2.4' } },
1486
+ status: { init_states: true, passphrase_enabled: true },
1247
1487
  },
1248
1488
  });
1249
1489
 
@@ -1253,26 +1493,26 @@ describe('Protocol V2 feature adapter', () => {
1253
1493
  const features = await device.getFeatures();
1254
1494
 
1255
1495
  expect(device.features).toMatchObject({
1256
- device_id: 'PR2SERIAL',
1257
- onekey_firmware_version: '1.2.4',
1258
- passphrase_protection: true,
1496
+ deviceId: null,
1497
+ firmwareVersion: '1.2.4',
1498
+ passphraseProtection: true,
1259
1499
  });
1260
1500
  expect(features).toMatchObject({
1261
- onekey_device_type: 'pro2',
1262
- onekey_serial_no: 'PR2SERIAL',
1263
- onekey_firmware_version: '1.2.4',
1264
- passphrase_protection: true,
1501
+ deviceType: 'pro2',
1502
+ serialNo: 'PR2SERIAL',
1503
+ firmwareVersion: '1.2.4',
1504
+ passphraseProtection: true,
1265
1505
  });
1266
1506
  expect(typedCall).toHaveBeenCalledTimes(2);
1267
1507
  expect(typedCall).toHaveBeenNthCalledWith(
1268
1508
  2,
1269
- 'DevGetDeviceInfo',
1509
+ 'DeviceInfoGet',
1270
1510
  'DeviceInfo',
1271
1511
  {
1272
1512
  targets: {
1273
1513
  hw: true,
1274
1514
  fw: true,
1275
- bt: true,
1515
+ coprocessor: true,
1276
1516
  status: true,
1277
1517
  },
1278
1518
  types: {
@@ -1285,14 +1525,14 @@ describe('Protocol V2 feature adapter', () => {
1285
1525
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1286
1526
  });
1287
1527
 
1288
- test('keeps Protocol V2 features available for legacy method internals such as evmSignTypedData', async () => {
1528
+ test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
1289
1529
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1290
1530
  const typedCall = jest.fn().mockResolvedValueOnce({
1291
1531
  type: 'DeviceInfo',
1292
1532
  message: {
1293
1533
  hw: { serial_no: 'PR2SERIAL' },
1294
- fw: { app: { version: '1.2.4' } },
1295
- status: { init_states: true, passphrase_protection: true },
1534
+ fw: { application: { version: '1.2.4' } },
1535
+ status: { init_states: true, passphrase_enabled: true },
1296
1536
  },
1297
1537
  });
1298
1538
  (device as any).commands = { typedCall };
@@ -1319,7 +1559,6 @@ describe('Protocol V2 feature adapter', () => {
1319
1559
  });
1320
1560
  method.init();
1321
1561
  (method as any).device = stubDevice({
1322
- profile: device.profile,
1323
1562
  features: device.features,
1324
1563
  commands: { typedCall: jest.fn() },
1325
1564
  });
@@ -1330,90 +1569,155 @@ describe('Protocol V2 feature adapter', () => {
1330
1569
  await expect(method.run()).resolves.toEqual({ address: '0x0', signature: '0x1' });
1331
1570
  });
1332
1571
 
1333
- test('unlocks Protocol V2 devices via UnLockDevice regardless of Pro-series version gates', async () => {
1572
+ test('unlocks Protocol V2 devices via DeviceSessionAskPin regardless of Pro-series version gates', async () => {
1334
1573
  const device = Device.fromDescriptor({
1335
1574
  path: 'usb-path',
1336
1575
  protocolType: 'V2',
1337
1576
  } as any);
1338
- const typedCall = jest.fn().mockResolvedValue({
1339
- type: 'UnLockDeviceResponse',
1340
- message: { unlocked: true, passphrase_protection: true },
1577
+ const typedCall = jest.fn().mockImplementation(requestType => {
1578
+ if (requestType === 'DeviceSessionAskPin') {
1579
+ return {
1580
+ type: 'DeviceSessionPinResult',
1581
+ message: { unlocked: false, passphrase_protection: false },
1582
+ };
1583
+ }
1584
+ if (requestType === 'DeviceStatusGet') {
1585
+ return {
1586
+ type: 'DeviceStatus',
1587
+ message: {
1588
+ device_id: 'PRO2-DEVICE-ID',
1589
+ unlocked: true,
1590
+ passphrase_enabled: true,
1591
+ },
1592
+ };
1593
+ }
1594
+ throw new Error(`Unexpected request: ${requestType}`);
1341
1595
  });
1342
1596
 
1343
1597
  (device as any).commands = { typedCall };
1344
1598
  // Pro2 版本线独立于 Pro(这里是 1.2.3,不满足 Pro 系列 4.15.0 门槛),
1345
- // Protocol V2 固件从首个版本即支持 UnLockDevice,不走 GetAddress 探测回退。
1599
+ // Protocol V2 走独立的设备端 PIN 解锁流程,不走 Pro 系列版本门槛或 GetAddress 探测回退。
1346
1600
  (device as any).features = normalizeProtocolV2Features(
1347
1601
  { ...descriptor, protocolType: 'V2' } as any,
1348
1602
  {
1349
1603
  hw: { serial_no: 'PR2SERIAL' },
1350
- fw: { app: { version: '1.2.3' } },
1604
+ fw: { application: { version: '1.2.3' } },
1351
1605
  status: { init_states: true },
1352
1606
  }
1353
1607
  );
1354
1608
 
1355
1609
  const features = await device.unlockDevice();
1356
1610
 
1357
- expect(typedCall).toHaveBeenCalledWith('UnLockDevice', 'UnLockDeviceResponse');
1611
+ expect(typedCall.mock.calls).toEqual([
1612
+ ['DeviceSessionAskPin', 'DeviceSessionPinResult'],
1613
+ ['DeviceStatusGet', 'DeviceStatus', {}],
1614
+ ]);
1358
1615
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
1359
1616
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1360
1617
  expect(features).toMatchObject({
1361
- onekey_device_type: 'pro2',
1362
- device_id: 'PR2SERIAL',
1363
- onekey_firmware_version: '1.2.3',
1618
+ deviceType: 'pro2',
1619
+ deviceId: 'PRO2-DEVICE-ID',
1620
+ firmwareVersion: '1.2.3',
1364
1621
  unlocked: true,
1622
+ passphraseProtection: true,
1365
1623
  });
1366
1624
  });
1367
1625
 
1368
- test('syncs Protocol V2 profile passphrase state after unlock response', async () => {
1626
+ test('syncs Protocol V2 features passphrase state from DeviceStatusGet after unlock', async () => {
1369
1627
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1370
1628
  (device as any).features = normalizeProtocolV2Features(
1371
1629
  { ...descriptor, protocolType: 'V2' } as any,
1372
1630
  {
1373
1631
  hw: { serial_no: 'PR2SERIAL' },
1374
- fw: { app: { version: '4.15.0' } },
1375
- status: { passphrase_protection: false },
1632
+ fw: { application: { version: '4.15.0' } },
1633
+ status: { passphrase_enabled: false },
1376
1634
  }
1377
1635
  );
1378
- device.updateProfile({
1379
- protocol: 'V2',
1380
- sources: ['deviceInfo'],
1381
- deviceType: 'pro2',
1382
- firmwareType: 'universal',
1383
- deviceId: 'PR2SERIAL',
1384
- serialNo: 'PR2SERIAL',
1385
- label: null,
1386
- bleName: null,
1387
- status: {
1388
- mode: 'normal',
1389
- initialized: true,
1390
- bootloaderMode: false,
1391
- unlocked: null,
1392
- passphraseProtection: false,
1393
- backupRequired: false,
1394
- noBackup: null,
1395
- language: null,
1396
- },
1397
- versions: {
1398
- firmware: '4.15.0',
1399
- bootloader: null,
1400
- board: null,
1401
- ble: null,
1402
- },
1636
+ const typedCall = jest.fn().mockImplementation(requestType => {
1637
+ if (requestType === 'DeviceSessionAskPin') {
1638
+ return {
1639
+ type: 'DeviceSessionPinResult',
1640
+ message: {
1641
+ unlocked: false,
1642
+ unlocked_attach_pin: false,
1643
+ passphrase_protection: false,
1644
+ },
1645
+ };
1646
+ }
1647
+ if (requestType === 'DeviceStatusGet') {
1648
+ return {
1649
+ type: 'DeviceStatus',
1650
+ message: {
1651
+ unlocked: true,
1652
+ unlocked_by_attach_to_pin: true,
1653
+ passphrase_enabled: true,
1654
+ },
1655
+ };
1656
+ }
1657
+ throw new Error(`Unexpected request: ${requestType}`);
1403
1658
  });
1659
+ (device as any).commands = { typedCall };
1660
+
1661
+ await device.unlockDevice();
1662
+
1663
+ expect((device as any).profile).toBeUndefined();
1664
+ expect(device.features?.passphraseProtection).toBe(true);
1665
+ expect(device.features?.unlockedAttachPin).toBe(true);
1666
+ });
1667
+
1668
+ test('maps unsupported Protocol V2 DeviceSessionAskPin to DeviceNotSupportMethod', async () => {
1669
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1670
+ (device as any).features = normalizeProtocolV2Features(
1671
+ { ...descriptor, protocolType: 'V2' } as any,
1672
+ {
1673
+ fw: { application: { version: '1.2.3' } },
1674
+ status: { unlocked: false },
1675
+ }
1676
+ );
1677
+ const typedCall = jest
1678
+ .fn()
1679
+ .mockRejectedValue(new Error('Failure_UnexpectedMessage,Unknown message'));
1680
+ (device as any).commands = { typedCall };
1681
+
1682
+ await expect(device.unlockDevice()).rejects.toEqual(
1683
+ expect.objectContaining({
1684
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1685
+ })
1686
+ );
1687
+ expect(typedCall).toHaveBeenCalledTimes(1);
1688
+ expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
1689
+ expect(typedCall).not.toHaveBeenCalledWith(
1690
+ 'DeviceSessionGet',
1691
+ 'DeviceSession',
1692
+ expect.anything()
1693
+ );
1694
+ });
1695
+
1696
+ test('keeps Protocol V1 unlock response handling unchanged', async () => {
1697
+ const device = Device.fromDescriptor({ path: 'v1-path', protocolType: 'V1' } as any);
1698
+ (device as any).features = {
1699
+ deviceType: 'pro',
1700
+ firmwareVersion: '4.15.0',
1701
+ capabilities: [],
1702
+ unlocked: false,
1703
+ passphraseProtection: false,
1704
+ } as Features;
1404
1705
  const typedCall = jest.fn().mockResolvedValue({
1405
1706
  type: 'UnLockDeviceResponse',
1406
1707
  message: {
1407
1708
  unlocked: true,
1709
+ unlocked_attach_pin: true,
1408
1710
  passphrase_protection: true,
1409
1711
  },
1410
1712
  });
1411
1713
  (device as any).commands = { typedCall };
1412
1714
 
1413
- await device.unlockDevice();
1414
-
1415
- expect(device.profile?.status.passphraseProtection).toBe(true);
1416
- expect(device.features?.passphrase_protection).toBe(true);
1715
+ await expect(device.unlockDevice()).resolves.toMatchObject({
1716
+ unlocked: true,
1717
+ unlockedAttachPin: true,
1718
+ passphraseProtection: true,
1719
+ });
1720
+ expect(typedCall.mock.calls).toEqual([['UnLockDevice', 'UnLockDeviceResponse']]);
1417
1721
  });
1418
1722
  });
1419
1723
 
@@ -1432,7 +1736,7 @@ describe('API compatibility handling', () => {
1432
1736
  method.init();
1433
1737
  (method as any).device = stubDevice({
1434
1738
  features: {
1435
- onekey_device_type: 'pro2',
1739
+ deviceType: 'pro2',
1436
1740
  },
1437
1741
  originalDescriptor: {
1438
1742
  protocolType: 'V2',
@@ -1450,27 +1754,9 @@ describe('API compatibility handling', () => {
1450
1754
  );
1451
1755
  });
1452
1756
 
1453
- test('returns a typed unsupported error for Tron sign message V1 before device binding', () => {
1454
- const method = new TronSignMessage({
1455
- id: 1,
1456
- payload: {
1457
- method: 'tronSignMessage',
1458
- path: "m/44'/195'/0'/0/0",
1459
- messageHex: '0x1234',
1460
- messageType: 'V1',
1461
- },
1462
- });
1463
-
1464
- expect(() => method.init()).toThrow(
1465
- expect.objectContaining({
1466
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1467
- })
1468
- );
1469
- });
1470
-
1471
- test('does not mark Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1757
+ test('does not mark supported Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1472
1758
  const features = {
1473
- onekey_device_type: 'pro2',
1759
+ deviceType: 'pro2',
1474
1760
  } as Features;
1475
1761
 
1476
1762
  const tronMethod = new TronGetAddress({
@@ -1589,9 +1875,7 @@ describe('API compatibility handling', () => {
1589
1875
  });
1590
1876
  expect(
1591
1877
  getMethodVersionRange(features, type => tonSignDataMethod.getVersionRange()[type])
1592
- ).toEqual({
1593
- min: '0.0.0',
1594
- });
1878
+ ).toBeUndefined();
1595
1879
  expect(
1596
1880
  getMethodVersionRange(features, type => suiGetAddressMethod.getVersionRange()[type])
1597
1881
  ).toEqual({
@@ -1621,7 +1905,7 @@ describe('API compatibility handling', () => {
1621
1905
 
1622
1906
  test('allows Pro2 Solana signing methods through Protocol V2 version checks', () => {
1623
1907
  const features = {
1624
- onekey_device_type: 'pro2',
1908
+ deviceType: 'pro2',
1625
1909
  } as Features;
1626
1910
 
1627
1911
  const solSignMessageMethod = new SolSignMessage({
@@ -1685,15 +1969,6 @@ describe('API compatibility handling', () => {
1685
1969
  });
1686
1970
  });
1687
1971
 
1688
- test('includes TON signData in the Protocol V2 protobuf schema', () => {
1689
- const protocolV2Messages = DataManager.getProtobufMessages('v2Schema') as any;
1690
-
1691
- expect(protocolV2Messages.nested.TonSignData).toBeDefined();
1692
- expect(protocolV2Messages.nested.TonSignedData).toBeDefined();
1693
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignData).toBe(11908);
1694
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignedData).toBe(11909);
1695
- });
1696
-
1697
1972
  test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
1698
1973
  const rawTx = '0x'.concat('ab'.repeat(5000));
1699
1974
  const typedCall = jest.fn(() => ({
@@ -1715,7 +1990,7 @@ describe('API compatibility handling', () => {
1715
1990
  method.init();
1716
1991
  (method as any).device = stubDevice({
1717
1992
  features: {
1718
- onekey_device_type: 'pro2',
1993
+ deviceType: 'pro2',
1719
1994
  },
1720
1995
  originalDescriptor: {
1721
1996
  protocolType: 'V2',
@@ -1738,54 +2013,6 @@ describe('API compatibility handling', () => {
1738
2013
  expect(params.data_initial_chunk).toHaveLength(2048);
1739
2014
  });
1740
2015
 
1741
- test('accepts string XRP payment amount values', () => {
1742
- const method = new XrpSignTransaction({
1743
- id: 1,
1744
- payload: {
1745
- method: 'xrpSignTransaction',
1746
- path: "m/44'/144'/0'/0/0",
1747
- transaction: {
1748
- fee: '100000',
1749
- flags: 2147483648,
1750
- sequence: 25,
1751
- maxLedgerVersion: 8820051,
1752
- payment: {
1753
- amount: '100000000',
1754
- destination: 'rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws',
1755
- },
1756
- },
1757
- },
1758
- });
1759
-
1760
- expect(() => method.init()).not.toThrow();
1761
- expect(method.params.payment?.amount).toBe('100000000');
1762
- });
1763
-
1764
- test('accepts Conflux base32 recipient addresses without hex formatting them', () => {
1765
- const to = 'cfx:aak2rra2njvd77ezwjvx04kkds9fzagfe6ku8scz91';
1766
- const method = new ConfluxSignTransaction({
1767
- id: 1,
1768
- payload: {
1769
- method: 'confluxSignTransaction',
1770
- path: "m/44'/503'/0'/0/0",
1771
- transaction: {
1772
- to,
1773
- value: '0x0',
1774
- data: '0x',
1775
- chainId: 1,
1776
- nonce: '0x00',
1777
- epochHeight: '0x00',
1778
- gasLimit: '0x5208',
1779
- storageLimit: '0x5208',
1780
- gasPrice: '0xbebc200',
1781
- },
1782
- },
1783
- });
1784
-
1785
- expect(() => method.init()).not.toThrow();
1786
- expect(method.formattedTx?.to).toBe(to);
1787
- });
1788
-
1789
2016
  test('returns a typed unsupported error for Dynex signing on Protocol V2', async () => {
1790
2017
  const method = new DnxSignTransaction({
1791
2018
  id: 1,
@@ -1841,7 +2068,41 @@ describe('API compatibility handling', () => {
1841
2068
  });
1842
2069
 
1843
2070
  describe('Protocol V2 firmware update targets', () => {
1844
- test('keeps Protocol V2 firmware updates off the legacy firmwareUpdateV3 path', async () => {
2071
+ const OKPP_HEADER_SIZE = 0x52a0;
2072
+
2073
+ const buildOkppHeader = ({
2074
+ type = 'RESC',
2075
+ version = [1, 0, 0],
2076
+ payloadHash = 'ab'.repeat(64),
2077
+ headerHash = 'cd'.repeat(64),
2078
+ }: {
2079
+ type?: string;
2080
+ version?: [number, number, number];
2081
+ payloadHash?: string;
2082
+ headerHash?: string;
2083
+ } = {}) => {
2084
+ const header = new Uint8Array(OKPP_HEADER_SIZE);
2085
+ const view = new DataView(header.buffer);
2086
+ 'OKPP'.split('').forEach((char, index) => {
2087
+ header[index] = char.charCodeAt(0);
2088
+ });
2089
+ view.setUint32(0x04, 1, true);
2090
+ type.split('').forEach((char, index) => {
2091
+ header[0x08 + index] = char.charCodeAt(0);
2092
+ });
2093
+ view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
2094
+ view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
2095
+ const writeHex = (offset: number, hex: string) => {
2096
+ for (let i = 0; i < hex.length / 2; i++) {
2097
+ header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
2098
+ }
2099
+ };
2100
+ writeHex(0x200, payloadHash);
2101
+ writeHex(0x240, headerHash);
2102
+ return header;
2103
+ };
2104
+
2105
+ test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', async () => {
1845
2106
  const method = new FirmwareUpdateV3({
1846
2107
  id: 1,
1847
2108
  payload: {
@@ -1855,25 +2116,26 @@ describe('Protocol V2 firmware update targets', () => {
1855
2116
  await expect(method.run()).rejects.toThrow('firmwareUpdateV4');
1856
2117
  });
1857
2118
 
1858
- test('uses Protocol V2 features after BLE final reconnect without legacy Initialize', async () => {
2119
+ test('uses Protocol V2 features after BLE final reconnect without V1 Initialize', async () => {
1859
2120
  const method = new FirmwareUpdateV4({
1860
2121
  id: 1,
1861
2122
  payload: {
1862
2123
  method: 'firmwareUpdateV4',
2124
+ coprocessorBinary: new Uint8Array([1, 2, 3]).buffer,
1863
2125
  },
1864
2126
  });
1865
2127
  const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' });
1866
2128
  const typedCall = jest.fn().mockImplementation((name: string) => {
1867
- if (name === 'DevGetDeviceInfo') {
2129
+ if (name === 'DeviceInfoGet') {
1868
2130
  return Promise.resolve({
1869
2131
  type: 'DeviceInfo',
1870
2132
  message: {
1871
2133
  fw: {
1872
- boot: { version: '0.0.0' },
1873
- app: { version: '0.0.0' },
2134
+ bootloader: { version: '0.0.0' },
2135
+ application: { version: '0.0.0' },
1874
2136
  },
1875
- bt: {
1876
- app: { version: '0.0.0' },
2137
+ coprocessor: {
2138
+ application: { version: '0.0.0' },
1877
2139
  },
1878
2140
  },
1879
2141
  });
@@ -1887,7 +2149,13 @@ describe('Protocol V2 firmware update targets', () => {
1887
2149
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1888
2150
  deviceConnector: { acquire },
1889
2151
  getCommands: () => commands,
1890
- _updateFeatures: jest.fn(),
2152
+ updateProtocolV2Features: jest.fn(() => ({
2153
+ bootloaderMode: false,
2154
+ mode: 'normal',
2155
+ firmwareVersion: '0.0.0',
2156
+ bootloaderVersion: '0.0.0',
2157
+ bleVersion: '0.0.0',
2158
+ })),
1891
2159
  });
1892
2160
 
1893
2161
  const versions = await (method as any).waitForProtocolV2FinalFeatures();
@@ -1896,13 +2164,13 @@ describe('Protocol V2 firmware update targets', () => {
1896
2164
  // 更新完成判定使用 VERSIONS 请求(含 SE targets),scope 与请求内容一致
1897
2165
  expect(typedCall).toHaveBeenNthCalledWith(
1898
2166
  1,
1899
- 'DevGetDeviceInfo',
2167
+ 'DeviceInfoGet',
1900
2168
  'DeviceInfo',
1901
2169
  {
1902
2170
  targets: {
1903
2171
  hw: true,
1904
2172
  fw: true,
1905
- bt: true,
2173
+ coprocessor: true,
1906
2174
  se1: true,
1907
2175
  se2: true,
1908
2176
  se3: true,
@@ -1925,7 +2193,7 @@ describe('Protocol V2 firmware update targets', () => {
1925
2193
  });
1926
2194
  });
1927
2195
 
1928
- test('skips automatic Protocol V2 bootloader entry before upload while boot flow is disabled', async () => {
2196
+ test('enters Protocol V2 bootloader before upload', async () => {
1929
2197
  const method = new FirmwareUpdateV4({
1930
2198
  id: 1,
1931
2199
  payload: {
@@ -1938,14 +2206,14 @@ describe('Protocol V2 firmware update targets', () => {
1938
2206
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1939
2207
  features: { capabilities: [] },
1940
2208
  });
1941
- (method as any).prepareResourceBinary = jest.fn().mockResolvedValue(null);
1942
- (method as any).prepareFirmwareAndBleBinary = jest.fn().mockResolvedValue([
2209
+ (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
2210
+ (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
1943
2211
  {
1944
- fileName: 'ble-firmware.bin',
2212
+ fileName: 'coprocessor.bin',
1945
2213
  binary: new Uint8Array([1, 2, 3]).buffer,
2214
+ targetId: 5,
1946
2215
  },
1947
2216
  ]);
1948
- (method as any).prepareBootloaderBinary = jest.fn().mockResolvedValue(null);
1949
2217
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
1950
2218
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
1951
2219
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
@@ -1959,17 +2227,17 @@ describe('Protocol V2 firmware update targets', () => {
1959
2227
 
1960
2228
  await method.run();
1961
2229
 
2230
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
1962
2231
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
1963
- resourceBinary: null,
1964
2232
  fwBinaryMap: [
1965
2233
  {
1966
- fileName: 'ble-firmware.bin',
2234
+ fileName: 'coprocessor.bin',
1967
2235
  binary: expect.any(ArrayBuffer),
2236
+ targetId: 5,
1968
2237
  },
1969
2238
  ],
1970
2239
  bootloaderBinary: null,
1971
2240
  });
1972
- expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
1973
2241
  });
1974
2242
 
1975
2243
  test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
@@ -1979,31 +2247,70 @@ describe('Protocol V2 firmware update targets', () => {
1979
2247
  method: 'firmwareUpdateV4',
1980
2248
  },
1981
2249
  });
1982
- const acquire = jest.fn().mockResolvedValue(undefined);
1983
2250
  (method as any).device = stubDevice({
1984
2251
  originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
1985
2252
  features: { bootloader_mode: false, capabilities: [] },
1986
2253
  isBootloader: () => false,
1987
- acquire,
1988
2254
  });
1989
2255
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
1990
2256
  message: 'Device rebooted successfully',
1991
2257
  });
1992
- (method as any).checkDeviceToBootloader = jest.fn(() => {
1993
- (method as any).checkPromise = { promise: Promise.resolve(true) };
2258
+ (method as any).checkDeviceToBootloader = jest.fn();
2259
+ const typedCall = jest.fn().mockResolvedValue({
2260
+ type: 'DeviceInfo',
2261
+ message: protocolV2BootloaderDeviceInfo,
1994
2262
  });
2263
+ const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
2264
+ (method as any).device.isBootloader = () => true;
2265
+ return Promise.resolve();
2266
+ });
2267
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2268
+ (method as any).device.getCommands = () => ({ typedCall });
1995
2269
  method.postTipMessage = jest.fn();
1996
2270
 
1997
2271
  await (method as any).enterProtocolV2BootloaderMode();
1998
2272
 
1999
2273
  expect(method.postTipMessage).toHaveBeenCalledWith('AutoRebootToBootloader');
2000
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Bootloader);
2274
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Bootloader);
2001
2275
  expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
2002
- expect((method as any).checkDeviceToBootloader).toHaveBeenCalledWith(undefined);
2003
- expect(acquire).toHaveBeenCalledTimes(1);
2276
+ expect((method as any).checkDeviceToBootloader).not.toHaveBeenCalled();
2277
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2278
+ });
2279
+
2280
+ test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
2281
+ const method = new FirmwareUpdateV4({
2282
+ id: 1,
2283
+ payload: {
2284
+ method: 'firmwareUpdateV4',
2285
+ },
2286
+ });
2287
+ (method as any).device = stubDevice({
2288
+ originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
2289
+ features: { bootloader_mode: false, capabilities: [] },
2290
+ isBootloader: () => false,
2291
+ });
2292
+ const typedCall = jest.fn().mockResolvedValue({
2293
+ type: 'DeviceInfo',
2294
+ message: protocolV2BootloaderDeviceInfo,
2295
+ });
2296
+ const reconnectProtocolV2Device = jest
2297
+ .fn()
2298
+ .mockRejectedValueOnce(new Error('Device not found'))
2299
+ .mockRejectedValueOnce(new Error('Device not found'))
2300
+ .mockImplementation(() => {
2301
+ (method as any).device.isBootloader = () => true;
2302
+ return Promise.resolve();
2303
+ });
2304
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2305
+ (method as any).device.getCommands = () => ({ typedCall });
2306
+
2307
+ await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
2308
+
2309
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
2310
+ expect(typedCall).toHaveBeenCalledTimes(1);
2004
2311
  });
2005
2312
 
2006
- test('reboots Protocol V2 firmware flow back to normal before final feature polling', async () => {
2313
+ test('reboots Protocol V2 firmware flow back to normal after install status is finished', async () => {
2007
2314
  const method = new FirmwareUpdateV4({
2008
2315
  id: 1,
2009
2316
  payload: {
@@ -2018,7 +2325,7 @@ describe('Protocol V2 firmware update targets', () => {
2018
2325
  await (method as any).exitProtocolV2BootloaderToNormal();
2019
2326
 
2020
2327
  expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
2021
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Normal);
2328
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal);
2022
2329
  });
2023
2330
 
2024
2331
  test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
@@ -2038,7 +2345,7 @@ describe('Protocol V2 firmware update targets', () => {
2038
2345
  getCommands: () => ({ typedCall }),
2039
2346
  });
2040
2347
 
2041
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2348
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
2042
2349
  message: 'Device rebooted successfully',
2043
2350
  });
2044
2351
  });
@@ -2058,7 +2365,7 @@ describe('Protocol V2 firmware update targets', () => {
2058
2365
  getCommands: () => ({ typedCall }),
2059
2366
  });
2060
2367
 
2061
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2368
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
2062
2369
  message: 'Device rebooted successfully',
2063
2370
  });
2064
2371
  });
@@ -2086,192 +2393,811 @@ describe('Protocol V2 firmware update targets', () => {
2086
2393
 
2087
2394
  await expect(
2088
2395
  (method as any).protocolV2StartFirmwareUpdate({
2089
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2396
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2090
2397
  })
2091
2398
  ).resolves.toBeUndefined();
2092
2399
 
2093
2400
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2094
2401
  });
2095
2402
 
2096
- test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => {
2403
+ test('continues Protocol V2 install polling when update request transfer times out', async () => {
2097
2404
  const method = new FirmwareUpdateV4({
2098
2405
  id: 1,
2099
2406
  payload: {
2100
2407
  method: 'firmwareUpdateV4',
2101
2408
  },
2102
2409
  });
2103
- const typedCall = jest
2104
- .fn()
2105
- .mockRejectedValueOnce(
2106
- new Error(
2107
- 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2108
- )
2109
- )
2110
- .mockResolvedValueOnce({
2111
- type: 'DevFirmwareUpdateStatus',
2112
- message: {
2113
- targets: [{ target_id: 2, status: 0 }],
2114
- },
2115
- });
2116
- const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2410
+ const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
2117
2411
 
2118
2412
  (method as any).device = stubDevice({
2119
2413
  getCommands: () => ({ typedCall }),
2120
2414
  });
2121
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2122
- method.postProgressMessage = jest.fn();
2415
+ method.postTipMessage = jest.fn();
2123
2416
 
2124
- await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2125
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2126
- ]);
2417
+ await expect(
2418
+ (method as any).protocolV2StartFirmwareUpdate({
2419
+ targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
2420
+ })
2421
+ ).resolves.toBeUndefined();
2127
2422
 
2128
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2129
- expect(typedCall).toHaveBeenCalledTimes(2);
2423
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2130
2424
  });
2131
2425
 
2132
- test('uses dev firmware status semantics for Protocol V2 install polling', () => {
2426
+ test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => {
2133
2427
  const method = new FirmwareUpdateV4({
2134
2428
  id: 1,
2135
2429
  payload: {
2136
2430
  method: 'firmwareUpdateV4',
2137
2431
  },
2138
2432
  });
2139
- method.postProgressMessage = jest.fn();
2140
-
2141
- const expectedTargetIds = new Set([3]);
2433
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2434
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2435
+ const callCount = typedCall.mock.calls.filter(call => call[0] === name).length;
2436
+ if (callCount === 1) {
2437
+ return Promise.reject(
2438
+ new Error(
2439
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2440
+ )
2441
+ );
2442
+ }
2443
+ return Promise.resolve({
2444
+ type: 'DeviceFirmwareUpdateStatus',
2445
+ message: {
2446
+ records: [{ target_id: 6, status: 2 }],
2447
+ },
2448
+ });
2449
+ }
2450
+ if (name === 'DeviceInfoGet') {
2451
+ return Promise.reject(new Error('DeviceInfo not ready'));
2452
+ }
2453
+ if (name === 'Ping') {
2454
+ return Promise.resolve({ type: 'Success', message: { message: 'ready' } });
2455
+ }
2456
+ return Promise.reject(new Error(`unexpected call ${name}`));
2457
+ });
2458
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2142
2459
 
2143
- expect(
2144
- (method as any).assertProtocolV2TargetStatus(
2145
- [{ target_id: 3, status: 0 }],
2146
- expectedTargetIds
2147
- )
2148
- ).toBe(true);
2460
+ (method as any).device = stubDevice({
2461
+ getCommands: () => ({ typedCall }),
2462
+ });
2463
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2464
+ method.postProgressMessage = jest.fn();
2149
2465
 
2150
- expect(
2151
- (method as any).assertProtocolV2TargetStatus(
2152
- [{ target_id: 3, status: 1 }],
2153
- expectedTargetIds
2154
- )
2155
- ).toBe(false);
2156
- expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2466
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2467
+ { target_id: 6, path: 'vol1:ble-firmware.bin' },
2468
+ ]);
2157
2469
 
2158
- try {
2159
- (method as any).assertProtocolV2TargetStatus(
2160
- [{ target_id: 3, status: 2 }],
2161
- expectedTargetIds
2162
- );
2163
- throw new Error('Expected Protocol V2 failed firmware status to throw');
2164
- } catch (error: any) {
2165
- expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2166
- }
2470
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2471
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2472
+ 'DeviceFirmwareUpdateStatusGet',
2473
+ 'DeviceInfoGet',
2474
+ 'Ping',
2475
+ 'DeviceFirmwareUpdateStatusGet',
2476
+ ]);
2167
2477
  });
2168
2478
 
2169
- test('passes resource, bootloader, BLE, SE and app files to DeviceFirmwareUpdate targets', async () => {
2479
+ test('treats Protocol V2 normal mode after reconnect as firmware update complete', async () => {
2170
2480
  const method = new FirmwareUpdateV4({
2171
2481
  id: 1,
2172
2482
  payload: {
2173
2483
  method: 'firmwareUpdateV4',
2174
2484
  },
2175
2485
  });
2176
-
2177
- const writtenPaths: string[] = [];
2486
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2487
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2488
+ return Promise.reject(new Error('Device disconnected'));
2489
+ }
2490
+ if (name === 'DeviceInfoGet') {
2491
+ return Promise.resolve({
2492
+ type: 'DeviceInfo',
2493
+ message: {
2494
+ hw: { serial_no: 'PR9999999999' },
2495
+ fw: {
2496
+ bootloader: { version: '9.9.9' },
2497
+ application: { version: '9.9.9' },
2498
+ },
2499
+ status: {
2500
+ device_id: 'PRO2-DEVICE-ID',
2501
+ init_states: true,
2502
+ },
2503
+ },
2504
+ });
2505
+ }
2506
+ return Promise.reject(new Error(`unexpected call ${name}`));
2507
+ });
2508
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2509
+ const updateProtocolV2Features = jest.fn((deviceInfo: ProtocolV2DeviceInfo) =>
2510
+ normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, deviceInfo)
2511
+ );
2512
+
2513
+ (method as any).device = stubDevice({
2514
+ getCommands: () => ({ typedCall }),
2515
+ updateProtocolV2Features,
2516
+ });
2517
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2518
+ method.postProgressMessage = jest.fn();
2519
+
2520
+ await expect(
2521
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
2522
+ { target_id: 3, path: 'vol1:bootloader.bin' },
2523
+ { target_id: 4, path: 'vol1:application_p1.bin' },
2524
+ ])
2525
+ ).resolves.toBeUndefined();
2526
+
2527
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2528
+ expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
2529
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2530
+ 'DeviceFirmwareUpdateStatusGet',
2531
+ 'DeviceInfoGet',
2532
+ ]);
2533
+ });
2534
+
2535
+ test('uses SDK decoded enum names for Protocol V2 install polling', () => {
2536
+ const method = new FirmwareUpdateV4({
2537
+ id: 1,
2538
+ payload: {
2539
+ method: 'firmwareUpdateV4',
2540
+ },
2541
+ });
2542
+ method.postProgressMessage = jest.fn();
2543
+
2544
+ const expectedTargetIds = new Set([4]);
2545
+
2546
+ expect(
2547
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 2 }], expectedTargetIds)
2548
+ ).toBe(true);
2549
+ expect(
2550
+ (method as any).assertProtocolV2TargetStatus(
2551
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' }],
2552
+ expectedTargetIds
2553
+ )
2554
+ ).toBe(true);
2555
+ expect(
2556
+ (method as any).assertProtocolV2TargetStatus(
2557
+ [
2558
+ {
2559
+ target_id: 'FW_MGMT_TARGET_APPLICATION_P1',
2560
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
2561
+ },
2562
+ { target_id: 'FW_MGMT_TARGET_SE04', status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
2563
+ ],
2564
+ new Set([4, 10])
2565
+ )
2566
+ ).toBe(true);
2567
+
2568
+ expect(
2569
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds)
2570
+ ).toBe(false);
2571
+ expect(
2572
+ (method as any).assertProtocolV2TargetStatus(
2573
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS' }],
2574
+ expectedTargetIds
2575
+ )
2576
+ ).toBe(false);
2577
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2578
+
2579
+ try {
2580
+ (method as any).assertProtocolV2TargetStatus(
2581
+ [{ target_id: 4, status: 3 }],
2582
+ expectedTargetIds
2583
+ );
2584
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2585
+ } catch (error: any) {
2586
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2587
+ }
2588
+
2589
+ try {
2590
+ (method as any).assertProtocolV2TargetStatus(
2591
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY' }],
2592
+ expectedTargetIds
2593
+ );
2594
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2595
+ } catch (error: any) {
2596
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2597
+ }
2598
+ });
2599
+
2600
+ test('passes bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
2601
+ const method = new FirmwareUpdateV4({
2602
+ id: 1,
2603
+ payload: {
2604
+ method: 'firmwareUpdateV4',
2605
+ },
2606
+ });
2607
+
2608
+ const writtenPaths: string[] = [];
2178
2609
  method.postTipMessage = jest.fn();
2179
2610
  method.postProgressMessage = jest.fn();
2180
2611
  (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
2181
2612
  writtenPaths.push(params.filePath);
2182
2613
  return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
2183
2614
  });
2615
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2184
2616
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2185
2617
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2186
2618
  .fn()
2187
2619
  .mockResolvedValue(undefined);
2188
2620
 
2189
2621
  await (method as any).executeProtocolV2Update({
2190
- // resource 只支持单文件 .bin,整文件一次上传
2191
- resourceBinary: new Uint8Array([1, 2, 3]).buffer,
2192
2622
  bootloaderBinary: new Uint8Array([4, 5]).buffer,
2193
2623
  fwBinaryMap: [
2194
2624
  {
2195
- fileName: 'ble-firmware.bin',
2625
+ fileName: 'coprocessor.bin',
2196
2626
  binary: new Uint8Array([6]).buffer,
2627
+ targetId: 6,
2197
2628
  },
2198
2629
  {
2199
- fileName: 'se1-firmware.bin',
2630
+ fileName: 'se01.bin',
2200
2631
  binary: new Uint8Array([7]).buffer,
2632
+ targetId: 7,
2201
2633
  },
2202
2634
  {
2203
- fileName: 'firmware.bin',
2635
+ fileName: 'application_p1.bin',
2204
2636
  binary: new Uint8Array([8]).buffer,
2637
+ targetId: 4,
2205
2638
  },
2206
2639
  ],
2207
2640
  });
2208
2641
 
2209
2642
  expect(writtenPaths).toEqual([
2210
- 'vol1:resource.bin',
2211
- 'vol1:bootloader.bin',
2212
- 'vol1:ble-firmware.bin',
2213
- 'vol1:se1-firmware.bin',
2214
- 'vol1:firmware.bin',
2643
+ 'vol0:/bootloader.bin',
2644
+ 'vol0:/coprocessor.bin',
2645
+ 'vol0:/se01.bin',
2646
+ 'vol0:/application_p1.bin',
2215
2647
  ]);
2216
- // DevFirmwareTargetType:RESOURCE=10, MAIN_BOOT=1, BT=2, SE1=3, MAIN_APP=0
2648
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
2649
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
2650
+ 2,
2651
+ 'vol0:/coprocessor.bin',
2652
+ 1
2653
+ );
2654
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
2217
2655
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2218
2656
  targets: [
2219
- { target_id: 10, path: 'vol1:resource.bin' },
2220
- { target_id: 1, path: 'vol1:bootloader.bin' },
2221
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2222
- { target_id: 3, path: 'vol1:se1-firmware.bin' },
2223
- { target_id: 0, path: 'vol1:firmware.bin' },
2657
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
2658
+ { target_id: 6, path: 'vol0:/coprocessor.bin' },
2659
+ { target_id: 7, path: 'vol0:/se01.bin' },
2660
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
2224
2661
  ],
2225
2662
  });
2226
2663
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2227
2664
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2228
2665
  });
2229
2666
 
2230
- test('passes explicit per-target binaries through without file name heuristics', async () => {
2667
+ test('announces one transfer when syncing resource bundles and staging firmware', async () => {
2668
+ const method = new FirmwareUpdateV4({
2669
+ id: 1,
2670
+ payload: {
2671
+ method: 'firmwareUpdateV4',
2672
+ },
2673
+ });
2674
+
2675
+ method.postTipMessage = jest.fn();
2676
+ method.postProgressMessage = jest.fn();
2677
+ (method as any).protocolV2CommonUpdateProcess = jest
2678
+ .fn()
2679
+ .mockImplementation(params =>
2680
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2681
+ );
2682
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2683
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2684
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2685
+ .fn()
2686
+ .mockResolvedValue(undefined);
2687
+
2688
+ await (method as any).executeProtocolV2Update({
2689
+ resourceBundles: [
2690
+ {
2691
+ name: 'images.okpkg',
2692
+ binary: new Uint8Array([1, 2]).buffer,
2693
+ devicePath: 'vol0:/bundles/images/images.okpkg',
2694
+ },
2695
+ ],
2696
+ bootloaderBinary: null,
2697
+ fwBinaryMap: [
2698
+ {
2699
+ fileName: 'application_p1.bin',
2700
+ binary: new Uint8Array([3]).buffer,
2701
+ targetId: 3,
2702
+ },
2703
+ ],
2704
+ });
2705
+
2706
+ expect(method.postTipMessage).toHaveBeenCalledTimes(2);
2707
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
2708
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
2709
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
2710
+ 1,
2711
+ expect.objectContaining({ processedSize: 0, totalSize: 3 })
2712
+ );
2713
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
2714
+ 2,
2715
+ expect.objectContaining({ processedSize: 2, totalSize: 3 })
2716
+ );
2717
+ });
2718
+
2719
+ test('does not request installation when the staged file size does not match', async () => {
2720
+ const method = new FirmwareUpdateV4({
2721
+ id: 1,
2722
+ payload: {
2723
+ method: 'firmwareUpdateV4',
2724
+ },
2725
+ });
2726
+
2727
+ method.postTipMessage = jest.fn();
2728
+ method.postProgressMessage = jest.fn();
2729
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
2730
+ (method as any).device = stubDevice({
2731
+ getCommands: () => ({
2732
+ typedCall: jest.fn().mockResolvedValue({
2733
+ type: 'FilesystemPathInfo',
2734
+ message: { exist: true, directory: false, size: 2 },
2735
+ }),
2736
+ }),
2737
+ });
2738
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
2739
+
2740
+ await expect(
2741
+ (method as any).executeProtocolV2Update({
2742
+ bootloaderBinary: null,
2743
+ fwBinaryMap: [
2744
+ {
2745
+ fileName: 'coprocessor.bin',
2746
+ binary: new Uint8Array([1, 2, 3]).buffer,
2747
+ targetId: 5,
2748
+ },
2749
+ ],
2750
+ })
2751
+ ).rejects.toThrow(/staged file verification failed/);
2752
+ expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
2753
+ });
2754
+
2755
+ test('passes explicit per-target binaries through without file name heuristics', async () => {
2756
+ const method = new FirmwareUpdateV4({
2757
+ id: 1,
2758
+ payload: {
2759
+ method: 'firmwareUpdateV4',
2760
+ platform: 'web',
2761
+ applicationP2Binary: new Uint8Array([2]).buffer,
2762
+ se04Binary: new Uint8Array([3]).buffer,
2763
+ },
2764
+ });
2765
+ method.init();
2766
+
2767
+ const explicit = (method as any).collectExplicitTargetBinaries();
2768
+ expect(explicit).toEqual([
2769
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2770
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2771
+ ]);
2772
+
2773
+ method.postTipMessage = jest.fn();
2774
+ method.postProgressMessage = jest.fn();
2775
+ (method as any).protocolV2CommonUpdateProcess = jest
2776
+ .fn()
2777
+ .mockImplementation(params =>
2778
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2779
+ );
2780
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2781
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2782
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2783
+ .fn()
2784
+ .mockResolvedValue(undefined);
2785
+
2786
+ await (method as any).executeProtocolV2Update({
2787
+ bootloaderBinary: null,
2788
+ fwBinaryMap: explicit,
2789
+ });
2790
+
2791
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2792
+ targets: [
2793
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
2794
+ { target_id: 10, path: 'vol0:/se04.bin' },
2795
+ ],
2796
+ });
2797
+ });
2798
+
2799
+ test('rejects romloaderBinary before sending a Protocol V2 update request', () => {
2800
+ const method = new FirmwareUpdateV4({
2801
+ id: 1,
2802
+ payload: {
2803
+ method: 'firmwareUpdateV4',
2804
+ platform: 'web',
2805
+ romloaderBinary: new Uint8Array([1]).buffer,
2806
+ },
2807
+ });
2808
+ method.init();
2809
+
2810
+ expect(() => (method as any).collectExplicitTargetBinaries()).toThrow(
2811
+ 'FW_MGMT_TARGET_ROMLOADER is not accepted'
2812
+ );
2813
+ });
2814
+
2815
+ test('maps Pro2 remote firmware-v1 components to Protocol V2 targets in installOrder', async () => {
2816
+ const method = new FirmwareUpdateV4({
2817
+ id: 1,
2818
+ payload: {
2819
+ method: 'firmwareUpdateV4',
2820
+ platform: 'web',
2821
+ },
2822
+ });
2823
+ method.init();
2824
+
2825
+ const binaries = new Map(
2826
+ [
2827
+ 'bootloader',
2828
+ 'applicationP1',
2829
+ 'applicationP2',
2830
+ 'coprocessor',
2831
+ 'se01',
2832
+ 'se02',
2833
+ 'se03',
2834
+ 'se04',
2835
+ ].map((key, index) => [
2836
+ `https://example.com/${key}.pp.bin`,
2837
+ new Uint8Array([index + 1]).buffer,
2838
+ ])
2839
+ );
2840
+ const getSysResourceBinarySpy = jest
2841
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2842
+ .mockImplementation(url =>
2843
+ Promise.resolve({
2844
+ binary: binaries.get(url) ?? new Uint8Array([0]).buffer,
2845
+ })
2846
+ );
2847
+ const getFirmwareLatestReleaseSpy = jest
2848
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2849
+ .mockReturnValue({
2850
+ required: false,
2851
+ version: [1, 0, 0],
2852
+ url: 'https://example.com/applicationP1.pp.bin',
2853
+ bootloaderResource: 'https://example.com/bootloader.pp.bin',
2854
+ bootloaderVersion: [1, 0, 0],
2855
+ displayBootloaderVersion: [1, 0, 0],
2856
+ upgradeType: 'payload-package-set',
2857
+ installOrder: [
2858
+ 'bootloader',
2859
+ 'applicationP1',
2860
+ 'applicationP2',
2861
+ 'coprocessor',
2862
+ 'se01',
2863
+ 'se02',
2864
+ 'se03',
2865
+ 'se04',
2866
+ ],
2867
+ components: {
2868
+ bootloader: {
2869
+ target: 'BOOTLOADER',
2870
+ url: 'https://example.com/bootloader.pp.bin',
2871
+ },
2872
+ applicationP1: {
2873
+ target: 'APPLICATION_P1',
2874
+ url: 'https://example.com/applicationP1.pp.bin',
2875
+ },
2876
+ applicationP2: {
2877
+ target: 'APPLICATION_P2',
2878
+ url: 'https://example.com/applicationP2.pp.bin',
2879
+ },
2880
+ coprocessor: {
2881
+ target: 'COPROCESSOR',
2882
+ url: 'https://example.com/coprocessor.pp.bin',
2883
+ },
2884
+ se01: { target: 'SE01', url: 'https://example.com/se01.pp.bin' },
2885
+ se02: { target: 'SE02', url: 'https://example.com/se02.pp.bin' },
2886
+ se03: { target: 'SE03', url: 'https://example.com/se03.pp.bin' },
2887
+ se04: { target: 'SE04', url: 'https://example.com/se04.pp.bin' },
2888
+ },
2889
+ fingerprint: '',
2890
+ changelog: {
2891
+ 'zh-CN': '',
2892
+ 'en-US': '',
2893
+ },
2894
+ });
2895
+
2896
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2897
+ deviceType: 'pro2',
2898
+ firmwareVersion: '0.0.0',
2899
+ });
2900
+
2901
+ expect(remoteBinaries.bootloaderBinary).toBe(
2902
+ binaries.get('https://example.com/bootloader.pp.bin')
2903
+ );
2904
+ expect(remoteBinaries.fwBinaryMap).toEqual([
2905
+ { fileName: 'application_p1.bin', binary: expect.anything(), targetId: 4 },
2906
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2907
+ { fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 },
2908
+ { fileName: 'se01.bin', binary: expect.anything(), targetId: 7 },
2909
+ { fileName: 'se02.bin', binary: expect.anything(), targetId: 8 },
2910
+ { fileName: 'se03.bin', binary: expect.anything(), targetId: 9 },
2911
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2912
+ ]);
2913
+ expect(getSysResourceBinarySpy.mock.calls.map(call => call[0])).toEqual([
2914
+ 'https://example.com/bootloader.pp.bin',
2915
+ 'https://example.com/applicationP1.pp.bin',
2916
+ 'https://example.com/applicationP2.pp.bin',
2917
+ 'https://example.com/coprocessor.pp.bin',
2918
+ 'https://example.com/se01.pp.bin',
2919
+ 'https://example.com/se02.pp.bin',
2920
+ 'https://example.com/se03.pp.bin',
2921
+ 'https://example.com/se04.pp.bin',
2922
+ ]);
2923
+
2924
+ getSysResourceBinarySpy.mockRestore();
2925
+ getFirmwareLatestReleaseSpy.mockRestore();
2926
+ });
2927
+
2928
+ test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
2929
+ const method = new FirmwareUpdateV4({
2930
+ id: 1,
2931
+ payload: {
2932
+ method: 'firmwareUpdateV4',
2933
+ platform: 'web',
2934
+ },
2935
+ });
2936
+ method.init();
2937
+
2938
+ const getFirmwareLatestReleaseSpy = jest
2939
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2940
+ .mockReturnValue({
2941
+ required: false,
2942
+ version: [1, 0, 0],
2943
+ url: '',
2944
+ resourceBundles: [
2945
+ {
2946
+ name: 'images',
2947
+ url: 'https://example.com/images.okpkg',
2948
+ devicePath: 'vol0:/resource/images/images.okpkg',
2949
+ version: [2, 0, 0],
2950
+ payloadHash: 'ab'.repeat(64),
2951
+ headerHash: 'cd'.repeat(64),
2952
+ },
2953
+ ],
2954
+ fingerprint: '',
2955
+ changelog: {
2956
+ 'zh-CN': '',
2957
+ 'en-US': '',
2958
+ },
2959
+ });
2960
+
2961
+ const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', {
2962
+ deviceType: 'pro2',
2963
+ firmwareVersion: '1.0.0',
2964
+ capabilities: [],
2965
+ });
2966
+
2967
+ expect(bundles).toEqual([
2968
+ {
2969
+ name: 'images',
2970
+ binary: expect.any(ArrayBuffer),
2971
+ devicePath: 'vol0:/resource/images/images.okpkg',
2972
+ url: 'https://example.com/images.okpkg',
2973
+ version: [2, 0, 0],
2974
+ payloadHash: 'ab'.repeat(64),
2975
+ headerHash: 'cd'.repeat(64),
2976
+ },
2977
+ ]);
2978
+ expect(bundles[0].binary.byteLength).toBe(0);
2979
+
2980
+ getFirmwareLatestReleaseSpy.mockRestore();
2981
+ });
2982
+
2983
+ test('skips Pro2 firmware components when configured versions are already installed', async () => {
2984
+ const method = new FirmwareUpdateV4({
2985
+ id: 1,
2986
+ payload: {
2987
+ method: 'firmwareUpdateV4',
2988
+ platform: 'web',
2989
+ },
2990
+ });
2991
+ method.init();
2992
+
2993
+ const getSysResourceBinarySpy = jest
2994
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2995
+ .mockResolvedValue({ binary: new Uint8Array([1]).buffer });
2996
+ const getFirmwareLatestReleaseSpy = jest
2997
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2998
+ .mockReturnValue({
2999
+ required: false,
3000
+ version: [1, 0, 0],
3001
+ url: 'https://example.com/applicationP1.pp.bin',
3002
+ bootloaderVersion: [1, 0, 0],
3003
+ upgradeType: 'payload-package-set',
3004
+ installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
3005
+ components: {
3006
+ bootloader: {
3007
+ target: 'BOOTLOADER',
3008
+ url: 'https://example.com/bootloader.pp.bin',
3009
+ },
3010
+ applicationP1: {
3011
+ target: 'APPLICATION_P1',
3012
+ url: 'https://example.com/applicationP1.pp.bin',
3013
+ },
3014
+ applicationP2: {
3015
+ target: 'APPLICATION_P2',
3016
+ url: 'https://example.com/applicationP2.pp.bin',
3017
+ },
3018
+ },
3019
+ fingerprint: '',
3020
+ changelog: {
3021
+ 'zh-CN': '',
3022
+ 'en-US': '',
3023
+ },
3024
+ });
3025
+
3026
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
3027
+ deviceType: 'pro2',
3028
+ firmwareVersion: '1.0.0',
3029
+ bootloaderVersion: '1.0.0',
3030
+ capabilities: [],
3031
+ });
3032
+
3033
+ expect(remoteBinaries).toEqual({
3034
+ bootloaderBinary: null,
3035
+ fwBinaryMap: [],
3036
+ installItems: [],
3037
+ });
3038
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
3039
+
3040
+ getSysResourceBinarySpy.mockRestore();
3041
+ getFirmwareLatestReleaseSpy.mockRestore();
3042
+ });
3043
+
3044
+ test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
3045
+ const method = new FirmwareUpdateV4({
3046
+ id: 1,
3047
+ payload: {
3048
+ method: 'firmwareUpdateV4',
3049
+ platform: 'web',
3050
+ },
3051
+ });
3052
+ method.init();
3053
+
3054
+ const remoteBinaries = {
3055
+ bootloaderBinary: new Uint8Array([1]).buffer,
3056
+ fwBinaryMap: [
3057
+ {
3058
+ fileName: 'application_p1.bin',
3059
+ binary: new Uint8Array([2]).buffer,
3060
+ targetId: 3,
3061
+ },
3062
+ ],
3063
+ };
3064
+
3065
+ (method as any).device = stubDevice({
3066
+ originalDescriptor: { protocolType: 'V2' },
3067
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3068
+ });
3069
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
3070
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3071
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3072
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3073
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3074
+ bootloaderVersion: '1.0.0',
3075
+ bleVersion: '0.0.0',
3076
+ firmwareVersion: '1.0.0',
3077
+ });
3078
+ method.postTipMessage = jest.fn();
3079
+
3080
+ await method.run();
3081
+
3082
+ expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
3083
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3084
+ bootloaderBinary: remoteBinaries.bootloaderBinary,
3085
+ fwBinaryMap: remoteBinaries.fwBinaryMap,
3086
+ });
3087
+ });
3088
+
3089
+ test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
3090
+ const method = new FirmwareUpdateV4({
3091
+ id: 1,
3092
+ payload: {
3093
+ method: 'firmwareUpdateV4',
3094
+ platform: 'web',
3095
+ coprocessorBinary: new Uint8Array([1]).buffer,
3096
+ },
3097
+ });
3098
+ method.init();
3099
+
3100
+ (method as any).device = stubDevice({
3101
+ originalDescriptor: { protocolType: 'V2' },
3102
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3103
+ });
3104
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3105
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3106
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3107
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3108
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3109
+ bootloaderVersion: '1.0.0',
3110
+ bleVersion: '0.0.0',
3111
+ firmwareVersion: '1.0.0',
3112
+ });
3113
+ method.postTipMessage = jest.fn();
3114
+
3115
+ await method.run();
3116
+
3117
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3118
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3119
+ bootloaderBinary: null,
3120
+ fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
3121
+ });
3122
+ });
3123
+
3124
+ test('treats manual resource bundles as explicit payload without remote firmware auto-fill', async () => {
3125
+ const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
3126
+ const method = new FirmwareUpdateV4({
3127
+ id: 1,
3128
+ payload: {
3129
+ method: 'firmwareUpdateV4',
3130
+ platform: 'web',
3131
+ resourceBundleFiles: [
3132
+ {
3133
+ binary: resourceBundle,
3134
+ devicePath: 'vol0:/resource/images/images.okpkg',
3135
+ },
3136
+ ],
3137
+ },
3138
+ });
3139
+ method.init();
3140
+
3141
+ (method as any).device = stubDevice({
3142
+ originalDescriptor: { protocolType: 'V2' },
3143
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3144
+ });
3145
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3146
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3147
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3148
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3149
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3150
+ bootloaderVersion: '1.0.0',
3151
+ bleVersion: '0.0.0',
3152
+ firmwareVersion: '1.0.0',
3153
+ });
3154
+ method.postTipMessage = jest.fn();
3155
+
3156
+ await method.run();
3157
+
3158
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3159
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith(
3160
+ expect.objectContaining({
3161
+ resourceBundles: [
3162
+ {
3163
+ name: 'images.okpkg',
3164
+ binary: resourceBundle,
3165
+ devicePath: 'vol0:/resource/images/images.okpkg',
3166
+ },
3167
+ ],
3168
+ })
3169
+ );
3170
+ });
3171
+
3172
+ test('syncs resource bundles without sending a firmware install request', async () => {
2231
3173
  const method = new FirmwareUpdateV4({
2232
3174
  id: 1,
2233
3175
  payload: {
2234
3176
  method: 'firmwareUpdateV4',
2235
- platform: 'web',
2236
- romloaderBinary: new Uint8Array([1]).buffer,
2237
- applicationP2Binary: new Uint8Array([2]).buffer,
2238
- se04Binary: new Uint8Array([3]).buffer,
2239
3177
  },
2240
3178
  });
2241
- method.init();
2242
-
2243
- const explicit = (method as any).collectExplicitTargetBinaries();
2244
- expect(explicit).toEqual([
2245
- { fileName: 'romloader.bin', binary: expect.anything(), targetId: 1 },
2246
- { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 0 },
2247
- { fileName: 'se04.bin', binary: expect.anything(), targetId: 6 },
2248
- ]);
2249
3179
 
2250
3180
  method.postTipMessage = jest.fn();
2251
3181
  method.postProgressMessage = jest.fn();
2252
- (method as any).protocolV2CommonUpdateProcess = jest
2253
- .fn()
2254
- .mockImplementation(params =>
2255
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2256
- );
2257
- (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2258
- (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2259
- .fn()
2260
- .mockResolvedValue(undefined);
3182
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
3183
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
3184
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
2261
3185
 
2262
3186
  await (method as any).executeProtocolV2Update({
2263
- resourceBinary: null,
3187
+ resourceBundles: [
3188
+ {
3189
+ name: 'images.okpkg',
3190
+ binary: new Uint8Array([1, 2, 3]).buffer,
3191
+ devicePath: 'vol0:/resource/images/images.okpkg',
3192
+ },
3193
+ ],
2264
3194
  bootloaderBinary: null,
2265
- fwBinaryMap: explicit,
3195
+ fwBinaryMap: [],
2266
3196
  });
2267
3197
 
2268
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2269
- targets: [
2270
- { target_id: 1, path: 'vol1:romloader.bin' },
2271
- { target_id: 0, path: 'vol1:application_p2.bin' },
2272
- { target_id: 6, path: 'vol1:se04.bin' },
2273
- ],
2274
- });
3198
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
3199
+ expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
3200
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
2275
3201
  });
2276
3202
 
2277
3203
  test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
@@ -2308,10 +3234,65 @@ describe('Protocol V2 firmware update targets', () => {
2308
3234
  });
2309
3235
 
2310
3236
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2311
- expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4096]);
2312
- expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4096, 1]);
3237
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
3238
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
2313
3239
  expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
2314
3240
  expect(writePayloads.every(payload => payload.append === false)).toBe(true);
3241
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
3242
+ });
3243
+
3244
+ test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => {
3245
+ const method = new FirmwareUpdateV4({
3246
+ id: 1,
3247
+ payload: {
3248
+ method: 'firmwareUpdateV4',
3249
+ },
3250
+ });
3251
+ const writeOffsets: number[] = [];
3252
+ let failedSecondChunk = false;
3253
+ const typedCall = jest.fn(
3254
+ (
3255
+ _name: string,
3256
+ _resType: string,
3257
+ params: { file: { offset: number; data: { byteLength: number } } }
3258
+ ) => {
3259
+ writeOffsets.push(params.file.offset);
3260
+ if (params.file.offset === 4000 && !failedSecondChunk) {
3261
+ failedSecondChunk = true;
3262
+ return Promise.reject(new Error('response lost after device write'));
3263
+ }
3264
+ return Promise.resolve({
3265
+ type: 'FilesystemFile',
3266
+ message: {
3267
+ processed_byte: params.file.offset + params.file.data.byteLength,
3268
+ },
3269
+ });
3270
+ }
3271
+ );
3272
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
3273
+ callback: (...args: any[]) => void
3274
+ ) => {
3275
+ callback();
3276
+ return 0 as any;
3277
+ }) as typeof setTimeout);
3278
+
3279
+ (method as any).device = stubDevice({
3280
+ getCommands: () => ({ typedCall }),
3281
+ });
3282
+ method.postProgressMessage = jest.fn();
3283
+
3284
+ try {
3285
+ await (method as any).protocolV2CommonUpdateProcess({
3286
+ payload: new Uint8Array(8001).buffer,
3287
+ filePath: 'vol0:/firmware.bin',
3288
+ processedSize: 0,
3289
+ totalSize: 8001,
3290
+ });
3291
+ } finally {
3292
+ setTimeoutSpy.mockRestore();
3293
+ }
3294
+
3295
+ expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
2315
3296
  });
2316
3297
 
2317
3298
  test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
@@ -2330,9 +3311,23 @@ describe('Protocol V2 firmware update targets', () => {
2330
3311
  },
2331
3312
  });
2332
3313
  }
2333
- if (name === 'DevFirmwareUpdate') {
3314
+ if (name === 'FilesystemPathInfoQuery') {
3315
+ return Promise.resolve({
3316
+ type: 'FilesystemPathInfo',
3317
+ message: { exist: true, directory: false, size: 4097 },
3318
+ });
3319
+ }
3320
+ if (name === 'DeviceFirmwareUpdateRequest') {
2334
3321
  return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
2335
3322
  }
3323
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
3324
+ return Promise.resolve({
3325
+ type: 'DeviceFirmwareUpdateStatus',
3326
+ message: {
3327
+ records: [{ target_id: 4, status: 2 }],
3328
+ },
3329
+ });
3330
+ }
2336
3331
  return Promise.reject(new Error(`unexpected call ${name}`));
2337
3332
  });
2338
3333
 
@@ -2343,23 +3338,25 @@ describe('Protocol V2 firmware update targets', () => {
2343
3338
  method.postTipMessage = jest.fn();
2344
3339
 
2345
3340
  await (method as any).executeProtocolV2Update({
2346
- resourceBinary: null,
2347
3341
  bootloaderBinary: null,
2348
3342
  fwBinaryMap: [
2349
3343
  {
2350
3344
  fileName: 'firmware.bin',
2351
3345
  binary: new Uint8Array(4097).buffer,
3346
+ targetId: 4,
2352
3347
  },
2353
3348
  ],
2354
3349
  });
2355
3350
 
2356
3351
  expect(typedCall).toHaveBeenCalledWith(
2357
- 'DevFirmwareUpdate',
2358
- ['Success', 'DevFirmwareUpdateStatus'],
3352
+ 'DeviceFirmwareUpdateRequest',
3353
+ ['Success', 'DeviceFirmwareUpdateStatus'],
2359
3354
  {
2360
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3355
+ targets: [{ target_id: 4, path: 'vol0:/firmware.bin' }],
2361
3356
  },
2362
- expect.any(Object)
3357
+ expect.objectContaining({
3358
+ timeoutMs: 3 * 60 * 1000,
3359
+ })
2363
3360
  );
2364
3361
  });
2365
3362
 
@@ -2403,6 +3400,7 @@ describe('Protocol V2 firmware update targets', () => {
2403
3400
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2404
3401
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
2405
3402
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
3403
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
2406
3404
  });
2407
3405
 
2408
3406
  test('consumes Protocol V2 install progress before final update success', async () => {
@@ -2421,18 +3419,18 @@ describe('Protocol V2 firmware update targets', () => {
2421
3419
  method.postTipMessage = jest.fn();
2422
3420
 
2423
3421
  await (method as any).protocolV2StartFirmwareUpdate({
2424
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3422
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2425
3423
  });
2426
3424
 
2427
3425
  const callOptions = typedCall.mock.calls[0][3];
2428
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2429
- expect(callOptions.intermediateTypes).toEqual(['DevFirmwareInstallProgress']);
3426
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
3427
+ expect(callOptions.intermediateTypes).toEqual(['DeviceFirmwareUpdateStatus']);
2430
3428
  callOptions.onIntermediateResponse({
2431
- type: 'DevFirmwareInstallProgress',
2432
- message: { target_id: 0, progress: 42 },
3429
+ type: 'DeviceFirmwareUpdateStatus',
3430
+ message: { records: [{ target_id: 4, status: 1 }] },
2433
3431
  });
2434
3432
 
2435
- expect(method.postProgressMessage).toHaveBeenCalledWith(42, 'installingFirmware');
3433
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2436
3434
  });
2437
3435
 
2438
3436
  test('accepts Protocol V2 firmware update status as start response', async () => {
@@ -2443,8 +3441,8 @@ describe('Protocol V2 firmware update targets', () => {
2443
3441
  },
2444
3442
  });
2445
3443
  const typedCall = jest.fn().mockResolvedValue({
2446
- type: 'DevFirmwareUpdateStatus',
2447
- message: { targets: [{ target_id: 0, status: 1 }] },
3444
+ type: 'DeviceFirmwareUpdateStatus',
3445
+ message: { records: [{ target_id: 4, status: 1 }] },
2448
3446
  });
2449
3447
 
2450
3448
  (method as any).device = stubDevice({
@@ -2454,29 +3452,29 @@ describe('Protocol V2 firmware update targets', () => {
2454
3452
  method.postTipMessage = jest.fn();
2455
3453
 
2456
3454
  await (method as any).protocolV2StartFirmwareUpdate({
2457
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3455
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2458
3456
  });
2459
3457
 
2460
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3458
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2461
3459
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2462
3460
  });
2463
3461
  });
2464
3462
 
2465
3463
  describe('Protocol V2 firmware update method', () => {
2466
- test('returns DevFirmwareUpdateStatus from low-level update trigger', async () => {
3464
+ test('returns DeviceFirmwareUpdateStatus from low-level update trigger', async () => {
2467
3465
  const method = new DeviceFirmwareUpdate({
2468
3466
  id: 1,
2469
3467
  payload: {
2470
3468
  method: 'deviceFirmwareUpdate',
2471
- targetId: 0,
3469
+ targetId: 4,
2472
3470
  path: 'vol0:firmware.bin',
2473
3471
  },
2474
3472
  });
2475
3473
  method.init();
2476
3474
 
2477
3475
  const typedCall = jest.fn().mockResolvedValue({
2478
- type: 'DevFirmwareUpdateStatus',
2479
- message: { targets: [{ target_id: 0, status: 1 }] },
3476
+ type: 'DeviceFirmwareUpdateStatus',
3477
+ message: { records: [{ target_id: 4, status: 1 }] },
2480
3478
  });
2481
3479
 
2482
3480
  (method as any).device = stubDevice({
@@ -2484,11 +3482,11 @@ describe('Protocol V2 firmware update method', () => {
2484
3482
  });
2485
3483
 
2486
3484
  await expect(method.run()).resolves.toEqual({
2487
- targets: [{ target_id: 0, status: 1 }],
3485
+ records: [{ target_id: 4, status: 1 }],
2488
3486
  });
2489
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3487
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2490
3488
  expect(typedCall.mock.calls[0][2]).toEqual({
2491
- targets: [{ target_id: 0, path: 'vol0:firmware.bin' }],
3489
+ targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
2492
3490
  });
2493
3491
  });
2494
3492
 
@@ -2497,7 +3495,7 @@ describe('Protocol V2 firmware update method', () => {
2497
3495
  id: 1,
2498
3496
  payload: {
2499
3497
  method: 'deviceFirmwareUpdate',
2500
- targetId: 0,
3498
+ targetId: 4,
2501
3499
  path: 'vol0:firmware.bin',
2502
3500
  },
2503
3501
  });
@@ -2542,6 +3540,30 @@ describe('Protocol V2 firmware update method', () => {
2542
3540
  expect(typedCall).not.toHaveBeenCalled();
2543
3541
  });
2544
3542
 
3543
+ test.each([0, 2])(
3544
+ 'rejects firmware target %s because the Pro2 bootloader cannot install it',
3545
+ async targetId => {
3546
+ const typedCall = jest.fn();
3547
+ const method = new DeviceFirmwareUpdate({
3548
+ id: 1,
3549
+ payload: {
3550
+ method: 'deviceFirmwareUpdate',
3551
+ targetId,
3552
+ path: 'vol0:firmware.bin',
3553
+ },
3554
+ });
3555
+ method.init();
3556
+ (method as any).device = stubDevice({
3557
+ commands: { typedCall },
3558
+ });
3559
+
3560
+ await expect(method.run()).rejects.toMatchObject({
3561
+ errorCode: HardwareErrorCode.CallMethodInvalidParameter,
3562
+ });
3563
+ expect(typedCall).not.toHaveBeenCalled();
3564
+ }
3565
+ );
3566
+
2545
3567
  test('accepts targetId alias inside firmware targets', async () => {
2546
3568
  const typedCall = jest.fn().mockResolvedValue({ message: {} });
2547
3569
  const method = new DeviceFirmwareUpdate({
@@ -2551,8 +3573,8 @@ describe('Protocol V2 firmware update method', () => {
2551
3573
  targets: [
2552
3574
  {
2553
3575
  target_id: undefined,
2554
- targetId: 3,
2555
- path: 'vol0:firmware.bin',
3576
+ targetId: 1,
3577
+ path: 'vol0:resource.crate.okpkg',
2556
3578
  },
2557
3579
  ],
2558
3580
  } as any,
@@ -2564,31 +3586,71 @@ describe('Protocol V2 firmware update method', () => {
2564
3586
 
2565
3587
  await method.run();
2566
3588
  expect(typedCall.mock.calls[0][2]).toEqual({
2567
- targets: [{ target_id: 3, path: 'vol0:firmware.bin' }],
3589
+ targets: [{ target_id: 1, path: 'vol0:resource.crate.okpkg' }],
2568
3590
  });
2569
3591
  });
2570
- });
2571
3592
 
2572
- describe('Protocol V2 reboot methods', () => {
2573
- test('sends DevReboot from devReboot', async () => {
2574
- const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2575
- const method = new DevReboot({
3593
+ test('accepts firmware target enum names from low-level update params', async () => {
3594
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3595
+ const method = new DeviceFirmwareUpdate({
2576
3596
  id: 1,
2577
3597
  payload: {
2578
- method: 'devReboot',
2579
- rebootType: 2,
3598
+ method: 'deviceFirmwareUpdate',
3599
+ targetId: 'FW_MGMT_TARGET_APPLICATION_P1',
3600
+ path: 'vol0:application_p1.bin',
2580
3601
  },
2581
3602
  });
2582
3603
  method.init();
2583
- (method as any).device = stubDevice({ commands: { typedCall } });
3604
+ (method as any).device = stubDevice({
3605
+ commands: { typedCall },
3606
+ });
2584
3607
 
2585
3608
  await method.run();
3609
+ expect(typedCall.mock.calls[0][2]).toEqual({
3610
+ targets: [{ target_id: 4, path: 'vol0:application_p1.bin' }],
3611
+ });
3612
+ });
2586
3613
 
2587
- expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
2588
- reboot_type: 2,
3614
+ test('passes firmware update status fields through to Protocol V2', async () => {
3615
+ const typedCall = jest.fn().mockResolvedValue({
3616
+ message: {
3617
+ records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
3618
+ },
3619
+ });
3620
+ const method = new DeviceGetFirmwareUpdateStatus({
3621
+ id: 1,
3622
+ payload: {
3623
+ method: 'deviceGetFirmwareUpdateStatus',
3624
+ fields: {
3625
+ status: true,
3626
+ payload_version: true,
3627
+ path: true,
3628
+ },
3629
+ },
2589
3630
  });
3631
+ method.init();
3632
+ (method as any).device = stubDevice({
3633
+ commands: { typedCall },
3634
+ });
3635
+
3636
+ await expect(method.run()).resolves.toEqual({
3637
+ records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
3638
+ });
3639
+ expect(typedCall).toHaveBeenCalledWith(
3640
+ 'DeviceFirmwareUpdateStatusGet',
3641
+ 'DeviceFirmwareUpdateStatus',
3642
+ {
3643
+ fields: {
3644
+ status: true,
3645
+ payload_version: true,
3646
+ path: true,
3647
+ },
3648
+ }
3649
+ );
2590
3650
  });
3651
+ });
2591
3652
 
3653
+ describe('Protocol V2 reboot methods', () => {
2592
3654
  test('sends DeviceReboot from deviceReboot', async () => {
2593
3655
  const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2594
3656
  const method = new DeviceReboot({
@@ -2609,12 +3671,97 @@ describe('Protocol V2 reboot methods', () => {
2609
3671
  });
2610
3672
  });
2611
3673
 
3674
+ describe('Protocol V2 current low-level methods', () => {
3675
+ test('sends ProtocolInfoRequest from protocolInfoRequest', async () => {
3676
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } });
3677
+ const method = new ProtocolInfoRequest({
3678
+ id: 1,
3679
+ payload: { method: 'protocolInfoRequest' },
3680
+ });
3681
+ method.init();
3682
+ (method as any).device = stubDevice({ commands: { typedCall } });
3683
+
3684
+ await method.run();
3685
+
3686
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {});
3687
+ });
3688
+
3689
+ test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
3690
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3691
+ const setMethod = new DeviceFactoryInfoSet({
3692
+ id: 1,
3693
+ payload: {
3694
+ method: 'deviceFactoryInfoSet',
3695
+ serial_number: 'PR2SERIAL',
3696
+ burn_in_completed: true,
3697
+ },
3698
+ });
3699
+ setMethod.init();
3700
+ (setMethod as any).device = stubDevice({ commands: { typedCall } });
3701
+
3702
+ await setMethod.run();
3703
+
3704
+ expect(typedCall).toHaveBeenCalledWith('DeviceFactoryInfoSet', 'Success', {
3705
+ info: {
3706
+ version: undefined,
3707
+ serial_number: 'PR2SERIAL',
3708
+ burn_in_completed: true,
3709
+ factory_test_completed: undefined,
3710
+ manufacture_time: undefined,
3711
+ },
3712
+ });
3713
+
3714
+ typedCall.mockResolvedValueOnce({ message: { serial_number: 'PR2SERIAL' } });
3715
+ const getMethod = new DeviceFactoryInfoGet({
3716
+ id: 2,
3717
+ payload: { method: 'deviceFactoryInfoGet' },
3718
+ });
3719
+ getMethod.init();
3720
+ (getMethod as any).device = stubDevice({ commands: { typedCall } });
3721
+
3722
+ await getMethod.run();
3723
+
3724
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceFactoryInfoGet', 'DeviceFactoryInfo', {});
3725
+ });
3726
+
3727
+ test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {
3728
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3729
+ const method = new FilesystemPermissionFix({
3730
+ id: 1,
3731
+ payload: { method: 'filesystemPermissionFix' },
3732
+ });
3733
+ method.init();
3734
+ (method as any).device = stubDevice({ commands: { typedCall } });
3735
+
3736
+ await method.run();
3737
+
3738
+ expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {});
3739
+ });
3740
+
3741
+ test('sends required FilesystemFormat partition flags', async () => {
3742
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3743
+ const method = new FilesystemFormat({
3744
+ id: 1,
3745
+ payload: { method: 'filesystemFormat' },
3746
+ });
3747
+ method.init();
3748
+ (method as any).device = stubDevice({ commands: { typedCall } });
3749
+
3750
+ await method.run();
3751
+
3752
+ expect(typedCall).toHaveBeenCalledWith('FilesystemFormat', 'Success', {
3753
+ data: true,
3754
+ user: true,
3755
+ });
3756
+ });
3757
+ });
3758
+
2612
3759
  describe('Protocol V2 raw device info method', () => {
2613
3760
  const buildMethod = (payload: Record<string, unknown> = {}) => {
2614
- const method = new DeviceGetDeviceInfo({
3761
+ const method = new DeviceInfoGet({
2615
3762
  id: 1,
2616
3763
  payload: {
2617
- method: 'deviceGetDeviceInfo',
3764
+ method: 'deviceInfoGet',
2618
3765
  ...payload,
2619
3766
  },
2620
3767
  });
@@ -2624,7 +3771,7 @@ describe('Protocol V2 raw device info method', () => {
2624
3771
 
2625
3772
  test('passes requested targets/types through and returns the raw DeviceInfo message', async () => {
2626
3773
  const method = buildMethod({
2627
- targets: { hw: true, se1: true, junk: true },
3774
+ targets: { hw: true, se1: true },
2628
3775
  types: { version: true, hash: true },
2629
3776
  });
2630
3777
  const typedCall = jest.fn().mockResolvedValue({
@@ -2640,9 +3787,8 @@ describe('Protocol V2 raw device info method', () => {
2640
3787
  protocol_version: 1,
2641
3788
  hw: { serial_no: 'PR2SERIAL' },
2642
3789
  });
2643
- // 未知 key(junk)被过滤,已选 key 原样透传,不构建 DeviceProfile
2644
3790
  expect(typedCall).toHaveBeenCalledWith(
2645
- 'DevGetDeviceInfo',
3791
+ 'DeviceInfoGet',
2646
3792
  'DeviceInfo',
2647
3793
  {
2648
3794
  targets: { hw: true, se1: true },
@@ -2652,6 +3798,20 @@ describe('Protocol V2 raw device info method', () => {
2652
3798
  );
2653
3799
  });
2654
3800
 
3801
+ test('rejects removed target/type names before transport call', () => {
3802
+ expect(() =>
3803
+ buildMethod({
3804
+ targets: { hw: true, bt: true },
3805
+ })
3806
+ ).toThrow('targets');
3807
+
3808
+ expect(() =>
3809
+ buildMethod({
3810
+ types: { version: true, digest: true },
3811
+ })
3812
+ ).toThrow('types');
3813
+ });
3814
+
2655
3815
  test('defaults to the basic targets/types when none are given', async () => {
2656
3816
  const method = buildMethod();
2657
3817
  const typedCall = jest.fn().mockResolvedValue({ type: 'DeviceInfo', message: {} });
@@ -2663,10 +3823,10 @@ describe('Protocol V2 raw device info method', () => {
2663
3823
  await method.run();
2664
3824
 
2665
3825
  expect(typedCall).toHaveBeenCalledWith(
2666
- 'DevGetDeviceInfo',
3826
+ 'DeviceInfoGet',
2667
3827
  'DeviceInfo',
2668
3828
  {
2669
- targets: { hw: true, fw: true, bt: true, status: true },
3829
+ targets: { hw: true, fw: true, coprocessor: true, status: true },
2670
3830
  types: { version: true, specific: true },
2671
3831
  },
2672
3832
  { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
@@ -2686,48 +3846,6 @@ describe('Protocol V2 raw device info method', () => {
2686
3846
  });
2687
3847
  });
2688
3848
 
2689
- describe('Protocol V2 onboarding status method', () => {
2690
- test('returns OnboardingStatus from low-level status query', async () => {
2691
- const method = new DeviceGetOnboardingStatus({
2692
- id: 1,
2693
- payload: {
2694
- method: 'deviceGetOnboardingStatus',
2695
- },
2696
- });
2697
- method.init();
2698
-
2699
- const typedCall = jest.fn().mockResolvedValue({
2700
- type: 'OnboardingStatus',
2701
- message: {
2702
- step: 3,
2703
- setup: {
2704
- restore: {
2705
- mnemonic: true,
2706
- },
2707
- },
2708
- detail_code: 7,
2709
- detail_str: 'Recovery Phrase',
2710
- },
2711
- });
2712
-
2713
- (method as any).device = stubDevice({
2714
- commands: { typedCall },
2715
- });
2716
-
2717
- await expect(method.run()).resolves.toEqual({
2718
- step: 3,
2719
- setup: {
2720
- restore: {
2721
- mnemonic: true,
2722
- },
2723
- },
2724
- detail_code: 7,
2725
- detail_str: 'Recovery Phrase',
2726
- });
2727
- expect(typedCall).toHaveBeenCalledWith('GetOnboardingStatus', 'OnboardingStatus', {});
2728
- });
2729
- });
2730
-
2731
3849
  describe('Protocol V2 file write method', () => {
2732
3850
  test('rejects invalid write parameters before transport call', () => {
2733
3851
  expect(() => {
@@ -2777,17 +3895,22 @@ describe('Protocol V2 file write method', () => {
2777
3895
  method.init();
2778
3896
  await method.run();
2779
3897
 
2780
- expect(typedCall).toHaveBeenCalledWith('FilesystemFileWrite', 'FilesystemFile', {
2781
- file: {
2782
- path: 'vol1:test.bin',
2783
- offset: 1,
2784
- total_size: 2,
2785
- data: new Uint8Array([1]),
3898
+ expect(typedCall).toHaveBeenCalledWith(
3899
+ 'FilesystemFileWrite',
3900
+ 'FilesystemFile',
3901
+ {
3902
+ file: {
3903
+ path: 'vol1:test.bin',
3904
+ offset: 1,
3905
+ total_size: 2,
3906
+ data: new Uint8Array([1]),
3907
+ },
3908
+ overwrite: false,
3909
+ append: false,
3910
+ ui_percentage: 100,
2786
3911
  },
2787
- overwrite: false,
2788
- append: false,
2789
- ui_percentage: 99,
2790
- }, { timeoutMs: undefined });
3912
+ { timeoutMs: undefined }
3913
+ );
2791
3914
  expect(method.postMessage).toHaveBeenCalledWith({
2792
3915
  event: 'UI_EVENT',
2793
3916
  type: UI_REQUEST.DEVICE_PROGRESS,
@@ -2820,28 +3943,40 @@ describe('Protocol V2 file write method', () => {
2820
3943
  const result = await method.run();
2821
3944
 
2822
3945
  expect(typedCall).toHaveBeenCalledTimes(2);
2823
- expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemFileWrite', 'FilesystemFile', {
2824
- file: {
2825
- path: 'vol1:test.bin',
2826
- offset: 0,
2827
- total_size: 4097,
2828
- data: data.slice(0, 4096),
2829
- },
2830
- overwrite: true,
2831
- append: false,
2832
- ui_percentage: 99,
2833
- }, { timeoutMs: undefined });
2834
- expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileWrite', 'FilesystemFile', {
2835
- file: {
2836
- path: 'vol1:test.bin',
2837
- offset: 4096,
2838
- total_size: 4097,
2839
- data: data.slice(4096),
2840
- },
2841
- overwrite: false,
2842
- append: false,
2843
- ui_percentage: 99,
2844
- }, { timeoutMs: undefined });
3946
+ expect(typedCall).toHaveBeenNthCalledWith(
3947
+ 1,
3948
+ 'FilesystemFileWrite',
3949
+ 'FilesystemFile',
3950
+ {
3951
+ file: {
3952
+ path: 'vol1:test.bin',
3953
+ offset: 0,
3954
+ total_size: 4097,
3955
+ data: data.slice(0, 4000),
3956
+ },
3957
+ overwrite: true,
3958
+ append: false,
3959
+ ui_percentage: 0,
3960
+ },
3961
+ { timeoutMs: undefined }
3962
+ );
3963
+ expect(typedCall).toHaveBeenNthCalledWith(
3964
+ 2,
3965
+ 'FilesystemFileWrite',
3966
+ 'FilesystemFile',
3967
+ {
3968
+ file: {
3969
+ path: 'vol1:test.bin',
3970
+ offset: 4000,
3971
+ total_size: 4097,
3972
+ data: data.slice(4000),
3973
+ },
3974
+ overwrite: false,
3975
+ append: false,
3976
+ ui_percentage: 100,
3977
+ },
3978
+ { timeoutMs: undefined }
3979
+ );
2845
3980
  expect(result).toMatchObject({
2846
3981
  path: 'vol1:test.bin',
2847
3982
  processed_byte: 4097,
@@ -2851,8 +3986,8 @@ describe('Protocol V2 file write method', () => {
2851
3986
  event: 'UI_EVENT',
2852
3987
  type: UI_REQUEST.DEVICE_PROGRESS,
2853
3988
  payload: expect.objectContaining({
2854
- progress: 99,
2855
- transferredBytes: 4096,
3989
+ progress: 97,
3990
+ transferredBytes: 4000,
2856
3991
  totalBytes: 4097,
2857
3992
  elapsedMs: expect.any(Number),
2858
3993
  }),
@@ -2901,6 +4036,36 @@ describe('Protocol V2 file write method', () => {
2901
4036
  });
2902
4037
 
2903
4038
  describe('Protocol V2 file read method', () => {
4039
+ test('uses a 900-byte chunk limit by default in BLE environments', async () => {
4040
+ const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
4041
+ const typedCall = jest
4042
+ .fn()
4043
+ .mockResolvedValueOnce({ message: { data: new Uint8Array(900) } })
4044
+ .mockResolvedValueOnce({ message: { data: new Uint8Array(1) } });
4045
+ const method = new FileRead({
4046
+ id: 1,
4047
+ payload: {
4048
+ method: 'fileRead',
4049
+ path: 'vol1:test.bin',
4050
+ offset: 0,
4051
+ totalSize: 901,
4052
+ },
4053
+ });
4054
+ (method as any).device = stubDevice({ commands: { typedCall } });
4055
+
4056
+ try {
4057
+ method.init();
4058
+ await method.run();
4059
+ } finally {
4060
+ getSettingsSpy.mockRestore();
4061
+ }
4062
+
4063
+ expect(typedCall).toHaveBeenCalledTimes(2);
4064
+ expect(typedCall.mock.calls[0][2].chunk_len).toBe(900);
4065
+ expect(typedCall.mock.calls[1][2].file.offset).toBe(900);
4066
+ expect(typedCall.mock.calls[1][2].chunk_len).toBe(1);
4067
+ });
4068
+
2904
4069
  test('rejects invalid read and directory parameters before transport call', () => {
2905
4070
  expect(() => {
2906
4071
  const method = new FileRead({
@@ -2977,7 +4142,7 @@ describe('Protocol V2 file read method', () => {
2977
4142
  total_size: 0,
2978
4143
  },
2979
4144
  chunk_len: 64,
2980
- ui_percentage: 99,
4145
+ ui_percentage: 0,
2981
4146
  });
2982
4147
  expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', {
2983
4148
  file: {
@@ -2986,7 +4151,7 @@ describe('Protocol V2 file read method', () => {
2986
4151
  total_size: 0,
2987
4152
  },
2988
4153
  chunk_len: 1,
2989
- ui_percentage: 99,
4154
+ ui_percentage: 100,
2990
4155
  });
2991
4156
  expect(result.data.byteLength).toBe(65);
2992
4157
  expect(result.data[0]).toBe(1);