@onekeyfe/hd-core 1.1.26 → 1.1.27-alpha.30

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 (156) hide show
  1. package/__tests__/protocol-v2.test.ts +940 -0
  2. package/dist/api/BaseMethod.d.ts +1 -3
  3. package/dist/api/BaseMethod.d.ts.map +1 -1
  4. package/dist/api/DirList.d.ts +10 -0
  5. package/dist/api/DirList.d.ts.map +1 -0
  6. package/dist/api/DirMake.d.ts +9 -0
  7. package/dist/api/DirMake.d.ts.map +1 -0
  8. package/dist/api/DirRemove.d.ts +9 -0
  9. package/dist/api/DirRemove.d.ts.map +1 -0
  10. package/dist/api/FileDelete.d.ts +9 -0
  11. package/dist/api/FileDelete.d.ts.map +1 -0
  12. package/dist/api/FileRead.d.ts +19 -0
  13. package/dist/api/FileRead.d.ts.map +1 -0
  14. package/dist/api/FileWrite.d.ts +23 -0
  15. package/dist/api/FileWrite.d.ts.map +1 -0
  16. package/dist/api/FirmwareUpdateV3.d.ts +1 -0
  17. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  18. package/dist/api/FirmwareUpdateV4.d.ts +32 -0
  19. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -0
  20. package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
  21. package/dist/api/PathInfo.d.ts +9 -0
  22. package/dist/api/PathInfo.d.ts.map +1 -0
  23. package/dist/api/SearchDevices.d.ts +2 -1
  24. package/dist/api/SearchDevices.d.ts.map +1 -1
  25. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  26. package/dist/api/device/DeviceRebootToBoardloader.d.ts +1 -1
  27. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  28. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +10 -2
  30. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  31. package/dist/api/index.d.ts +26 -0
  32. package/dist/api/index.d.ts.map +1 -1
  33. package/dist/api/protocol-v2/DevFirmwareUpdate.d.ts +7 -0
  34. package/dist/api/protocol-v2/DevFirmwareUpdate.d.ts.map +1 -0
  35. package/dist/api/protocol-v2/DevGetDeviceInfo.d.ts +7 -0
  36. package/dist/api/protocol-v2/DevGetDeviceInfo.d.ts.map +1 -0
  37. package/dist/api/protocol-v2/DevGetFirmwareUpdateStatus.d.ts +6 -0
  38. package/dist/api/protocol-v2/DevGetFirmwareUpdateStatus.d.ts.map +1 -0
  39. package/dist/api/protocol-v2/DevGetOnboardingStatus.d.ts +6 -0
  40. package/dist/api/protocol-v2/DevGetOnboardingStatus.d.ts.map +1 -0
  41. package/dist/api/protocol-v2/DevReboot.d.ts +7 -0
  42. package/dist/api/protocol-v2/DevReboot.d.ts.map +1 -0
  43. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +7 -0
  44. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +1 -0
  45. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +6 -0
  46. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +1 -0
  47. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts +6 -0
  48. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +1 -0
  49. package/dist/api/protocol-v2/FilesystemFormat.d.ts +6 -0
  50. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -0
  51. package/dist/api/protocol-v2/GetProtoVersion.d.ts +6 -0
  52. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +1 -0
  53. package/dist/api/protocol-v2/Ping.d.ts +8 -0
  54. package/dist/api/protocol-v2/Ping.d.ts.map +1 -0
  55. package/dist/api/protocol-v2/helpers.d.ts +49 -0
  56. package/dist/api/protocol-v2/helpers.d.ts.map +1 -0
  57. package/dist/core/index.d.ts.map +1 -1
  58. package/dist/data-manager/DataManager.d.ts +4 -2
  59. package/dist/data-manager/DataManager.d.ts.map +1 -1
  60. package/dist/data-manager/TransportManager.d.ts +2 -1
  61. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  62. package/dist/device/Device.d.ts +5 -3
  63. package/dist/device/Device.d.ts.map +1 -1
  64. package/dist/device/DeviceCommands.d.ts +8 -8
  65. package/dist/device/DeviceCommands.d.ts.map +1 -1
  66. package/dist/device/DeviceConnector.d.ts +2 -1
  67. package/dist/device/DeviceConnector.d.ts.map +1 -1
  68. package/dist/events/ui-request.d.ts +8 -0
  69. package/dist/events/ui-request.d.ts.map +1 -1
  70. package/dist/index.d.ts +188 -20
  71. package/dist/index.js +15626 -753
  72. package/dist/inject.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/features.d.ts +56 -0
  74. package/dist/protocols/protocol-v2/features.d.ts.map +1 -0
  75. package/dist/protocols/protocol-v2/firmware.d.ts +12 -0
  76. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -0
  77. package/dist/protocols/protocol-v2/index.d.ts +3 -0
  78. package/dist/protocols/protocol-v2/index.d.ts.map +1 -0
  79. package/dist/types/api/export.d.ts +1 -1
  80. package/dist/types/api/export.d.ts.map +1 -1
  81. package/dist/types/api/firmwareUpdate.d.ts +7 -0
  82. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  83. package/dist/types/api/index.d.ts +28 -1
  84. package/dist/types/api/index.d.ts.map +1 -1
  85. package/dist/types/api/protocolV2.d.ts +123 -0
  86. package/dist/types/api/protocolV2.d.ts.map +1 -0
  87. package/dist/types/api/searchDevices.d.ts +2 -2
  88. package/dist/types/api/searchDevices.d.ts.map +1 -1
  89. package/dist/types/device.d.ts +1 -1
  90. package/dist/types/device.d.ts.map +1 -1
  91. package/dist/types/params.d.ts +2 -0
  92. package/dist/types/params.d.ts.map +1 -1
  93. package/dist/types/settings.d.ts +1 -1
  94. package/dist/types/settings.d.ts.map +1 -1
  95. package/dist/utils/deviceInfoUtils.d.ts +1 -0
  96. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  97. package/dist/utils/index.d.ts +1 -1
  98. package/dist/utils/index.d.ts.map +1 -1
  99. package/dist/utils/patch.d.ts +1 -1
  100. package/dist/utils/patch.d.ts.map +1 -1
  101. package/dist/utils/versionUtils.d.ts +1 -1
  102. package/package.json +4 -4
  103. package/src/api/BaseMethod.ts +12 -60
  104. package/src/api/DirList.ts +25 -0
  105. package/src/api/DirMake.ts +20 -0
  106. package/src/api/DirRemove.ts +20 -0
  107. package/src/api/FileDelete.ts +20 -0
  108. package/src/api/FileRead.ts +158 -0
  109. package/src/api/FileWrite.ts +191 -0
  110. package/src/api/FirmwareUpdateV3.ts +21 -4
  111. package/src/api/FirmwareUpdateV4.ts +810 -0
  112. package/src/api/GetOnekeyFeatures.ts +75 -3
  113. package/src/api/PathInfo.ts +24 -0
  114. package/src/api/SearchDevices.ts +7 -2
  115. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +10 -9
  116. package/src/api/device/DeviceRebootToBoardloader.ts +10 -1
  117. package/src/api/device/DeviceRebootToBootloader.ts +10 -1
  118. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +27 -4
  119. package/src/api/index.ts +28 -0
  120. package/src/api/protocol-v2/DevFirmwareUpdate.ts +33 -0
  121. package/src/api/protocol-v2/DevGetDeviceInfo.ts +35 -0
  122. package/src/api/protocol-v2/DevGetFirmwareUpdateStatus.ts +18 -0
  123. package/src/api/protocol-v2/DevGetOnboardingStatus.ts +18 -0
  124. package/src/api/protocol-v2/DevReboot.ts +22 -0
  125. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +27 -0
  126. package/src/api/protocol-v2/FactoryGetDeviceInfo.ts +18 -0
  127. package/src/api/protocol-v2/FilesystemFixPermission.ts +14 -0
  128. package/src/api/protocol-v2/FilesystemFormat.ts +14 -0
  129. package/src/api/protocol-v2/GetProtoVersion.ts +14 -0
  130. package/src/api/protocol-v2/Ping.ts +16 -0
  131. package/src/api/protocol-v2/helpers.ts +138 -0
  132. package/src/core/index.ts +26 -4
  133. package/src/data/messages/messages-pro2.json +13102 -0
  134. package/src/data-manager/DataManager.ts +6 -2
  135. package/src/data-manager/TransportManager.ts +29 -3
  136. package/src/device/Device.ts +68 -8
  137. package/src/device/DeviceCommands.ts +162 -26
  138. package/src/device/DeviceConnector.ts +29 -4
  139. package/src/device/DevicePool.ts +1 -1
  140. package/src/events/ui-request.ts +8 -0
  141. package/src/inject.ts +42 -1
  142. package/src/protocols/protocol-v2/features.ts +266 -0
  143. package/src/protocols/protocol-v2/firmware.ts +26 -0
  144. package/src/protocols/protocol-v2/index.ts +2 -0
  145. package/src/types/api/export.ts +1 -0
  146. package/src/types/api/firmwareUpdate.ts +12 -0
  147. package/src/types/api/index.ts +63 -1
  148. package/src/types/api/protocolV2.ts +221 -0
  149. package/src/types/api/searchDevices.ts +2 -2
  150. package/src/types/device.ts +3 -1
  151. package/src/types/params.ts +7 -0
  152. package/src/types/settings.ts +1 -1
  153. package/src/utils/deviceInfoUtils.ts +14 -5
  154. package/src/utils/index.ts +1 -0
  155. package/__tests__/DeviceCommands.test.ts +0 -99
  156. package/__tests__/evmLedgerLegacySafety.test.ts +0 -261
