@onekeyfe/hd-core 1.2.0-alpha.11 → 1.2.0-alpha.13

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 (59) hide show
  1. package/__tests__/protocol-v2-bootloader-mode.test.ts +37 -0
  2. package/__tests__/protocol-v2.test.ts +240 -82
  3. package/__tests__/protocolV2FileWrite.test.ts +67 -0
  4. package/dist/api/FileWrite.d.ts.map +1 -1
  5. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  6. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  7. package/dist/api/helpers/protocolV2FileWrite.d.ts +32 -0
  8. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -0
  9. package/dist/api/index.d.ts +1 -1
  10. package/dist/api/index.d.ts.map +1 -1
  11. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +6 -0
  12. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +1 -0
  13. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  14. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  15. package/dist/device/Device.d.ts.map +1 -1
  16. package/dist/deviceProfile/buildDeviceFeatures.d.ts +2 -2
  17. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  18. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  19. package/dist/index.d.ts +9 -11
  20. package/dist/index.js +736 -391
  21. package/dist/protocols/protocol-v2/features.d.ts +1 -0
  22. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  23. package/dist/protocols/protocol-v2/index.d.ts +1 -1
  24. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  25. package/dist/types/api/getDeviceInfo.d.ts +1 -1
  26. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  27. package/dist/types/api/index.d.ts +2 -2
  28. package/dist/types/api/index.d.ts.map +1 -1
  29. package/dist/types/api/protocolV2.d.ts +2 -5
  30. package/dist/types/api/protocolV2.d.ts.map +1 -1
  31. package/dist/types/device.d.ts +3 -2
  32. package/dist/types/device.d.ts.map +1 -1
  33. package/dist/utils/patch.d.ts +1 -1
  34. package/dist/utils/patch.d.ts.map +1 -1
  35. package/package.json +4 -4
  36. package/src/api/FileWrite.ts +18 -186
  37. package/src/api/FirmwareUpdateV4.ts +46 -6
  38. package/src/api/cardano/CardanoSignMessage.ts +1 -1
  39. package/src/api/cardano/CardanoSignTransaction.ts +1 -1
  40. package/src/api/helpers/protocolV2FileWrite.ts +168 -0
  41. package/src/api/index.ts +1 -1
  42. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +18 -0
  43. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +22 -52
  44. package/src/api/protocol-v2/helpers.ts +0 -3
  45. package/src/data/messages/messages-protocol-v2.json +422 -16
  46. package/src/data/messages/messages.json +0 -8
  47. package/src/device/Device.ts +7 -17
  48. package/src/deviceProfile/buildDeviceFeatures.ts +28 -12
  49. package/src/deviceProfile/buildDeviceProfile.ts +8 -6
  50. package/src/inject.ts +2 -2
  51. package/src/protocols/protocol-v2/features.ts +19 -1
  52. package/src/protocols/protocol-v2/index.ts +2 -0
  53. package/src/types/api/getDeviceInfo.ts +1 -1
  54. package/src/types/api/index.ts +2 -2
  55. package/src/types/api/protocolV2.ts +6 -6
  56. package/src/types/device.ts +6 -1
  57. package/dist/api/protocol-v2/FilesystemDiskControl.d.ts +0 -13
  58. package/dist/api/protocol-v2/FilesystemDiskControl.d.ts.map +0 -1
  59. package/src/api/protocol-v2/FilesystemDiskControl.ts +0 -50
@@ -0,0 +1,37 @@
1
+ import { Device } from '../src/device/Device';
2
+ import { UI_REQUEST } from '../src/events/ui-request';
3
+
4
+ jest.mock('../src/data/config', () => ({
5
+ getSDKVersion: jest.fn(() => '1.0.0'),
6
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
7
+ }));
8
+
9
+ const createBootloaderDevice = (protocolType: 'V1' | 'V2') => {
10
+ const device = Device.fromDescriptor({
11
+ id: `${protocolType.toLowerCase()}-bootloader`,
12
+ path: `${protocolType.toLowerCase()}-bootloader`,
13
+ protocolType,
14
+ } as any);
15
+
16
+ (device as any).features = {
17
+ bootloaderMode: true,
18
+ initialized: true,
19
+ noBackup: false,
20
+ };
21
+
22
+ return device;
23
+ };
24
+
25
+ describe('Pro2 bootloader mode', () => {
26
+ test('does not block Protocol V2 methods in bootloader mode', () => {
27
+ const device = createBootloaderDevice('V2');
28
+
29
+ expect(device.hasUnexpectedMode([], [])).toBeNull();
30
+ });
31
+
32
+ test('keeps the Protocol V1 bootloader restriction', () => {
33
+ const device = createBootloaderDevice('V1');
34
+
35
+ expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
36
+ });
37
+ });
@@ -12,6 +12,7 @@ import DeviceFactoryInfoGet from '../src/api/protocol-v2/DeviceFactoryInfoGet';
12
12
  import DeviceFactoryInfoSet from '../src/api/protocol-v2/DeviceFactoryInfoSet';
