@onekeyfe/hd-core 1.2.0-alpha.75 → 1.2.0-alpha.76

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 (111) hide show
  1. package/__tests__/connectSettings.test.ts +45 -5
  2. package/__tests__/device-initialize-timeout.test.ts +56 -0
  3. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  5. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  6. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  7. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  8. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  11. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  12. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  14. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +64 -0
  15. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  16. package/__tests__/protocol-v2.test.ts +405 -100
  17. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  20. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  22. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV4.d.ts +25 -5
  24. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  25. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  26. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  28. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  31. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  32. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  36. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  37. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  38. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  39. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  40. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  41. package/dist/api/firmware/getBinary.d.ts +7 -0
  42. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  43. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  44. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  45. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  46. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/data-manager/DataManager.d.ts +1 -0
  49. package/dist/data-manager/DataManager.d.ts.map +1 -1
  50. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  51. package/dist/device/Device.d.ts +1 -0
  52. package/dist/device/Device.d.ts.map +1 -1
  53. package/dist/index.d.ts +179 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3079 -1062
  56. package/dist/inject.d.ts.map +1 -1
  57. package/dist/lowLevelInject.d.ts.map +1 -1
  58. package/dist/topLevelInject.d.ts.map +1 -1
  59. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  60. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  61. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  62. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  63. package/dist/types/api/export.d.ts +4 -1
  64. package/dist/types/api/export.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  66. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  68. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  71. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  72. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  73. package/dist/types/api/index.d.ts +9 -1
  74. package/dist/types/api/index.d.ts.map +1 -1
  75. package/dist/types/settings.d.ts +13 -0
  76. package/dist/types/settings.d.ts.map +1 -1
  77. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  78. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  79. package/package.json +4 -4
  80. package/src/api/CheckAllFirmwareRelease.ts +57 -6
  81. package/src/api/FirmwareUpdate.ts +13 -1
  82. package/src/api/FirmwareUpdateV2.ts +296 -121
  83. package/src/api/FirmwareUpdateV3.ts +263 -57
  84. package/src/api/FirmwareUpdateV4.ts +881 -349
  85. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  86. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  87. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  88. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  89. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  90. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  91. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  92. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  93. package/src/api/firmware/updateBootloader.ts +19 -4
  94. package/src/api/firmware/uploadFirmware.ts +140 -22
  95. package/src/data-manager/DataManager.ts +62 -54
  96. package/src/data-manager/connectSettings.ts +11 -0
  97. package/src/device/Device.ts +6 -1
  98. package/src/index.ts +5 -0
  99. package/src/inject.ts +22 -2
  100. package/src/lowLevelInject.ts +5 -1
  101. package/src/topLevelInject.ts +5 -1
  102. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  103. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  104. package/src/types/api/export.ts +18 -0
  105. package/src/types/api/firmwareUpdate.ts +75 -0
  106. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  107. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  108. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  109. package/src/types/api/index.ts +14 -0
  110. package/src/types/settings.ts +13 -0
  111. package/src/utils/deviceFeaturesCompat.ts +6 -0