@@ -0,0 +1,940 @@
1
+ import JSZip from 'jszip';
2
+ import { DevRebootType } from '@onekeyfe/hd-transport';
3
+
4
+ import FileRead from '../src/api/FileRead';
5
+ import FileWrite from '../src/api/FileWrite';
6
+ import DevFirmwareUpdate from '../src/api/protocol-v2/DevFirmwareUpdate';
7
+ import DevGetOnboardingStatus from '../src/api/protocol-v2/DevGetOnboardingStatus';
8
+ import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
9
+ import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4';
10
+ import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures';
11
+ import KaspaGetAddress from '../src/api/kaspa/KaspaGetAddress';
12
+ import { DataManager } from '../src/data-manager';
13
+ import { Device } from '../src/device/Device';
14
+ import { UI_REQUEST } from '../src/events/ui-request';
15
+ import { getProtocolV2Features, normalizeProtocolV2Features } from '../src/protocols/protocol-v2';
16
+ import { shouldSkipMethodSupportCheck } from '../src/utils';
17
+
18
+ import type { DeviceCommands } from '../src/device/DeviceCommands';
19
+
20
+ jest.mock('../src/data/config', () => ({
21
+ getSDKVersion: jest.fn(() => '1.0.0'),
22
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
23
+ }));
24
+
25
+ const descriptor = {
26
+ id: 'ble-id',
27
+ path: 'usb-path',
28
+ };
29
+
30
+ describe('Protocol V2 feature adapter', () => {
31
+ test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
32
+ const features = normalizeProtocolV2Features(descriptor as any, {
33
+ hw: {
34
+ serial_no: 'PR2SERIAL',
35
+ },
36
+ fw: {
37
+ board: {
38
+ version: '0.1.0',
39
+ hash: [1, 2, 255],
40
+ },
41
+ boot: {
42
+ version: '0.2.0',
43
+ build_id: 'boot-build',
44
+ hash: new Uint8Array([10, 11]),
45
+ },
46
+ app: {
47
+ version: '1.2.3',
48
+ build_id: 'app-build',
49
+ hash: 'abc123',
50
+ },
51
+ },
52
+ bt: {
53
+ app: {
54
+ version: '4.5.6',
55
+ build_id: 'bt-build',
56
+ hash: [12, 13],
57
+ },
58
+ adv_name: 'Pro2 BLE',
59
+ },
60
+ se1: {
61
+ app: {
62
+ version: '7.8.9',
63
+ build_id: 'se-build',
64
+ hash: [14, 15],
65
+ },
66
+ state: 85,
67
+ },
68
+ se2: {
69
+ app: {
70
+ version: '8.0.0',
71
+ },
72
+ state: 0,
73
+ },
74
+ status: {
75
+ label: 'My Pro2',
76
+ language: 'en-US',
77
+ bt_enable: true,
78
+ init_states: false,
79
+ backup_required: true,
80
+ passphrase_protection: true,
81
+ },
82
+ });
83
+
84
+ expect(features.device_id).toBe('PR2SERIAL');
85
+ expect(features.serial_no).toBe('PR2SERIAL');
86
+ expect(features.onekey_serial_no).toBe('PR2SERIAL');
87
+ expect(features.onekey_device_type).toBe('pro2');
88
+ expect(features.major_version).toBe(1);
89
+ expect(features.minor_version).toBe(2);
90
+ expect(features.patch_version).toBe(3);
91
+ expect(features.onekey_firmware_version).toBe('1.2.3');
92
+ expect(features.onekey_firmware_build_id).toBe('app-build');
93
+ expect(features.onekey_firmware_hash).toBe('abc123');
94
+ expect(features.bootloader_version).toBe('0.2.0');
95
+ expect(features.onekey_boot_build_id).toBe('boot-build');
96
+ expect(features.onekey_boot_hash).toBe('0a0b');
97
+ expect(features.onekey_board_hash).toBe('0102ff');
98
+ expect(features.ble_name).toBe('Pro2 BLE');
99
+ expect(features.onekey_ble_version).toBe('4.5.6');
100
+ expect(features.onekey_ble_hash).toBe('0c0d');
101
+ expect(features.onekey_se01_version).toBe('7.8.9');
102
+ expect(features.onekey_se01_hash).toBe('0e0f');
103
+ expect(features.onekey_se01_state).toBe('APP');
104
+ expect(features.onekey_se02_state).toBe('BOOT');
105
+ expect(features.label).toBe('My Pro2');
106
+ expect(features.language).toBe('en-US');
107
+ expect(features.initialized).toBe(false);
108
+ expect(features.needs_backup).toBe(true);
109
+ expect(features.passphrase_protection).toBe(true);
110
+ expect(features.ble_enable).toBe(true);
111
+ });
112
+
113
+ test('marks fallback features as unavailable when DeviceInfo is missing', () => {
114
+ const features = normalizeProtocolV2Features(descriptor as any);
115
+
116
+ expect(features.device_id).toBe('usb-path');
117
+ expect(features.serial_no).toBe('usb-path');
118
+ expect(features.onekey_serial_no).toBe('usb-path');
119
+ expect(features.initialized).toBe(false);
120
+ expect(features.unlocked).toBe(false);
121
+ expect(features.firmware_present).toBe(false);
122
+ });
123
+
124
+ test('bypasses legacy method support checks for Protocol V2 fallback features', () => {
125
+ const features = normalizeProtocolV2Features({
126
+ ...descriptor,
127
+ protocolType: 'V2',
128
+ } as any);
129
+
130
+ expect(shouldSkipMethodSupportCheck(features, 'V2')).toBe(true);
131
+ expect(shouldSkipMethodSupportCheck(features)).toBe(true);
132
+ });
133
+
134
+ test('initializes Protocol V2 features with Ping only while DeviceInfo is unsupported', async () => {
135
+ const onDeviceInfoError = jest.fn();
136
+ const commands = {
137
+ typedCall: jest.fn().mockResolvedValueOnce({ type: 'Success', message: { message: 'pong' } }),
138
+ };
139
+
140
+ const features = await getProtocolV2Features({
141
+ commands: commands as unknown as DeviceCommands,
142
+ descriptor: descriptor as any,
143
+ onDeviceInfoError,
144
+ });
145
+
146
+ expect(features.device_id).toBe('usb-path');
147
+ expect(commands.typedCall).toHaveBeenNthCalledWith(1, 'Ping', 'Success', { message: 'init' });
148
+ expect(commands.typedCall).toHaveBeenCalledTimes(1);
149
+ expect(onDeviceInfoError).not.toHaveBeenCalled();
150
+ });
151
+
152
+ test('does not block method-level legacy version checks on Protocol V2', async () => {
153
+ const method = new KaspaGetAddress({
154
+ id: 1,
155
+ payload: {
156
+ method: 'kaspaGetAddress',
157
+ path: "m/44'/111111'/0'/0/0",
158
+ prefix: 'kaspa',
159
+ showOnOneKey: false,
160
+ useTweak: false,
161
+ },
162
+ });
163
+ const typedCall = jest.fn().mockResolvedValue({
164
+ type: 'KaspaAddress',
165
+ message: {
166
+ address: 'kaspa:test-address',
167
+ },
168
+ });
169
+
170
+ method.init();
171
+ method.postMessage = jest.fn();
172
+ (method as any).device = {
173
+ originalDescriptor: { protocolType: 'V2' },
174
+ features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
175
+ commands: { typedCall },
176
+ toMessageObject: jest.fn(() => ({})),
177
+ };
178
+
179
+ await expect(method.run()).resolves.toMatchObject({
180
+ address: 'kaspa:test-address',
181
+ });
182
+ expect(typedCall).toHaveBeenCalledWith('KaspaGetAddress', 'KaspaAddress', expect.any(Object));
183
+ });
184
+
185
+ test('returns Protocol V2 oneKey fields without calling legacy OnekeyGetFeatures', async () => {
186
+ const method = new GetOnekeyFeatures({
187
+ id: 1,
188
+ payload: {
189
+ method: 'getOnekeyFeatures',
190
+ },
191
+ });
192
+ const typedCall = jest.fn();
193
+
194
+ (method as any).device = {
195
+ originalDescriptor: { protocolType: 'V2' },
196
+ commands: { typedCall },
197
+ features: {
198
+ label: 'ignored label',
199
+ onekey_device_type: 'pro2',
200
+ onekey_firmware_version: '1.2.3',
201
+ onekey_firmware_build_id: 'app-build',
202
+ onekey_serial_no: 'PR2SERIAL',
203
+ onekey_ble_name: 'Pro2 BLE',
204
+ },
205
+ };
206
+
207
+ const message = await method.run();
208
+
209
+ expect(typedCall).not.toHaveBeenCalled();
210
+ expect(message).toMatchObject({
211
+ onekey_device_type: 'pro2',
212
+ onekey_firmware_version: '1.2.3',
213
+ onekey_firmware_build_id: 'app-build',
214
+ onekey_serial_no: 'PR2SERIAL',
215
+ onekey_ble_name: 'Pro2 BLE',
216
+ });
217
+ expect(message).not.toHaveProperty('label');
218
+ });
219
+
220
+ test('reuses cached Protocol V2 features after the first initialization', async () => {
221
+ const device = Device.fromDescriptor({
222
+ path: 'usb-path',
223
+ protocolType: 'V2',
224
+ } as any);
225
+ const typedCall = jest
226
+ .fn()
227
+ .mockResolvedValueOnce({ type: 'Success', message: { message: 'init' } });
228
+
229
+ (device as any).commands = { typedCall };
230
+
231
+ await device.initialize();
232
+ await device.initialize();
233
+
234
+ expect(device.features?.device_id).toBe('usb-path');
235
+ expect(typedCall).toHaveBeenCalledTimes(1);
236
+ expect(typedCall).toHaveBeenNthCalledWith(
237
+ 1,
238
+ 'Ping',
239
+ 'Success',
240
+ { message: 'init' },
241
+ {
242
+ timeoutMs: 10000,
243
+ }
244
+ );
245
+ });
246
+ });
247
+
248
+ describe('Protocol V2 firmware update targets', () => {
249
+ test('keeps Protocol V2 firmware updates off the legacy firmwareUpdateV3 path', async () => {
250
+ const method = new FirmwareUpdateV3({
251
+ id: 1,
252
+ payload: {
253
+ method: 'firmwareUpdateV3',
254
+ },
255
+ });
256
+ (method as any).device = {
257
+ originalDescriptor: { protocolType: 'V2' },
258
+ };
259
+
260
+ await expect(method.run()).rejects.toThrow('firmwareUpdateV4');
261
+ });
262
+
263
+ test('uses Protocol V2 features after BLE final reconnect without legacy Initialize', async () => {
264
+ const method = new FirmwareUpdateV4({
265
+ id: 1,
266
+ payload: {
267
+ method: 'firmwareUpdateV4',
268
+ },
269
+ });
270
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' });
271
+ const typedCall = jest.fn().mockImplementation((name: string) => {
272
+ if (name === 'Ping') {
273
+ return Promise.resolve({ type: 'Success', message: { message: 'init' } });
274
+ }
275
+ return Promise.reject(new Error(`unexpected call ${name}`));
276
+ });
277
+ const commands = { typedCall };
278
+
279
+ (method as any).isBleReconnect = jest.fn(() => true);
280
+ (method as any).device = {
281
+ originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
282
+ deviceConnector: { acquire },
283
+ getCommands: () => commands,
284
+ _updateFeatures: jest.fn(),
285
+ };
286
+
287
+ const versions = await (method as any).waitForProtocolV2FinalFeatures();
288
+
289
+ expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2');
290
+ expect(typedCall).toHaveBeenNthCalledWith(
291
+ 1,
292
+ 'Ping',
293
+ 'Success',
294
+ { message: 'init' },
295
+ { timeoutMs: 5000 }
296
+ );
297
+ expect(typedCall).toHaveBeenCalledTimes(1);
298
+ expect(typedCall).not.toHaveBeenCalledWith('Initialize', 'Features', {});
299
+ expect(versions).toEqual({
300
+ bootloaderVersion: '0.0.0',
301
+ bleVersion: '0.0.0',
302
+ firmwareVersion: '0.0.0',
303
+ });
304
+ });
305
+
306
+ test('runs Protocol V2 upload and install without rebooting to bootloader first', async () => {
307
+ const method = new FirmwareUpdateV4({
308
+ id: 1,
309
+ payload: {
310
+ method: 'firmwareUpdateV4',
311
+ },
312
+ });
313
+ method.init();
314
+
315
+ (method as any).device = {
316
+ originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
317
+ features: { capabilities: [] },
318
+ };
319
+ (method as any).prepareResourceBinary = jest.fn().mockResolvedValue(null);
320
+ (method as any).prepareFirmwareAndBleBinary = jest.fn().mockResolvedValue([
321
+ {
322
+ fileName: 'ble-firmware.bin',
323
+ binary: new Uint8Array([1, 2, 3]).buffer,
324
+ },
325
+ ]);
326
+ (method as any).prepareBootloaderBinary = jest.fn().mockResolvedValue(null);
327
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
328
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
329
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
330
+ bootloaderVersion: '0.2.0',
331
+ bleVersion: '4.5.6',
332
+ firmwareVersion: '1.2.3',
333
+ });
334
+ (method as any).protocolV2Reboot = jest.fn();
335
+ method.postTipMessage = jest.fn();
336
+
337
+ await method.run();
338
+
339
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
340
+ resourceBinary: null,
341
+ fwBinaryMap: [
342
+ {
343
+ fileName: 'ble-firmware.bin',
344
+ binary: expect.any(ArrayBuffer),
345
+ },
346
+ ],
347
+ bootloaderBinary: null,
348
+ });
349
+ expect((method as any).protocolV2Reboot).not.toHaveBeenCalledWith(DevRebootType.Bootloader);
350
+ expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
351
+ });
352
+
353
+ test('reboots Protocol V2 firmware flow back to normal before final feature polling', async () => {
354
+ const method = new FirmwareUpdateV4({
355
+ id: 1,
356
+ payload: {
357
+ method: 'firmwareUpdateV4',
358
+ },
359
+ });
360
+ method.postTipMessage = jest.fn();
361
+ (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
362
+ message: 'Device rebooted successfully',
363
+ });
364
+
365
+ await (method as any).exitProtocolV2BootloaderToNormal();
366
+
367
+ expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
368
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Normal);
369
+ });
370
+
371
+ test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
372
+ const method = new FirmwareUpdateV4({
373
+ id: 1,
374
+ payload: {
375
+ method: 'firmwareUpdateV4',
376
+ },
377
+ });
378
+ const typedCall = jest
379
+ .fn()
380
+ .mockRejectedValue(
381
+ new Error("The operation couldn't be completed. (MultiplatformBleAdapter.RxError error 6.)")
382
+ );
383
+
384
+ (method as any).device = {
385
+ getCommands: () => ({ typedCall }),
386
+ };
387
+
388
+ await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
389
+ message: 'Device rebooted successfully',
390
+ });
391
+ });
392
+
393
+ test('treats direct disconnect during Protocol V2 normal reboot as expected', async () => {
394
+ const method = new FirmwareUpdateV4({
395
+ id: 1,
396
+ payload: {
397
+ method: 'firmwareUpdateV4',
398
+ },
399
+ });
400
+ const typedCall = jest
401
+ .fn()
402
+ .mockRejectedValue(new Error('Connection error has occured: Device disconnected'));
403
+
404
+ (method as any).device = {
405
+ getCommands: () => ({ typedCall }),
406
+ };
407
+
408
+ await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
409
+ message: 'Device rebooted successfully',
410
+ });
411
+ });
412
+
413
+ test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => {
414
+ const method = new FirmwareUpdateV4({
415
+ id: 1,
416
+ payload: {
417
+ method: 'firmwareUpdateV4',
418
+ },
419
+ });
420
+ const typedCall = jest
421
+ .fn()
422
+ .mockRejectedValueOnce(
423
+ new Error(
424
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
425
+ )
426
+ )
427
+ .mockResolvedValueOnce({
428
+ type: 'DevFirmwareUpdateStatus',
429
+ message: {
430
+ targets: [{ target_id: 2, status: 0 }],
431
+ },
432
+ });
433
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
434
+
435
+ (method as any).device = {
436
+ getCommands: () => ({ typedCall }),
437
+ };
438
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
439
+ method.postProgressMessage = jest.fn();
440
+
441
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
442
+ { target_id: 2, path: 'vol1:ble-firmware.bin' },
443
+ ]);
444
+
445
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
446
+ expect(typedCall).toHaveBeenCalledTimes(2);
447
+ });
448
+
449
+ test('passes resource, bootloader, BLE, SE and app files to DevFirmwareUpdate targets', async () => {
450
+ const resourceZip = new JSZip();
451
+ resourceZip.file('icons/home.png', new Uint8Array([1, 2, 3]));
452
+ const resourceBinary = await resourceZip.generateAsync({ type: 'arraybuffer' });
453
+ const method = new FirmwareUpdateV4({
454
+ id: 1,
455
+ payload: {
456
+ method: 'firmwareUpdateV4',
457
+ },
458
+ });
459
+
460
+ const writtenPaths: string[] = [];
461
+ method.postTipMessage = jest.fn();
462
+ (method as any).protocolV2CreateFolder = jest.fn().mockResolvedValue(undefined);
463
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
464
+ writtenPaths.push(params.filePath);
465
+ return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
466
+ });
467
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
468
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
469
+ .fn()
470
+ .mockResolvedValue(undefined);
471
+
472
+ await (method as any).executeProtocolV2Update({
473
+ resourceBinary,
474
+ bootloaderBinary: new Uint8Array([4, 5]).buffer,
475
+ fwBinaryMap: [
476
+ {
477
+ fileName: 'ble-firmware.bin',
478
+ binary: new Uint8Array([6]).buffer,
479
+ },
480
+ {
481
+ fileName: 'se1-firmware.bin',
482
+ binary: new Uint8Array([7]).buffer,
483
+ },
484
+ {
485
+ fileName: 'firmware.bin',
486
+ binary: new Uint8Array([8]).buffer,
487
+ },
488
+ ],
489
+ });
490
+
491
+ expect((method as any).protocolV2CreateFolder).toHaveBeenCalledWith('vol1:res/');
492
+ expect(writtenPaths).toEqual([
493
+ 'vol1:res/home.png',
494
+ 'vol1:bootloader.bin',
495
+ 'vol1:ble-firmware.bin',
496
+ 'vol1:se1-firmware.bin',
497
+ 'vol1:firmware.bin',
498
+ ]);
499
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
500
+ targets: [
501
+ { target_id: 10, path: 'vol1:res/' },
502
+ { target_id: 1, path: 'vol1:bootloader.bin' },
503
+ { target_id: 2, path: 'vol1:ble-firmware.bin' },
504
+ { target_id: 3, path: 'vol1:se1-firmware.bin' },
505
+ { target_id: 0, path: 'vol1:firmware.bin' },
506
+ ],
507
+ });
508
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
509
+ });
510
+
511
+ test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
512
+ const method = new FirmwareUpdateV4({
513
+ id: 1,
514
+ payload: {
515
+ method: 'firmwareUpdateV4',
516
+ },
517
+ });
518
+ const typedCall = jest.fn(
519
+ (
520
+ _name: string,
521
+ _resType: string,
522
+ params: { file: { offset: number; data: { byteLength: number } } }
523
+ ) =>
524
+ Promise.resolve({
525
+ type: 'FilesystemFile',
526
+ message: {
527
+ processed_byte: params.file.offset + params.file.data.byteLength,
528
+ },
529
+ })
530
+ );
531
+
532
+ (method as any).device = {
533
+ getCommands: () => ({ typedCall }),
534
+ };
535
+ method.postProgressMessage = jest.fn();
536
+
537
+ await (method as any).protocolV2CommonUpdateProcess({
538
+ payload: new Uint8Array(4097).buffer,
539
+ filePath: 'vol1:firmware.bin',
540
+ processedSize: 0,
541
+ totalSize: 4097,
542
+ transferStartTime: Date.now() - 1000,
543
+ });
544
+
545
+ const writePayloads = typedCall.mock.calls.map(call => call[2]);
546
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4096]);
547
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4096, 1]);
548
+ expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
549
+ expect(writePayloads.every(payload => payload.append === false)).toBe(true);
550
+ expect(method.postProgressMessage).toHaveBeenLastCalledWith(
551
+ 99,
552
+ 'transferData',
553
+ expect.objectContaining({
554
+ transferredBytes: 4097,
555
+ totalBytes: 4097,
556
+ rateBytesPerSecond: expect.any(Number),
557
+ elapsedMs: expect.any(Number),
558
+ })
559
+ );
560
+ });
561
+
562
+ test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
563
+ const method = new FirmwareUpdateV4({
564
+ id: 1,
565
+ payload: {
566
+ method: 'firmwareUpdateV4',
567
+ },
568
+ });
569
+ const typedCall = jest.fn(
570
+ (
571
+ _name: string,
572
+ _resType: string,
573
+ params: { file: { offset: number; data: { byteLength: number } } }
574
+ ) =>
575
+ Promise.resolve({
576
+ type: 'FilesystemFile',
577
+ message: {
578
+ processed_byte: params.file.offset + params.file.data.byteLength,
579
+ },
580
+ })
581
+ );
582
+
583
+ (method as any).params = {
584
+ platform: 'native',
585
+ chunkSize: 4096,
586
+ };
587
+ (method as any).device = {
588
+ getCommands: () => ({ typedCall }),
589
+ };
590
+ method.postProgressMessage = jest.fn();
591
+
592
+ await (method as any).protocolV2CommonUpdateProcess({
593
+ payload: new Uint8Array(1801).buffer,
594
+ filePath: 'vol1:ble-firmware.bin',
595
+ processedSize: 0,
596
+ totalSize: 1801,
597
+ });
598
+
599
+ const writePayloads = typedCall.mock.calls.map(call => call[2]);
600
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
601
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
602
+ });
603
+
604
+ test('consumes Protocol V2 install progress before final update success', async () => {
605
+ const method = new FirmwareUpdateV4({
606
+ id: 1,
607
+ payload: {
608
+ method: 'firmwareUpdateV4',
609
+ },
610
+ });
611
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: { message: 'ok' } });
612
+
613
+ (method as any).device = {
614
+ getCommands: () => ({ typedCall }),
615
+ };
616
+ method.postProgressMessage = jest.fn();
617
+ method.postTipMessage = jest.fn();
618
+
619
+ await (method as any).protocolV2StartFirmwareUpdate({
620
+ targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
621
+ });
622
+
623
+ const callOptions = typedCall.mock.calls[0][3];
624
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
625
+ expect(callOptions.intermediateTypes).toEqual(['DevFirmwareInstallProgress']);
626
+ callOptions.onIntermediateResponse({
627
+ type: 'DevFirmwareInstallProgress',
628
+ message: { target_id: 0, progress: 42 },
629
+ });
630
+
631
+ expect(method.postProgressMessage).toHaveBeenCalledWith(42, 'installingFirmware');
632
+ });
633
+
634
+ test('accepts Protocol V2 firmware update status as start response', async () => {
635
+ const method = new FirmwareUpdateV4({
636
+ id: 1,
637
+ payload: {
638
+ method: 'firmwareUpdateV4',
639
+ },
640
+ });
641
+ const typedCall = jest.fn().mockResolvedValue({
642
+ type: 'DevFirmwareUpdateStatus',
643
+ message: { targets: [{ target_id: 0, status: 1 }] },
644
+ });
645
+
646
+ (method as any).device = {
647
+ getCommands: () => ({ typedCall }),
648
+ };
649
+ method.postProgressMessage = jest.fn();
650
+ method.postTipMessage = jest.fn();
651
+
652
+ await (method as any).protocolV2StartFirmwareUpdate({
653
+ targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
654
+ });
655
+
656
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
657
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
658
+ });
659
+ });
660
+
661
+ describe('Protocol V2 firmware update method', () => {
662
+ test('returns DevFirmwareUpdateStatus from low-level update trigger', async () => {
663
+ const method = new DevFirmwareUpdate({
664
+ id: 1,
665
+ payload: {
666
+ method: 'devFirmwareUpdate',
667
+ path: 'vol0:firmware.bin',
668
+ },
669
+ });
670
+ method.init();
671
+
672
+ const typedCall = jest.fn().mockResolvedValue({
673
+ type: 'DevFirmwareUpdateStatus',
674
+ message: { targets: [{ target_id: 0, status: 1 }] },
675
+ });
676
+
677
+ (method as any).device = {
678
+ commands: { typedCall },
679
+ };
680
+
681
+ await expect(method.run()).resolves.toEqual({
682
+ targets: [{ target_id: 0, status: 1 }],
683
+ });
684
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
685
+ });
686
+ });
687
+
688
+ describe('Protocol V2 onboarding status method', () => {
689
+ test('returns DevOnboardingStatus from low-level status query', async () => {
690
+ const method = new DevGetOnboardingStatus({
691
+ id: 1,
692
+ payload: {
693
+ method: 'devGetOnboardingStatus',
694
+ },
695
+ });
696
+ method.init();
697
+
698
+ const typedCall = jest.fn().mockResolvedValue({
699
+ type: 'DevOnboardingStatus',
700
+ message: {
701
+ page_index: 2,
702
+ page_count: 5,
703
+ page_name: 'backup',
704
+ },
705
+ });
706
+
707
+ (method as any).device = {
708
+ commands: { typedCall },
709
+ };
710
+
711
+ await expect(method.run()).resolves.toEqual({
712
+ page_index: 2,
713
+ page_count: 5,
714
+ page_name: 'backup',
715
+ });
716
+ expect(typedCall).toHaveBeenCalledWith('DevGetOnboardingStatus', 'DevOnboardingStatus', {});
717
+ });
718
+ });
719
+
720
+ describe('Protocol V2 file write method', () => {
721
+ test('uses demo-aligned overwrite and append defaults', async () => {
722
+ const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 1 } });
723
+ const method = new FileWrite({
724
+ id: 1,
725
+ payload: {
726
+ method: 'fileWrite',
727
+ path: 'vol1:test.bin',
728
+ offset: 1,
729
+ totalSize: 2,
730
+ data: new Uint8Array([1]),
731
+ },
732
+ });
733
+ (method as any).device = { commands: { typedCall } };
734
+ method.postMessage = jest.fn();
735
+
736
+ method.init();
737
+ await method.run();
738
+
739
+ expect(typedCall).toHaveBeenCalledWith('FilesystemFileWrite', 'FilesystemFile', {
740
+ file: {
741
+ path: 'vol1:test.bin',
742
+ offset: 1,
743
+ total_size: 2,
744
+ data: new Uint8Array([1]),
745
+ },
746
+ overwrite: false,
747
+ append: false,
748
+ ui_percentage: 99,
749
+ });
750
+ expect(method.postMessage).toHaveBeenCalledWith({
751
+ event: 'UI_EVENT',
752
+ type: UI_REQUEST.DEVICE_PROGRESS,
753
+ payload: expect.objectContaining({
754
+ progress: 100,
755
+ transferredBytes: 1,
756
+ totalBytes: 1,
757
+ elapsedMs: expect.any(Number),
758
+ }),
759
+ });
760
+ });
761
+
762
+ test('splits data larger than the Protocol V2 file payload limit', async () => {
763
+ const data = new Uint8Array(4097);
764
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
765
+ const method = new FileWrite({
766
+ id: 1,
767
+ payload: {
768
+ method: 'fileWrite',
769
+ path: 'vol1:test.bin',
770
+ offset: 0,
771
+ totalSize: 4097,
772
+ data,
773
+ },
774
+ });
775
+ (method as any).device = { commands: { typedCall } };
776
+ method.postMessage = jest.fn();
777
+
778
+ method.init();
779
+ const result = await method.run();
780
+
781
+ expect(typedCall).toHaveBeenCalledTimes(2);
782
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemFileWrite', 'FilesystemFile', {
783
+ file: {
784
+ path: 'vol1:test.bin',
785
+ offset: 0,
786
+ total_size: 4097,
787
+ data: data.slice(0, 4096),
788
+ },
789
+ overwrite: true,
790
+ append: false,
791
+ ui_percentage: 99,
792
+ });
793
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileWrite', 'FilesystemFile', {
794
+ file: {
795
+ path: 'vol1:test.bin',
796
+ offset: 4096,
797
+ total_size: 4097,
798
+ data: data.slice(4096),
799
+ },
800
+ overwrite: false,
801
+ append: false,
802
+ ui_percentage: 99,
803
+ });
804
+ expect(result).toMatchObject({
805
+ path: 'vol1:test.bin',
806
+ processed_byte: 4097,
807
+ chunks: 2,
808
+ });
809
+ expect(method.postMessage).toHaveBeenNthCalledWith(1, {
810
+ event: 'UI_EVENT',
811
+ type: UI_REQUEST.DEVICE_PROGRESS,
812
+ payload: expect.objectContaining({
813
+ progress: 99,
814
+ transferredBytes: 4096,
815
+ totalBytes: 4097,
816
+ elapsedMs: expect.any(Number),
817
+ }),
818
+ });
819
+ expect(method.postMessage).toHaveBeenNthCalledWith(2, {
820
+ event: 'UI_EVENT',
821
+ type: UI_REQUEST.DEVICE_PROGRESS,
822
+ payload: expect.objectContaining({
823
+ progress: 100,
824
+ transferredBytes: 4097,
825
+ totalBytes: 4097,
826
+ elapsedMs: expect.any(Number),
827
+ }),
828
+ });
829
+ });
830
+
831
+ test('uses the BLE chunk limit by default in BLE environments', async () => {
832
+ const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
833
+ const data = new Uint8Array(1801);
834
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
835
+ const method = new FileWrite({
836
+ id: 1,
837
+ payload: {
838
+ method: 'fileWrite',
839
+ path: 'vol1:test.bin',
840
+ offset: 0,
841
+ totalSize: 1801,
842
+ data,
843
+ },
844
+ });
845
+ (method as any).device = { commands: { typedCall } };
846
+ method.postMessage = jest.fn();
847
+
848
+ try {
849
+ method.init();
850
+ await method.run();
851
+ } finally {
852
+ getSettingsSpy.mockRestore();
853
+ }
854
+
855
+ expect(typedCall).toHaveBeenCalledTimes(2);
856
+ expect(typedCall.mock.calls[0][2].file.data.byteLength).toBe(1800);
857
+ expect(typedCall.mock.calls[1][2].file.offset).toBe(1800);
858
+ expect(typedCall.mock.calls[1][2].file.data.byteLength).toBe(1);
859
+ });
860
+ });
861
+
862
+ describe('Protocol V2 file read method', () => {
863
+ test('reads full file in chunks when read length is 0', async () => {
864
+ const firstChunk = new Uint8Array(64).fill(1);
865
+ const typedCall = jest
866
+ .fn()
867
+ .mockResolvedValueOnce({ message: { exist: true, size: 65, directory: false } })
868
+ .mockResolvedValueOnce({ message: { data: firstChunk } })
869
+ .mockResolvedValueOnce({ message: { data: new Uint8Array([2]) } });
870
+ const method = new FileRead({
871
+ id: 1,
872
+ payload: {
873
+ method: 'fileRead',
874
+ path: 'vol1:test.bin',
875
+ offset: 0,
876
+ totalSize: 0,
877
+ chunkLen: 64,
878
+ },
879
+ });
880
+ (method as any).device = { commands: { typedCall } };
881
+
882
+ method.init();
883
+ const result = await method.run();
884
+
885
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemPathInfoQuery', 'FilesystemPathInfo', {
886
+ path: 'vol1:test.bin',
887
+ });
888
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileRead', 'FilesystemFile', {
889
+ file: {
890
+ path: 'vol1:test.bin',
891
+ offset: 0,
892
+ total_size: 0,
893
+ },
894
+ chunk_len: 64,
895
+ ui_percentage: 99,
896
+ });
897
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', {
898
+ file: {
899
+ path: 'vol1:test.bin',
900
+ offset: 64,
901
+ total_size: 0,
902
+ },
903
+ chunk_len: 1,
904
+ ui_percentage: 99,
905
+ });
906
+ expect(result.data.byteLength).toBe(65);
907
+ expect(result.data[0]).toBe(1);
908
+ expect(result.data[64]).toBe(2);
909
+ expect(result).toMatchObject({
910
+ path: 'vol1:test.bin',
911
+ offset: 0,
912
+ total_size: 65,
913
+ chunks: 2,
914
+ });
915
+ });
916
+
917
+ test('decodes protobuf bytes hex string returned by transport', async () => {
918
+ const typedCall = jest.fn().mockResolvedValue({
919
+ message: {
920
+ data: '0102ff',
921
+ },
922
+ });
923
+ const method = new FileRead({
924
+ id: 1,
925
+ payload: {
926
+ method: 'fileRead',
927
+ path: 'vol0:test.bin',
928
+ offset: 0,
929
+ totalSize: 3,
930
+ chunkLen: 512,
931
+ },
932
+ });
933
+ (method as any).device = { commands: { typedCall } };
934
+
935
+ method.init();
936
+ const result = await method.run();
937
+
938
+ expect(result.data).toEqual(new Uint8Array([1, 2, 255]));
939
+ });
940
+ });