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

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 +86 -31
  14. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +121 -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 +3088 -1063
  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 +308 -121
  83. package/src/api/FirmwareUpdateV3.ts +265 -58
  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
 
@@ -148,6 +151,33 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
148
151
  jest.restoreAllMocks();
149
152
  });
150
153
 
154
+ it('rejects mixed binary and prepared artifact inputs during initialization', () => {
155
+ const method = new FirmwareUpdateV2({
156
+ id: 1,
157
+ payload: {
158
+ method: 'firmwareUpdateV2',
159
+ connectId: 'connect-id',
160
+ platform: 'desktop',
161
+ updateType: 'firmware',
162
+ binary: new ArrayBuffer(4),
163
+ artifact: {
164
+ artifactRef: `fw:${'a'.repeat(64)}`,
165
+ size: 4,
166
+ sha256: 'a'.repeat(64),
167
+ },
168
+ preparedPlan: {},
169
+ hostBindingGeneration: 1,
170
+ },
171
+ });
172
+
173
+ expect(() => method.init()).toThrow(
174
+ expect.objectContaining({
175
+ errorCode: HardwareErrorCode.CallMethodInvalidParameter,
176
+ message: 'Firmware update binary and artifact are mutually exclusive',
177
+ })
178
+ );
179
+ });
180
+
151
181
  it.each([
152
182
  {
153
183
  context: 'Classic 1S normal mode',
@@ -175,7 +205,26 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
175
205
  expect(mockGetBinary).toHaveBeenCalledTimes(1);
176
206
  expectNoFirmwareMutationCalls(typedCall);
177
207
  expect(acquire).not.toHaveBeenCalled();
178
- expect(mockUploadFirmware).not.toHaveBeenCalled();
208
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
209
+ });
210
+
211
+ it('preserves structured preparation errors before any firmware mutation', async () => {
212
+ const preparationError = ERRORS.TypedError(
213
+ HardwareErrorCode.RuntimeError,
214
+ 'Firmware must be prepared by the external firmware host',
215
+ {
216
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
217
+ artifactName: 'firmware',
218
+ }
219
+ );
220
+ mockGetBinary.mockRejectedValue(preparationError);
221
+ const { method, typedCall, acquire } = createMethod();
222
+
223
+ await expect(method.run()).rejects.toBe(preparationError);
224
+
225
+ expectNoFirmwareMutationCalls(typedCall);
226
+ expect(acquire).not.toHaveBeenCalled();
227
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
179
228
  });
180
229
 
181
230
  it.each([
@@ -216,9 +265,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
216
265
  binary: firmwareBinary,
217
266
  } as Awaited<ReturnType<typeof getBinary>>);
218
267
  });
219
- mockUploadFirmware.mockImplementation(() => {
268
+ mockUploadFirmwareFromSource.mockImplementation(() => {
220
269
  callOrder.push('upload');
221
- return Promise.resolve(expectedResponse as Awaited<ReturnType<typeof uploadFirmware>>);
270
+ return Promise.resolve(
271
+ expectedResponse as Awaited<ReturnType<typeof uploadFirmwareFromSource>>
272
+ );
222
273
  });
223
274
  const { method, typedCall } = createMethod({
224
275
  updateType,
@@ -248,15 +299,15 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
248
299
  },
249
300
  })
250
301
  );
251
- expect(mockUploadFirmware).toHaveBeenCalledWith(
302
+ expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
252
303
  updateType,
253
304
  expect.any(Function),
254
305
  expect.any(Function),
255
306
  method.device,
256
- {
257
- payload: firmwareBinary,
258
- rebootOnSuccess: true,
259
- },
307
+ expect.objectContaining({
308
+ size: firmwareBinary.byteLength,
309
+ }),
310
+ true,
260
311
  isUpdateBootloader
261
312
  );
262
313
  }
@@ -271,11 +322,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
271
322
  binary: firmwareBinary,
272
323
  } as Awaited<ReturnType<typeof getBinary>>);
273
324
  });