@@ -0,0 +1,231 @@
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
+
3
+ import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
4
+ import {
5
+ assertFirmwareUpdatePreparedPlanBinding,
6
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
7
+ prepareFirmwareUpdatePlan,
8
+ validateFirmwareUpdatePreparedPlan,
9
+ } from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
10
+
11
+ import type { Features } from '../../src/types';
12
+
13
+ const buildPlan = () =>
14
+ buildFirmwareUpdatePlan({
15
+ features: {
16
+ deviceType: EDeviceType.Classic1s,
17
+ serialNo: 'classic1s-device-id',
18
+ firmwareVersion: '1.0.0',
19
+ bootloaderVersion: '1.0.0',
20
+ } as Features,
21
+ firmwareType: EFirmwareType.Universal,
22
+ platform: 'native',
23
+ firmware: {
24
+ status: 'outdated',
25
+ release: {
26
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
27
+ expectedSize: 4,
28
+ fingerprint: 'a'.repeat(64),
29
+ version: [3, 0, 0],
30
+ resource: 'https://firmware.onekey.so/classic/resource.zip',
31
+ resourceExpectedSize: 8,
32
+ resourceFingerprint: 'b'.repeat(64),
33
+ },
34
+ },
35
+ ble: {
36
+ status: 'valid',
37
+ release: undefined,
38
+ },
39
+ bootloader: {
40
+ status: 'valid',
41
+ release: undefined,
42
+ },
43
+ });
44
+
45
+ const artifact = (sha256: string, size: number) => ({
46
+ artifactRef: `fw:${sha256}`,
47
+ size,
48
+ sha256,
49
+ });
50
+
51
+ describe('FirmwareUpdatePreparedPlan', () => {
52
+ test('binds all receipts into a URL-free, network-forbidden plan', () => {
53
+ const plan = buildPlan();
54
+ const prepared = prepareFirmwareUpdatePlan({
55
+ plan,
56
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
57
+ artifacts: [
58
+ {
59
+ artifactId: 'firmware',
60
+ artifact: artifact('a'.repeat(64), 4),
61
+ },
62
+ {
63
+ artifactId: 'resource',
64
+ artifact: artifact('b'.repeat(64), 8),
65
+ materializedEntries: [
66
+ {
67
+ entryName: 'resource/index.bin',
68
+ artifact: artifact('c'.repeat(64), 2),
69
+ },
70
+ ],
71
+ },
72
+ ],
73
+ });
74
+
75
+ expect(prepared.networkPolicy).toBe('forbid');
76
+ expect(prepared.planDigest).toBe(plan.planDigest);
77
+ expect(prepared.preparedPlanDigest).toMatch(/^[a-f0-9]{64}$/u);
78
+ expect(JSON.stringify(prepared)).not.toContain('https://');
79
+ expect(validateFirmwareUpdatePreparedPlan(prepared)).toEqual(prepared);
80
+ expect(
81
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
82
+ preparedPlan: prepared,
83
+ deviceIdentity: plan.deviceIdentity,
84
+ })
85
+ ).toEqual(prepared);
86
+ expect(() =>
87
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
88
+ preparedPlan: prepared,
89
+ deviceIdentity: 'different-device-id',
90
+ })
91
+ ).toThrow();
92
+ expect(
93
+ assertFirmwareUpdatePreparedPlanBinding({
94
+ preparedPlan: prepared,
95
+ executor: 'v2',
96
+ platform: 'native',
97
+ scopeTargets: ['firmware', 'resource'],
98
+ bindings: [
99
+ {
100
+ target: 'firmware',
101
+ artifact: artifact('a'.repeat(64), 4),
102
+ },
103
+ {
104
+ target: 'resource',
105
+ entryName: 'resource/index.bin',
106
+ artifact: artifact('c'.repeat(64), 2),
107
+ },
108
+ ],
109
+ })
110
+ ).toEqual(prepared);
111
+ expect(() =>
112
+ assertFirmwareUpdatePreparedPlanBinding({
113
+ preparedPlan: prepared,
114
+ executor: 'v2',
115
+ platform: 'native',
116
+ scopeTargets: ['firmware', 'resource'],
117
+ bindings: [
118
+ {
119
+ target: 'firmware',
120
+ artifact: artifact('a'.repeat(64), 4),
121
+ },
122
+ {
123
+ target: 'resource',
124
+ artifact: artifact('b'.repeat(64), 8),
125
+ },
126
+ ],
127
+ })
128
+ ).toThrow('artifact binding is invalid');
129
+ expect(() =>
130
+ assertFirmwareUpdatePreparedPlanBinding({
131
+ preparedPlan: prepared,
132
+ executor: 'v2',
133
+ platform: 'native',
134
+ scopeTargets: ['firmware'],
135
+ bindings: [
136
+ {
137
+ target: 'firmware',
138
+ artifact: artifact('b'.repeat(64), 8),
139
+ },
140
+ ],
141
+ })
142
+ ).toThrow('artifact binding is invalid');
143
+ });
144
+
145
+ test('rejects receipt drift and a tampered update plan digest', () => {
146
+ const plan = buildPlan();
147
+ expect(() =>
148
+ prepareFirmwareUpdatePlan({
149
+ plan,
150
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
151
+ artifacts: [
152
+ {
153
+ artifactId: 'firmware',
154
+ artifact: artifact('d'.repeat(64), 4),
155
+ },
156
+ {
157
+ artifactId: 'resource',
158
+ artifact: artifact('b'.repeat(64), 8),
159
+ materializedEntries: [
160
+ {
161
+ entryName: 'resource/index.bin',
162
+ artifact: artifact('c'.repeat(64), 2),
163
+ },
164
+ ],
165
+ },
166
+ ],
167
+ })
168
+ ).toThrow('receipt does not match');
169
+
170
+ expect(() =>
171
+ prepareFirmwareUpdatePlan({
172
+ plan: { ...plan, planDigest: 'f'.repeat(64) },
173
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
174
+ artifacts: [],
175
+ })
176
+ ).toThrow('plan digest is invalid');
177
+ });
178
+
179
+ test('projects portable archive paths to unique device resource names', () => {
180
+ const plan = buildPlan();
181
+ const buildArtifacts = (entryNames: string[]) => [
182
+ {
183
+ artifactId: 'firmware',
184
+ artifact: artifact('a'.repeat(64), 4),
185
+ },
186
+ {
187
+ artifactId: 'resource',
188
+ artifact: artifact('b'.repeat(64), 8),
189
+ materializedEntries: entryNames.map((entryName, index) => ({
190
+ entryName,
191
+ artifact: artifact(String(index + 1).repeat(64), 2),
192
+ })),
193
+ },
194
+ ];
195
+
196
+ expect(() =>
197
+ prepareFirmwareUpdatePlan({
198
+ plan,
199
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
200
+ artifacts: buildArtifacts(['icons/index.bin', 'fonts/index.bin']),
201
+ })
202
+ ).toThrow('duplicate entry names');
203
+ expect(() =>
204
+ prepareFirmwareUpdatePlan({
205
+ plan,
206
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
207
+ artifacts: buildArtifacts(['../index.bin']),
208
+ })
209
+ ).toThrow('entry name is invalid');
210
+ });
211
+
212
+ test('rejects unknown fields even when a caller presents a plan-shaped object', () => {
213
+ const plan = buildPlan();
214
+ expect(() =>
215
+ prepareFirmwareUpdatePlan({
216
+ plan: {
217
+ ...plan,
218
+ artifacts: [
219
+ {
220
+ ...plan.artifacts[0],
221
+ unexpectedUrl: 'https://attacker.invalid/payload',
222
+ },
223
+ plan.artifacts[1],
224
+ ],
225
+ } as typeof plan,
226
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
227
+ artifacts: [],
228
+ })
229
+ ).toThrow('invalid field set');
230
+ });
231
+ });
@@ -1,8 +1,8 @@
1
- import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
4
4
  import { getBinary } from '../../src/api/firmware/getBinary';