13
13
  import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
14
14
  import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus';
15
+ import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus';
15
16
  import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
16
17
  import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
17
18
  import DeviceSettingsGet from '../src/api/protocol-v2/DeviceSettingsGet';
@@ -26,7 +27,7 @@ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
26
27
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
27
28
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
28
29
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
29
- import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4';
30
+ import FirmwareUpdateV4, { assertProtocolV2ReconnectIdentity } from '../src/api/FirmwareUpdateV4';
30
31
  import GetDeviceInfo from '../src/api/GetDeviceInfo';
31
32
  import GetPassphraseState from '../src/api/GetPassphraseState';
32
33
  import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures';
@@ -62,7 +63,11 @@ import {
62
63
  refreshProtocolV2DeviceStatus,
63
64
  } from '../src/protocols/protocol-v2/walletSession';
64
65
  import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry';
65
- import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
66
+ import {
67
+ buildProfileFromProtocolV2,
68
+ buildProtocolV1FeaturesPayload,
69
+ buildProtocolV2FeaturesPayload,
70
+ } from '../src/deviceProfile';
66
71
  import {
67
72
  getDeviceType,
68
73
  getFirmwareType,
@@ -92,9 +97,12 @@ describe('DeviceUploadWallpaper', () => {
92
97
  const typedCall = jest.fn().mockImplementation((request, _response, params) => {
93
98
  if (request === 'FilesystemDirMake') return { message: { message: 'directory ready' } };
94
99
  if (request === 'FilesystemFileWrite') {
95
- return { message: { processed_byte: params.file.offset + params.file.data.byteLength } };
100
+ const file = params.file as { data: Uint8Array; offset: number };
101
+ return { message: { processed_byte: file.offset + file.data.byteLength } };
102
+ }
103
+ if (request === 'DeviceSettingsSet') {
104
+ return { message: { message: 'wallpaper applied' } };
96
105
  }
97
- if (request === 'SetWallpaper') return { message: { message: 'wallpaper applied' } };
98
106
  throw new Error(`Unexpected request: ${request}`);
99
107
  });
100
108
  const method = new DeviceUploadWallpaper({
@@ -102,32 +110,66 @@ describe('DeviceUploadWallpaper', () => {
102
110
  payload: { method: 'deviceUploadWallpaper', width: 604, height: 1024, rgba },
103
111
  });
104
112
  (method as any).device = stubDevice({ commands: { typedCall } });
113
+ method.postMessage = jest.fn();
105
114
 
106
115
  method.init();
107
116
  const result = await method.run();
108
117
 
109
118
  expect(method.requireProtocolV2).toBe(true);
110
119
  expect(method.unlockPolicy).toBe('retry-on-locked');
111
- expect(typedCall).toHaveBeenNthCalledWith(
112
- 1,
113
- 'FilesystemDirMake',
114
- 'Success',
115
- { path: 'vol0:/wallpapers/user' }
116
- );
120
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
121
+ path: 'vol0:/wallpapers/user',
122
+ });
117
123
  const fileWriteCall = typedCall.mock.calls.find(call => call[0] === 'FilesystemFileWrite');
118
124
  expect(fileWriteCall?.[2]).toMatchObject({
119
125
  file: { path: expect.stringMatching(/^vol0:\/wallpapers\/user\/wallpaper-[a-f0-9]+\.bin$/) },
120
126
  overwrite: true,
121
127
  append: false,
122
128
  });
123
- expect(typedCall).toHaveBeenLastCalledWith('SetWallpaper', 'Success', {
124
- target: 1,
125
- path: result.path,
129
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', {
130
+ settings: { wallpaper_path: result.path },
126
131
  });
132
+ expect(typedCall.mock.calls.some(call => call[0] === 'SetWallpaper')).toBe(false);
127
133
  expect(result).toMatchObject({ colorFormat: 'RGB565', message: 'wallpaper applied' });
134
+ const fileWriteCallCount = typedCall.mock.calls.filter(
135
+ call => call[0] === 'FilesystemFileWrite'
136
+ ).length;
137
+ expect(method.postMessage).toHaveBeenCalledTimes(fileWriteCallCount);
138
+ expect(method.postMessage).toHaveBeenLastCalledWith({
139
+ event: 'UI_EVENT',
140
+ type: UI_REQUEST.DEVICE_PROGRESS,
141
+ payload: expect.objectContaining({
142
+ progress: 100,
143
+ transferredBytes: result.size,
144
+ totalBytes: result.size,
145
+ elapsedMs: expect.any(Number),
146
+ }),
147
+ });
128
148
  });
129
149
 
130
- test('rejects unsafe filenames before device communication', async () => {
150
+ test('文件上传失败时不修改 wallpaper_path', async () => {
151
+ const typedCall = jest.fn().mockImplementation(request => {
152
+ if (request === 'FilesystemDirMake') return { message: {} };
153
+ if (request === 'FilesystemFileWrite') throw new Error('write failed');
154
+ return { message: {} };
155
+ });
156
+ const method = new DeviceUploadWallpaper({
157
+ id: 1,
158
+ payload: {
159
+ method: 'deviceUploadWallpaper',
160
+ width: 604,
161
+ height: 1024,
162
+ rgba: new Uint8Array(604 * 1024 * 4),
163
+ },
164
+ });
165
+ (method as any).device = stubDevice({ commands: { typedCall } });
166
+ method.init();
167
+
168
+ await expect(method.run()).rejects.toThrow('write failed');
169
+ expect(typedCall.mock.calls.some(call => call[0] === 'DeviceSettingsSet')).toBe(false);
170
+ });
171
+
172
+ test('rejects unsafe filenames before device communication', () => {
131
173
  const method = new DeviceUploadWallpaper({
132
174
  id: 1,
133
175
  payload: {
@@ -208,9 +250,9 @@ describe('UploadPortfolio', () => {
208
250
  test('stops after the acknowledged chunk when the operation is aborted', async () => {
209
251
  const packageBytes = new Uint8Array(4001);
210
252
  const abortController = new AbortController();
211
- const typedCall = jest.fn().mockImplementationOnce(async () => {
253
+ const typedCall = jest.fn().mockImplementationOnce(() => {
212
254
  abortController.abort();
213
- return { message: { processed_byte: 2048 } };
255
+ return Promise.resolve({ message: { processed_byte: 2048 } });
214
256
  });
215
257
  const method = new UploadPortfolio({
216
258
  id: 1,
@@ -297,6 +339,38 @@ async function requestProtocolV2Features({
297
339
  }
298
340
 
299
341
  describe('Protocol V2 feature adapter', () => {
342
+ test('keeps legacy snake_case feature fields for existing SDK consumers', () => {
343
+ const protocolV1 = buildProtocolV1FeaturesPayload({
344
+ device_id: 'v1-device',
345
+ session_id: 'v1-session',
346
+ ble_name: 'Classic BLE',
347
+ passphrase_protection: true,
348
+ unlocked: true,
349
+ } as any);
350
+ const protocolV2 = buildProtocolV2FeaturesPayload({
351
+ hw: { serial_no: 'P2-001' },
352
+ coprocessor: { bt_adv_name: 'Pro 2 BLE' },
353
+ status: {
354
+ device_id: 'v2-device',
355
+ unlocked: true,
356
+ passphrase_enabled: true,
357
+ },
358
+ } as any);
359
+
360
+ expect(protocolV1).toMatchObject({
361
+ device_id: 'v1-device',
362
+ session_id: 'v1-session',
363
+ ble_name: 'Classic BLE',
364
+ passphrase_protection: true,
365
+ });
366
+ expect(protocolV2).toMatchObject({
367
+ device_id: 'v2-device',
368
+ ble_name: 'Pro 2 BLE',
369
+ onekey_device_type: 'PRO2',
370
+ passphrase_protection: true,
371
+ });
372
+ });
373
+
300
374
  test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
301
375
  const features = normalizeProtocolV2Features(descriptor as any, {
302
376
  protocol_version: 1,
@@ -304,10 +378,16 @@ describe('Protocol V2 feature adapter', () => {
304
378
  serial_no: 'PR2SERIAL',
305
379
  },
306
380
  fw: {
307
- application_data: {
381
+ romloader: {
308
382
  version: '0.1.0',
383
+ build_id: 'rom-build',
309
384
  hash: [1, 2, 255],
310
385
  },
386
+ application_data: {
387
+ version: '9.8.7',
388
+ build_id: 'app-data-build',
389
+ hash: [9, 8, 7],
390
+ },
311
391
  bootloader: {
312
392
  version: '0.2.0',
313
393
  build_id: 'boot-build',
@@ -362,6 +442,8 @@ describe('Protocol V2 feature adapter', () => {
362
442
  expect(features.bootloaderVersion).toBe('0.2.0');
363
443
  expect(features.verify?.bootloaderBuildId).toBe('boot-build');
364
444
  expect(features.verify?.bootloaderHash).toBe('0a0b');
445
+ expect(features.boardVersion).toBe('0.1.0');
446
+ expect(features.verify?.boardBuildId).toBe('rom-build');
365
447
  expect(features.verify?.boardHash).toBe('0102ff');
366
448
  expect(features.bleName).toBe('Pro2 BLE');
367
449
  expect(features.bleVersion).toBe('4.5.6');
@@ -401,6 +483,32 @@ describe('Protocol V2 feature adapter', () => {
401
483
  expect(profile.status.bootloaderMode).toBe(true);
402
484
  });
403
485
 
486
+ test('marks current romloader-shaped Protocol V2 DeviceInfo as romloader mode', () => {
487
+ const deviceInfo = {
488
+ protocol_version: 1,
489
+ hw: {
490
+ serial_no: 'PR2ROM',
491
+ },
492
+ fw: {
493
+ romloader: {
494
+ version: '1.0.0',
495
+ },
496
+ bootloader: {
497
+ version: '2.0.0',
498
+ },
499
+ },
500
+ };
501
+ const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
502
+ const profile = buildProfileFromProtocolV2({ deviceInfo });
503
+
504
+ expect(features.mode).toBe('romloader');
505
+ expect(features.bootloaderMode).toBe(false);
506
+ expect(features.boardVersion).toBe('1.0.0');
507
+ expect(profile.status.mode).toBe('romloader');
508
+ expect(profile.status.bootloaderMode).toBe(false);
509
+ expect(profile.versions.board).toBe('1.0.0');
510
+ });
511
+
404
512
  test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => {
405
513
  const features = normalizeProtocolV2Features(descriptor as any);
406
514
  features.firmwareVersion = '1.2.3';
@@ -460,6 +568,32 @@ describe('Protocol V2 feature adapter', () => {
460
568
  expect(updateProtocolV2Features).not.toHaveBeenCalled();
461
569
  });
462
570
 
571
+ test('deviceGetOnboardingStatus returns the real Protocol V2 onboarding stage', async () => {
572
+ const typedCall = jest.fn().mockResolvedValue({
573
+ type: 'DevOnboardingStatus',
574
+ message: { stage: 2, status_code: 3, detail_code: 4 },
575
+ });
576
+ const method = new DeviceGetOnboardingStatus({
577
+ payload: {
578
+ method: 'deviceGetOnboardingStatus',
579
+ connectId: 'connect-id',
580
+ },
581
+ });
582
+ method.init();
583
+ method.device = stubDevice({
584
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
585
+ commands: { typedCall },
586
+ }) as any;
587
+
588
+ await expect(method.run()).resolves.toEqual({
589
+ stage: 2,
590
+ status_code: 3,
591
+ detail_code: 4,
592
+ });
593
+ expect(typedCall).toHaveBeenCalledWith('DevGetOnboardingStatus', 'DevOnboardingStatus', {});
594
+ expect(method.requireProtocolV2).toBe(true);
595
+ });
596
+
463
597
  test('deviceSessionGet sends an empty request and does not mutate wallet cache', async () => {
464
598
  const typedCall = jest.fn().mockResolvedValue({
465
599
  type: 'DeviceSession',
@@ -492,6 +626,7 @@ describe('Protocol V2 feature adapter', () => {
492
626
  const api = createCoreApi(call as any);
493
627
 
494
628
  await api.deviceStatusGet('connect-id', { retryCount: 1 });
629
+ await api.deviceGetOnboardingStatus('connect-id', { retryCount: 1 });
495
630
  await api.deviceSessionGet('connect-id', { retryCount: 1 });
496
631
 
497
632
  expect(call).toHaveBeenNthCalledWith(1, {
@@ -500,6 +635,11 @@ describe('Protocol V2 feature adapter', () => {
500
635
  retryCount: 1,
501
636
  });
502
637
  expect(call).toHaveBeenNthCalledWith(2, {
638
+ method: 'deviceGetOnboardingStatus',
639
+ connectId: 'connect-id',
640
+ retryCount: 1,
641
+ });
642
+ expect(call).toHaveBeenNthCalledWith(3, {
503
643
  method: 'deviceSessionGet',
504
644
  connectId: 'connect-id',
505
645
  retryCount: 1,
@@ -1540,7 +1680,7 @@ describe('Protocol V2 feature adapter', () => {
1540
1680
  }),
1541
1681
  expect.anything()
1542
1682
  );
1543
- expect(message).toEqual({});
1683
+ expect(message).toEqual({ onekey_device_type: 'PRO2' });
1544
1684
  expect(message).not.toHaveProperty('label');
1545
1685
  });
1546
1686
 
@@ -1748,11 +1888,17 @@ describe('Protocol V2 feature adapter', () => {
1748
1888
  const typedCall = jest.fn().mockImplementation(requestType => {
1749
1889
  if (requestType === 'DeviceSessionAskPin') {
1750
1890
  return {
1751
- type: 'DeviceSessionPinResult',
1891
+ type: 'Success',
1892
+ message: { message: 'ok' },
1893
+ };
1894
+ }
1895
+ if (requestType === 'DeviceStatusGet') {
1896
+ return {
1897
+ type: 'DeviceStatus',
1752
1898
  message: {
1753
1899
  unlocked: true,
1754
- unlocked_attach_pin: true,
1755
- passphrase_protection: true,
1900
+ unlocked_by_attach_to_pin: true,
1901
+ passphrase_enabled: true,
1756
1902
  },
1757
1903
  };
1758
1904
  }
@@ -1773,7 +1919,10 @@ describe('Protocol V2 feature adapter', () => {
1773
1919
 
1774
1920
  const features = await device.unlockDevice();
1775
1921
 
1776
- expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
1922
+ expect(typedCall.mock.calls).toEqual([
1923
+ ['DeviceSessionAskPin', 'Success'],
1924
+ ['DeviceStatusGet', 'DeviceStatus', {}],
1925
+ ]);
1777
1926
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
1778
1927
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1779
1928
  expect(features).toMatchObject({
@@ -1790,7 +1939,7 @@ describe('Protocol V2 feature adapter', () => {
1790
1939
  });
1791
1940
  });
1792
1941
 
1793
- test('syncs Protocol V2 features passphrase state from DeviceSessionPinResult after unlock', async () => {
1942
+ test('syncs Protocol V2 features passphrase state from DeviceStatus after unlock', async () => {
1794
1943
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1795
1944
  (device as any).features = normalizeProtocolV2Features(
1796
1945
  { ...descriptor, protocolType: 'V2' } as any,
@@ -1803,11 +1952,17 @@ describe('Protocol V2 feature adapter', () => {
1803
1952
  const typedCall = jest.fn().mockImplementation(requestType => {
1804
1953
  if (requestType === 'DeviceSessionAskPin') {
1805
1954
  return {
1806
- type: 'DeviceSessionPinResult',
1955
+ type: 'Success',
1956
+ message: { message: 'ok' },
1957
+ };
1958
+ }
1959
+ if (requestType === 'DeviceStatusGet') {
1960
+ return {
1961
+ type: 'DeviceStatus',
1807
1962
  message: {
1808
1963
  unlocked: true,
1809
- unlocked_attach_pin: true,
1810
- passphrase_protection: true,
1964
+ unlocked_by_attach_to_pin: true,
1965
+ passphrase_enabled: true,
1811
1966
  },
1812
1967
  };
1813
1968
  }
@@ -1817,7 +1972,10 @@ describe('Protocol V2 feature adapter', () => {
1817
1972
 
1818
1973
  await device.unlockDevice();
1819
1974
 
1820
- expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
1975
+ expect(typedCall.mock.calls).toEqual([
1976
+ ['DeviceSessionAskPin', 'Success'],
1977
+ ['DeviceStatusGet', 'DeviceStatus', {}],
1978
+ ]);
1821
1979
  expect((device as any).profile).toBeUndefined();
1822
1980
  expect(device.features?.unlocked).toBe(true);
1823
1981
  expect(device.features?.passphraseProtection).toBe(true);
@@ -1844,7 +2002,7 @@ describe('Protocol V2 feature adapter', () => {
1844
2002
  })
1845
2003
  );
1846
2004
  expect(typedCall).toHaveBeenCalledTimes(1);
1847
- expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
2005
+ expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'Success']]);
1848
2006
  expect(typedCall).not.toHaveBeenCalledWith(
1849
2007
  'DeviceSessionGet',
1850
2008
  'DeviceSession',
@@ -3465,7 +3623,7 @@ describe('Protocol V2 firmware update targets', () => {
3465
3623
  expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
3466
3624
  });
3467
3625
 
3468
- test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
3626
+ test('rejects a chunk-relative processed_byte during firmware staging', async () => {
3469
3627
  const method = new FirmwareUpdateV4({
3470
3628
  id: 1,
3471
3629
  payload: {
@@ -3481,23 +3639,6 @@ describe('Protocol V2 firmware update targets', () => {
3481
3639
  },
3482
3640
  });
3483
3641
  }
3484
- if (name === 'FilesystemPathInfoQuery') {
3485
- return Promise.resolve({
3486
- type: 'FilesystemPathInfo',
3487
- message: { exist: true, directory: false, size: 4097 },
3488
- });
3489
- }
3490
- if (name === 'DeviceFirmwareUpdateRequest') {
3491
- return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
3492
- }
3493
- if (name === 'DeviceFirmwareUpdateStatusGet') {
3494
- return Promise.resolve({
3495
- type: 'DeviceFirmwareUpdateStatus',
3496
- message: {
3497
- records: [{ target_id: 4, status: 2 }],
3498
- },
3499
- });
3500
- }
3501
3642
  return Promise.reject(new Error(`unexpected call ${name}`));
3502
3643
  });
3503
3644
 
@@ -3507,27 +3648,15 @@ describe('Protocol V2 firmware update targets', () => {
3507
3648
  method.postProgressMessage = jest.fn();
3508
3649
  method.postTipMessage = jest.fn();
3509
3650
 
3510
- await (method as any).executeProtocolV2Update({
3511
- bootloaderBinary: null,
3512
- fwBinaryMap: [
3513
- {
3514
- fileName: 'firmware.bin',
3515
- binary: new Uint8Array(4097).buffer,
3516
- targetId: 4,
3517
- },
3518
- ],
3519
- });
3520
-
3521
- expect(typedCall).toHaveBeenCalledWith(
3522
- 'DeviceFirmwareUpdateRequest',
3523
- ['Success', 'DeviceFirmwareUpdateStatus'],
3524
- {
3525
- targets: [{ target_id: 4, path: 'vol0:/firmware.bin' }],
3526
- },
3527
- expect.objectContaining({
3528
- timeoutMs: 3 * 60 * 1000,
3651
+ await expect(
3652
+ (method as any).protocolV2WriteWholeFile({
3653
+ payload: new Uint8Array(4097).buffer,
3654
+ filePath: 'vol0:/firmware.bin',
3655
+ processedSize: 0,
3656
+ totalSize: 4097,
3529
3657
  })
3530
- );
3658
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
3659
+ expect(typedCall).toHaveBeenCalledTimes(2);
3531
3660
  });
3532
3661
 
3533
3662
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -3689,6 +3818,26 @@ describe('Protocol V2 firmware update method', () => {
3689
3818
  });
3690
3819
  });
3691
3820
 
3821
+ test('does not report a generic USB transfer failure as a started firmware update', async () => {
3822
+ const method = new DeviceFirmwareUpdate({
3823
+ id: 1,
3824
+ payload: {
3825
+ method: 'deviceFirmwareUpdate',
3826
+ targetId: 4,
3827
+ path: 'vol0:firmware.bin',
3828
+ },
3829
+ });
3830
+ method.init();
3831
+ const error = new Error(
3832
+ "Failed to execute 'transferOut' on 'USBDevice': A transfer error has occurred"
3833
+ );
3834
+ (method as any).device = stubDevice({
3835
+ commands: { typedCall: jest.fn().mockRejectedValue(error) },
3836
+ });
3837
+
3838
+ await expect(method.run()).rejects.toBe(error);
3839
+ });
3840
+
3692
3841
  test('rejects missing or invalid firmware targets before transport call', async () => {
3693
3842
  const typedCall = jest.fn();
3694
3843
  const method = new DeviceFirmwareUpdate({
@@ -3820,6 +3969,20 @@ describe('Protocol V2 firmware update method', () => {
3820
3969
  });
3821
3970
  });
3822
3971
 
3972
+ describe('Protocol V2 firmware reconnect identity', () => {
3973
+ test('rejects a different device before firmware transfer resumes', () => {
3974
+ expect(() => assertProtocolV2ReconnectIdentity('expected-device', 'other-device')).toThrow(
3975
+ 'identity mismatch'
3976
+ );
3977
+ expect(() => assertProtocolV2ReconnectIdentity('expected-device', undefined)).toThrow(
3978
+ 'identity unavailable'
3979
+ );
3980
+ expect(() =>
3981
+ assertProtocolV2ReconnectIdentity('expected-device', 'expected-device')
3982
+ ).not.toThrow();
3983
+ });
3984
+ });
3985
+
3823
3986
  describe('Protocol V2 reboot methods', () => {
3824
3987
  test('sends DeviceReboot from deviceReboot', async () => {
3825
3988
  const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
@@ -3853,19 +4016,20 @@ describe('Protocol V2 protected method execution', () => {
3853
4016
  unlockPolicy: 'retry-on-locked',
3854
4017
  run: jest
3855
4018
  .fn()
3856
- .mockImplementationOnce(async () => {
4019
+ .mockImplementationOnce(() => {
3857
4020
  calls.push('run-1');
3858
- throw deviceLockedError();
4021
+ return Promise.reject(deviceLockedError());
3859
4022
  })
3860
- .mockImplementationOnce(async () => {
4023
+ .mockImplementationOnce(() => {
3861
4024
  calls.push('run-2');
3862
- return { message: 'ok' };
4025
+ return Promise.resolve({ message: 'ok' });
3863
4026
  }),
3864
4027
  };
3865
4028
  const device = {
3866
4029
  isProtocolV2: () => true,
3867
- unlockDevice: jest.fn(async () => {
4030
+ unlockDevice: jest.fn(() => {
3868
4031
  calls.push('unlock');
4032
+ return Promise.resolve();
3869
4033
  }),
3870
4034
  };
3871
4035
 
@@ -3883,9 +4047,7 @@ describe('Protocol V2 protected method execution', () => {
3883
4047
  const error = deviceLockedError();
3884
4048
  const method = {
3885
4049
  unlockPolicy,
3886
- run: jest.fn(async () => {
3887
- throw error;
3888
- }),
4050
+ run: jest.fn().mockRejectedValue(error),
3889
4051
  };
3890
4052
  const device = {
3891
4053
  isProtocolV2: () => isProtocolV2,
@@ -3903,15 +4065,11 @@ describe('Protocol V2 protected method execution', () => {
3903
4065
  const unlockError = new Error('PIN cancelled');
3904
4066
  const unlockFailMethod = {
3905
4067
  unlockPolicy: 'retry-on-locked',
3906
- run: jest.fn(async () => {
3907
- throw initialError;
3908
- }),
4068
+ run: jest.fn().mockRejectedValue(initialError),
3909
4069
  };
3910
4070
  const unlockFailDevice = {
3911
4071
  isProtocolV2: () => true,
3912
- unlockDevice: jest.fn(async () => {
3913
- throw unlockError;
3914
- }),
4072
+ unlockDevice: jest.fn().mockRejectedValue(unlockError),
3915
4073
  };
3916
4074
 
3917
4075
  await expect(
@@ -3926,7 +4084,7 @@ describe('Protocol V2 protected method execution', () => {
3926
4084
  };
3927
4085
  const retryFailDevice = {
3928
4086
  isProtocolV2: () => true,
3929
- unlockDevice: jest.fn(async () => undefined),
4087
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
3930
4088
  };
3931
4089
 
3932
4090
  await expect(
@@ -4244,7 +4402,7 @@ describe('Protocol V2 file write method', () => {
4244
4402
  });
4245
4403
 
4246
4404
  test('uses demo-aligned overwrite and append defaults', async () => {
4247
- const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 1 } });
4405
+ const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 2 } });
4248
4406
  const method = new FileWrite({
4249
4407
  id: 1,
4250
4408
  payload: {
@@ -0,0 +1,67 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import { writeProtocolV2File } from '../src/api/helpers/protocolV2FileWrite';
4
+
5
+ jest.mock('../src/data/config', () => ({
6
+ getSDKVersion: jest.fn(() => '1.0.0'),
7
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
8
+ }));
9
+
10
+ describe('writeProtocolV2File', () => {
11
+ test('按分片写入并只在首片设置 overwrite', async () => {
12
+ const data = new Uint8Array(4097);
13
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
14
+ const onProgress = jest.fn();
15
+
16
+ const result = await writeProtocolV2File({
17
+ commands: { typedCall } as any,
18
+ path: 'vol0:/wallpapers/user/test.bin',
19
+ data,
20
+ totalSize: data.byteLength,
21
+ overwrite: true,
22
+ onProgress,
23
+ });
24
+
25
+ expect(typedCall).toHaveBeenCalledTimes(2);
26
+ expect(typedCall.mock.calls[0][2]).toMatchObject({
27
+ file: { offset: 0, total_size: 4097, data: data.slice(0, 4000) },
28
+ overwrite: true,
29
+ append: false,
30
+ ui_percentage: 0,
31
+ });
32
+ expect(typedCall.mock.calls[1][2]).toMatchObject({
33
+ file: { offset: 4000, total_size: 4097, data: data.slice(4000) },
34
+ overwrite: false,
35
+ append: false,
36
+ ui_percentage: 100,
37
+ });
38
+ expect(result).toMatchObject({ processed_byte: 4097, chunks: 2 });
39
+ expect(onProgress).toHaveBeenLastCalledWith(
40
+ expect.objectContaining({ progress: 100, transferredBytes: 4097, totalBytes: 4097 })
41
+ );
42
+ });
43
+
44
+ test('拒绝越过文件末尾的 processed_byte', async () => {
45
+ const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 10 } });
46
+
47
+ await expect(
48
+ writeProtocolV2File({
49
+ commands: { typedCall } as any,
50
+ path: 'vol0:/wallpapers/user/test.bin',
51
+ data: new Uint8Array([1]),
52
+ })
53
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.RuntimeError });
54
+ });
55
+
56
+ test('设备返回未前进的绝对 processed_byte 时立即失败', async () => {
57
+ const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 0 } });
58
+
59
+ await expect(
60
+ writeProtocolV2File({
61
+ commands: { typedCall } as any,
62
+ path: 'vol0:/wallpapers/user/test.bin',
63
+ data: new Uint8Array([1, 2, 3]),
64
+ })
65
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.RuntimeError });
66
+ });
67
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"FileWrite.d.ts","sourceRoot":"","sources":["../../src/api/FileWrite.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAW1C,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,GAAG,MAAM,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B,CAAC;AA2EF,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,UAAU,CAAC,eAAe,CAAC;IAChE,IAAI;IAsBE,GAAG;;;;;;;CA8GV"}
1
+ {"version":3,"file":"FileWrite.d.ts","sourceRoot":"","sources":["../../src/api/FileWrite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAW1C,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,GAAG,MAAM,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,UAAU,CAAC,eAAe,CAAC;IAChE,IAAI;IAsBE,GAAG;;;;;;;CAqBV"}
@@ -1,6 +1,8 @@
1
1
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
2
2
  import type { FirmwareUpdateV4Params } from '../types/api/firmwareUpdate';
3
+ export declare const assertProtocolV2ReconnectIdentity: (expectedDeviceId?: string, actualDeviceId?: string) => void;
3
4
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
5
+ private protocolV2ExpectedDeviceId?;
4
6
  init(): void;
5
7
  private getProtocolV2FirmwareChunkSize;
6
8
  run(): Promise<{