274
- mockUploadFirmware.mockImplementation(() => {
325
+ mockUploadFirmwareFromSource.mockImplementation(() => {
275
326
  callOrder.push('upload');
276
327
  return Promise.resolve({
277
328
  success: true,
278
- } as Awaited<ReturnType<typeof uploadFirmware>>);
329
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
279
330
  });
280
331
  const { method, typedCall, acquire } = createMethod({
281
332
  bootloaderMode: true,
@@ -300,9 +351,9 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
300
351
  resolveDownload = resolve;
301
352
  })
302
353
  );
303
- mockUploadFirmware.mockResolvedValue({
354
+ mockUploadFirmwareFromSource.mockResolvedValue({
304
355
  success: true,
305
- } as Awaited<ReturnType<typeof uploadFirmware>>);
356
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
306
357
  const { method, typedCall } = createMethod();
307
358
 
308
359
  const runPromise = method.run();
@@ -333,7 +384,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
333
384
 
334
385
  expectNoFirmwareMutationCalls(typedCall);
335
386
  expect(acquire).not.toHaveBeenCalled();
336
- expect(mockUploadFirmware).not.toHaveBeenCalled();
387
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
337
388
  });
338
389
 
339
390
  it.each([
@@ -342,23 +393,23 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
342
393
  ])(
343
394
  'keeps the non-empty %s binary overload and never downloads it again',
344
395
  async (_kind, binary) => {
345
- mockUploadFirmware.mockResolvedValue({
396
+ mockUploadFirmwareFromSource.mockResolvedValue({
346
397
  success: true,
347
- } as Awaited<ReturnType<typeof uploadFirmware>>);
398
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
348
399
  const { method } = createMethod({ binary });
349
400
 
350
401
  await method.run();
351
402
 
352
403
  expect(mockGetBinary).not.toHaveBeenCalled();
353
- expect(mockUploadFirmware).toHaveBeenCalledWith(
404
+ expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
354
405
  'firmware',
355
406
  expect.any(Function),
356
407
  expect.any(Function),
357
408
  method.device,
358
- {
359
- payload: binary,
360
- rebootOnSuccess: true,
361
- },
409
+ expect.objectContaining({
410
+ size: binary.byteLength,
411
+ }),
412
+ true,
362
413
  undefined
363
414
  );
364
415
  }
@@ -369,17 +420,21 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
369
420
  ['DataView', new DataView(new Uint8Array([9, 3, 4, 9]).buffer, 1, 2), [3, 4]],
370
421
  ['custom Buffer', createCustomBuffer(4), [1, 2, 3, 4]],
371
422
  ])('normalizes a non-empty %s before rebooting', async (_kind, binary, expectedBytes) => {
372
- mockUploadFirmware.mockResolvedValue({
373
- success: true,
374
- } as Awaited<ReturnType<typeof uploadFirmware>>);
423
+ let uploadedBytes: number[] = [];
424
+ mockUploadFirmwareFromSource.mockImplementation(
425
+ async (_updateType, _typedCall, _postMessage, _device, source) => {
426
+ uploadedBytes = Array.from(new Uint8Array(await source.readAt(0, source.size)));
427
+ return {
428
+ success: true,
429
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>;
430
+ }
431
+ );
375
432
  const { method } = createMethod({ binary });
376
433
 
377
434
  await method.run();
378
435
 
379
436
  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);
437
+ expect(uploadedBytes).toEqual(expectedBytes);
383
438
  });
384
439
 
385
440
  it.each([
@@ -397,6 +452,6 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
397
452
  expect(mockGetBinary).not.toHaveBeenCalled();
398
453
  expectNoFirmwareMutationCalls(typedCall);
399
454
  expect(acquire).not.toHaveBeenCalled();
400
- expect(mockUploadFirmware).not.toHaveBeenCalled();
455
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
401
456
  });
402
457
  });