5
- import { uploadFirmware } from '../../src/api/firmware/uploadFirmware';
5
+ import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
6
6
  import * as utils from '../../src/utils';
7
7
 
8
8
  jest.mock('../../src/data/config', () => ({
@@ -18,7 +18,8 @@ jest.mock('../../src/api/firmware/getBinary', () => ({
18
18
 
19
19
  jest.mock('../../src/api/firmware/uploadFirmware', () => ({
20
20
  updateResources: jest.fn(),
21
- uploadFirmware: jest.fn(),
21
+ updateResourcesFromSources: jest.fn(),
22
+ uploadFirmwareFromSource: jest.fn(),
22
23
  }));
23
24
 
24
25
  jest.mock('../../src/device/DevicePool', () => ({
@@ -29,7 +30,9 @@ jest.mock('../../src/device/DevicePool', () => ({
29
30
  }));
30
31
 
31
32
  const mockGetBinary = getBinary as jest.MockedFunction<typeof getBinary>;
32
- const mockUploadFirmware = uploadFirmware as jest.MockedFunction<typeof uploadFirmware>;
33
+ const mockUploadFirmwareFromSource = uploadFirmwareFromSource as jest.MockedFunction<
34
+ typeof uploadFirmwareFromSource
35
+ >;
33
36
 
34
37
  type DeviceType = 'CLASSIC1S' | 'PURE';
35
38
 
@@ -175,7 +178,26 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
175
178
  expect(mockGetBinary).toHaveBeenCalledTimes(1);
176
179
  expectNoFirmwareMutationCalls(typedCall);
177
180
  expect(acquire).not.toHaveBeenCalled();
178
- expect(mockUploadFirmware).not.toHaveBeenCalled();
181
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
182
+ });
183
+
184
+ it('preserves structured preparation errors before any firmware mutation', async () => {
185
+ const preparationError = ERRORS.TypedError(
186
+ HardwareErrorCode.RuntimeError,
187
+ 'Firmware must be prepared by the external firmware host',
188
+ {
189
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
190
+ artifactName: 'firmware',
191
+ }
192
+ );
193
+ mockGetBinary.mockRejectedValue(preparationError);
194
+ const { method, typedCall, acquire } = createMethod();
195
+
196
+ await expect(method.run()).rejects.toBe(preparationError);
197
+
198
+ expectNoFirmwareMutationCalls(typedCall);
199
+ expect(acquire).not.toHaveBeenCalled();
200
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
179
201
  });
180
202
 
181
203
  it.each([
@@ -216,9 +238,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
216
238
  binary: firmwareBinary,
217
239
  } as Awaited<ReturnType<typeof getBinary>>);
218
240
  });
219
- mockUploadFirmware.mockImplementation(() => {
241
+ mockUploadFirmwareFromSource.mockImplementation(() => {
220
242
  callOrder.push('upload');
221
- return Promise.resolve(expectedResponse as Awaited<ReturnType<typeof uploadFirmware>>);
243
+ return Promise.resolve(
244
+ expectedResponse as Awaited<ReturnType<typeof uploadFirmwareFromSource>>
245
+ );
222
246
  });
223
247
  const { method, typedCall } = createMethod({
224
248
  updateType,
@@ -248,15 +272,15 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
248
272
  },
249
273
  })
250
274
  );
251
- expect(mockUploadFirmware).toHaveBeenCalledWith(
275
+ expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
252
276
  updateType,
253
277
  expect.any(Function),
254
278
  expect.any(Function),
255
279
  method.device,
256
- {
257
- payload: firmwareBinary,
258
- rebootOnSuccess: true,
259
- },
280
+ expect.objectContaining({
281
+ size: firmwareBinary.byteLength,
282
+ }),
283
+ true,
260
284
  isUpdateBootloader
261
285
  );
262
286
  }
@@ -271,11 +295,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
271
295
  binary: firmwareBinary,
272
296
  } as Awaited<ReturnType<typeof getBinary>>);
273
297
  });
274
- mockUploadFirmware.mockImplementation(() => {
298
+ mockUploadFirmwareFromSource.mockImplementation(() => {
275
299
  callOrder.push('upload');
276
300
  return Promise.resolve({
277
301
  success: true,
278
- } as Awaited<ReturnType<typeof uploadFirmware>>);
302
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
279
303
  });
280
304
  const { method, typedCall, acquire } = createMethod({
281
305
  bootloaderMode: true,
@@ -300,9 +324,9 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
300
324
  resolveDownload = resolve;
301
325
  })
302
326
  );
303
- mockUploadFirmware.mockResolvedValue({
327
+ mockUploadFirmwareFromSource.mockResolvedValue({
304
328
  success: true,
305
- } as Awaited<ReturnType<typeof uploadFirmware>>);
329
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
306
330
  const { method, typedCall } = createMethod();
307
331
 
308
332
  const runPromise = method.run();
@@ -333,7 +357,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
333
357
 
334
358
  expectNoFirmwareMutationCalls(typedCall);
335
359
  expect(acquire).not.toHaveBeenCalled();
336
- expect(mockUploadFirmware).not.toHaveBeenCalled();
360
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
337
361
  });
338
362
 
339
363
  it.each([
@@ -342,23 +366,23 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
342
366
  ])(
343
367
  'keeps the non-empty %s binary overload and never downloads it again',
344
368
  async (_kind, binary) => {
345
- mockUploadFirmware.mockResolvedValue({
369
+ mockUploadFirmwareFromSource.mockResolvedValue({
346
370
  success: true,
347
- } as Awaited<ReturnType<typeof uploadFirmware>>);
371
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
348
372
  const { method } = createMethod({ binary });
349
373
 
350
374
  await method.run();
351
375
 
352
376
  expect(mockGetBinary).not.toHaveBeenCalled();
353
- expect(mockUploadFirmware).toHaveBeenCalledWith(
377
+ expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
354
378
  'firmware',
355
379
  expect.any(Function),
356
380
  expect.any(Function),
357
381
  method.device,
358
- {
359
- payload: binary,
360
- rebootOnSuccess: true,
361
- },
382
+ expect.objectContaining({
383
+ size: binary.byteLength,
384
+ }),
385
+ true,
362
386
  undefined
363
387
  );
364
388
  }
@@ -369,17 +393,21 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
369
393
  ['DataView', new DataView(new Uint8Array([9, 3, 4, 9]).buffer, 1, 2), [3, 4]],
370
394
  ['custom Buffer', createCustomBuffer(4), [1, 2, 3, 4]],
371
395
  ])('normalizes a non-empty %s before rebooting', async (_kind, binary, expectedBytes) => {
372
- mockUploadFirmware.mockResolvedValue({
373
- success: true,
374
- } as Awaited<ReturnType<typeof uploadFirmware>>);
396
+ let uploadedBytes: number[] = [];
397
+ mockUploadFirmwareFromSource.mockImplementation(
398
+ async (_updateType, _typedCall, _postMessage, _device, source) => {
399
+ uploadedBytes = Array.from(new Uint8Array(await source.readAt(0, source.size)));
400
+ return {
401
+ success: true,
402
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>;
403
+ }
404
+ );
375
405
  const { method } = createMethod({ binary });
376
406
 
377
407
  await method.run();
378
408
 
379
409
  expect(mockGetBinary).not.toHaveBeenCalled();
380
- const normalizedBinary = mockUploadFirmware.mock.calls[0][4].payload;
381
- expect(normalizedBinary).toBeInstanceOf(ArrayBuffer);
382
- expect(Array.from(new Uint8Array(normalizedBinary))).toEqual(expectedBytes);
410
+ expect(uploadedBytes).toEqual(expectedBytes);
383
411
  });
384
412
 
385
413
  it.each([
@@ -397,6 +425,6 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
397
425
  expect(mockGetBinary).not.toHaveBeenCalled();
398
426
  expectNoFirmwareMutationCalls(typedCall);
399
427
  expect(acquire).not.toHaveBeenCalled();
400
- expect(mockUploadFirmware).not.toHaveBeenCalled();
428
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
401
429
  });
402
430
  });
@@ -0,0 +1,64 @@
1
+ import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
2
+ import { DevicePool } from '../../src/device/DevicePool';
3
+
4
+ import type { Device } from '../../src/device/Device';
5
+
6
+ jest.mock('../../src/data/config', () => ({
7
+ getSDKVersion: jest.fn(() => '1.0.0'),
8
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
9
+ }));
10
+
11
+ describe('FirmwareUpdateV3 reconnect', () => {
12
+ afterEach(() => {
13
+ jest.restoreAllMocks();
14
+ });
15
+
16
+ it('keeps transient USB acquire failures inside the reconnect loop', async () => {
17
+ const acquire = jest.fn().mockResolvedValue(undefined);
18
+ const commands = {
19
+ disposed: true,
20
+ mainId: '',
21
+ };
22
+ const cachedDevice = {} as Device;
23
+ const updateFromCache = jest.fn();
24
+ jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
25
+ devices: {},
26
+ deviceList: [cachedDevice],
27
+ });
28
+
29
+ const method = new FirmwareUpdateV3({
30
+ id: 1,
31
+ payload: {
32
+ method: 'firmwareUpdateV3',
33
+ connectId: 'usb-path',
34
+ },
35
+ });
36
+ method.isBleReconnect = jest.fn(() => false);
37
+ method.device = {
38
+ originalDescriptor: {
39
+ id: 'usb-path',
40
+ path: 'usb-path',
41
+ protocolType: 'V1',
42
+ },
43
+ deviceConnector: {
44
+ enumerate: jest.fn().mockResolvedValue({
45
+ descriptors: [{ path: 'usb-path' }],
46
+ }),
47
+ },
48
+ updateFromCache,
49
+ acquire,
50
+ commands,
51
+ getCommands: () => commands,
52
+ mainId: 'usb-session',
53
+ } as unknown as Device;
54
+
55
+ await method.waitForDeviceReconnect(1000);
56
+
57
+ expect(updateFromCache).toHaveBeenCalledWith(cachedDevice);
58
+ expect(acquire).toHaveBeenCalledWith(undefined, {
59
+ throwOnRunPromiseError: true,
60
+ });
61
+ expect(commands.disposed).toBe(false);
62
+ expect(commands.mainId).toBe('usb-session');
63
+ });
64
+ });
@@ -0,0 +1,70 @@
1
+ import { EDeviceType } from '@onekeyfe/hd-shared';
2
+
3
+ import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
4
+
5
+ import type { Device } from '../../src/device/Device';
6
+ import type { FirmwareByteSource } from '../../src/api/firmware/FirmwareArtifactSource';
7
+
8
+ jest.mock('../../src/data/config', () => ({
9
+ getSDKVersion: jest.fn(() => '1.0.0'),
10
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
11
+ }));
12
+
13
+ describe('uploadFirmwareFromSource', () => {
14
+ it('serves legacy Touch firmware requests without materializing the complete artifact', async () => {
15
+ const size = 700 * 1024;
16
+ const requestedRanges: Array<{ offset: number; length: number }> = [];
17
+ const source: FirmwareByteSource = {
18
+ size,
19
+ readAt: jest.fn((offset: number, length: number) => {
20
+ requestedRanges.push({ offset, length });
21
+ return Promise.resolve(new ArrayBuffer(length));
22
+ }),
23
+ close: jest.fn(() => Promise.resolve()),
24
+ };
25
+ let nextOffset = 0;
26
+ const requestedLength = 128 * 1024;
27
+ const typedCall = jest.fn((type: string) => {
28
+ if (type === 'FirmwareErase') {
29
+ return Promise.resolve({
30
+ type: 'FirmwareRequest',
31
+ message: {
32
+ offset: nextOffset,
33
+ length: Math.min(requestedLength, size - nextOffset),
34
+ },
35
+ });
36
+ }
37
+ if (type === 'FirmwareUpload') {
38
+ nextOffset += Math.min(requestedLength, size - nextOffset);
39
+ if (nextOffset < size) {
40
+ return Promise.resolve({
41
+ type: 'FirmwareRequest',
42
+ message: {
43
+ offset: nextOffset,
44
+ length: Math.min(requestedLength, size - nextOffset),
45
+ },
46
+ });
47
+ }
48
+ return Promise.resolve({
49
+ type: 'Success',
50
+ message: { message: 'installed' },
51
+ });
52
+ }
53
+ return Promise.reject(new Error(`Unexpected command: ${type}`));
54
+ });
55
+ const device = {
56
+ features: undefined,
57
+ emit: jest.fn(),
58
+ getCurrentDeviceType: () => EDeviceType.Touch,
59
+ toMessageObject: () => ({}),
60
+ } as unknown as Device;
61
+
62
+ await expect(
63
+ uploadFirmwareFromSource('firmware', typedCall as never, jest.fn(), device, source)
64
+ ).resolves.toEqual({ message: 'installed' });
65
+
66
+ expect(requestedRanges).toHaveLength(Math.ceil(size / requestedLength));
67
+ expect(requestedRanges.every(({ length }) => length <= 256 * 1024)).toBe(true);
68
+ expect(requestedRanges.some(({ length }) => length === size)).toBe(false);
69
+ });
70
+ });