@@ -0,0 +1,121 @@
1
+ import * as hdShared from '@onekeyfe/hd-shared';
2
+
3
+ import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
4
+ import { DevicePool } from '../../src/device/DevicePool';
5
+
6
+ import type { Device } from '../../src/device/Device';
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('FirmwareUpdateV3 reconnect', () => {
14
+ afterEach(() => {
15
+ jest.restoreAllMocks();
16
+ });
17
+
18
+ it('keeps transient USB acquire failures inside the reconnect loop', async () => {
19
+ const acquire = jest.fn().mockResolvedValue(undefined);
20
+ const commands = {
21
+ disposed: true,
22
+ mainId: '',
23
+ };
24
+ const cachedDevice = {} as Device;
25
+ const updateFromCache = jest.fn();
26
+ jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
27
+ devices: {},
28
+ deviceList: [cachedDevice],
29
+ });
30
+
31
+ const method = new FirmwareUpdateV3({
32
+ id: 1,
33
+ payload: {
34
+ method: 'firmwareUpdateV3',
35
+ connectId: 'usb-path',
36
+ },
37
+ });
38
+ method.isBleReconnect = jest.fn(() => false);
39
+ method.device = {
40
+ originalDescriptor: {
41
+ id: 'usb-path',
42
+ path: 'usb-path',
43
+ protocolType: 'V1',
44
+ },
45
+ deviceConnector: {
46
+ enumerate: jest.fn().mockResolvedValue({
47
+ descriptors: [{ path: 'usb-path' }],
48
+ }),
49
+ },
50
+ updateFromCache,
51
+ acquire,
52
+ commands,
53
+ getCommands: () => commands,
54
+ mainId: 'usb-session',
55
+ } as unknown as Device;
56
+
57
+ await method.waitForDeviceReconnect(1000);
58
+
59
+ expect(updateFromCache).toHaveBeenCalledWith(cachedDevice);
60
+ expect(acquire).toHaveBeenCalledWith(undefined, {
61
+ throwOnRunPromiseError: true,
62
+ });
63
+ expect(commands.disposed).toBe(false);
64
+ expect(commands.mainId).toBe('usb-session');
65
+ });
66
+
67
+ it('uses the BLE reconnect budget for a prepared BLE artifact', async () => {
68
+ jest.useFakeTimers({ doNotFake: ['performance'] });
69
+ jest.spyOn(hdShared, 'wait').mockResolvedValue(undefined);
70
+ const method = new FirmwareUpdateV3({
71
+ id: 1,
72
+ payload: {
73
+ method: 'firmwareUpdateV3',
74
+ connectId: 'ble-device',
75
+ },
76
+ });
77
+ const reconnectError = new Error('stop after reconnect budget assertion');
78
+ const waitForDeviceReconnect = jest
79
+ .spyOn(method, 'waitForDeviceReconnect')
80
+ .mockRejectedValue(reconnectError);
81
+ method.isBleReconnect = jest.fn(() => true);
82
+ method.params = {
83
+ platform: 'native',
84
+ artifacts: {
85
+ ble: {
86
+ artifactRef: `fw:${'a'.repeat(64)}`,
87
+ size: 4,
88
+ sha256: 'a'.repeat(64),
89
+ },
90
+ },
91
+ } as any;
92
+ const commands = {
93
+ typedCall: jest.fn().mockRejectedValue(new Error('device rebooting')),
94
+ };
95
+ method.device = {
96
+ features: {},
97
+ getCommands: () => commands,
98
+ } as unknown as Device;
99
+ (method as any).createUpdatesFolderIfNotExists = jest.fn().mockResolvedValue(undefined);
100
+ (method as any).startEmmcFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
101
+ method.postTipMessage = jest.fn();
102
+ method.postProcessingMessage = jest.fn();
103
+ method.postProgressMessage = jest.fn();
104
+
105
+ try {
106
+ await expect(
107
+ (method as any).executeUpdate({
108
+ resourceBinary: null,
109
+ resourceEntries: [],
110
+ fwSources: [],
111
+ bootloaderSource: null,
112
+ })
113
+ ).rejects.toBe(reconnectError);
114
+
115
+ expect(waitForDeviceReconnect).toHaveBeenCalledWith(3 * 60 * 1000);
116
+ } finally {
117
+ jest.clearAllTimers();
118
+ jest.useRealTimers();
119
+ }
120
+ });
121
+ });
@@ -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
+ });