@onekeyfe/hd-core 1.1.32 → 1.1.34-alpha.1

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.
@@ -0,0 +1,394 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
4
+ import { getBinary } from '../../src/api/firmware/getBinary';
5
+ import { uploadFirmware } from '../../src/api/firmware/uploadFirmware';
6
+ import * as utils from '../../src/utils';
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
+ jest.mock('../../src/api/firmware/getBinary', () => ({
14
+ getBinary: jest.fn(),
15
+ getInfo: jest.fn(),
16
+ getSysResourceBinary: jest.fn(),
17
+ }));
18
+
19
+ jest.mock('../../src/api/firmware/uploadFirmware', () => ({
20
+ updateResources: jest.fn(),
21
+ uploadFirmware: jest.fn(),
22
+ }));
23
+
24
+ jest.mock('../../src/device/DevicePool', () => ({
25
+ DevicePool: {
26
+ clearDeviceCache: jest.fn(),
27
+ devicesCache: {},
28
+ },
29
+ }));
30
+
31
+ const mockGetBinary = getBinary as jest.MockedFunction<typeof getBinary>;
32
+ const mockUploadFirmware = uploadFirmware as jest.MockedFunction<typeof uploadFirmware>;
33
+
34
+ type DeviceType = 'CLASSIC1S' | 'PURE';
35
+
36
+ const createCustomBuffer = (byteLength: number) => {
37
+ const customBuffer: {
38
+ [index: number]: number;
39
+ byteLength: number;
40
+ constructor: {
41
+ isBuffer: (value: unknown) => boolean;
42
+ };
43
+ length: number;
44
+ } = {
45
+ byteLength,
46
+ constructor: {
47
+ isBuffer: (value: unknown) => value === customBuffer,
48
+ },
49
+ length: byteLength,
50
+ };
51
+ for (let index = 0; index < byteLength; index += 1) {
52
+ customBuffer[index] = index + 1;
53
+ }
54
+ return customBuffer;
55
+ };
56
+
57
+ const countCommandCalls = (typedCall: jest.Mock, command: string) =>
58
+ typedCall.mock.calls.filter(([calledCommand]) => calledCommand === command).length;
59
+
60
+ const expectNoFirmwareMutationCalls = (typedCall: jest.Mock) => {
61
+ expect(countCommandCalls(typedCall, 'DeviceBackToBoot')).toBe(0);
62
+ expect(countCommandCalls(typedCall, 'FirmwareErase')).toBe(0);
63
+ expect(countCommandCalls(typedCall, 'FirmwareErase_ex')).toBe(0);
64
+ expect(countCommandCalls(typedCall, 'FirmwareUpload')).toBe(0);
65
+ expect(countCommandCalls(typedCall, 'UpgradeFileHeader')).toBe(0);
66
+ };
67
+
68
+ const createMethod = ({
69
+ binary,
70
+ updateType = 'firmware',
71
+ isUpdateBootloader,
72
+ deviceType = 'CLASSIC1S',
73
+ bootloaderMode = false,
74
+ }: {
75
+ binary?: unknown;
76
+ updateType?: 'firmware' | 'ble';
77
+ isUpdateBootloader?: boolean;
78
+ deviceType?: DeviceType;
79
+ bootloaderMode?: boolean;
80
+ } = {}) => {
81
+ const method = new FirmwareUpdateV2({
82
+ id: 1,
83
+ payload: {
84
+ method: 'firmwareUpdateV2',
85
+ connectId: 'connect-id',
86
+ deviceId: 'device-id',
87
+ platform: 'desktop',
88
+ updateType,
89
+ isUpdateBootloader,
90
+ ...(binary !== undefined ? { binary } : { version: [3, 0, 0] }),
91
+ },
92
+ });
93
+ method.init();
94
+
95
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success' });
96
+ const checkDisposed = jest.fn();
97
+ const acquire = jest.fn().mockResolvedValue(undefined);
98
+ const commands = {
99
+ typedCall,
100
+ checkDisposed,
101
+ disposed: false,
102
+ };
103
+
104
+ method.device = {
105
+ features: {
106
+ onekey_device_type: deviceType,
107
+ onekey_serial_no: deviceType === 'PURE' ? 'CP123456' : 'CL123456',
108
+ bootloader_mode: bootloaderMode,
109
+ major_version: 3,
110
+ minor_version: 0,
111
+ patch_version: 0,
112
+ capabilities: [],
113
+ },
114
+ commands,
115
+ getCommands: () => commands,
116
+ acquire,
117
+ toMessageObject: jest.fn(() => ({})),
118
+ } as any;
119
+ method.postMessage = jest.fn();
120
+ jest.spyOn(method, 'checkDeviceToBootloader').mockImplementation(() => {
121
+ method.checkPromise = {
122
+ promise: Promise.resolve(true),
123
+ } as any;
124
+ });
125
+
126
+ return {
127
+ method,
128
+ typedCall,
129
+ acquire,
130
+ };
131
+ };
132
+
133
+ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
134
+ beforeEach(() => {
135
+ jest.clearAllMocks();
136
+ jest.spyOn(utils, 'wait').mockResolvedValue(undefined);
137
+ });
138
+
139
+ afterEach(() => {
140
+ jest.restoreAllMocks();
141
+ });
142
+
143
+ it.each([
144
+ {
145
+ context: 'Classic 1S normal mode',
146
+ deviceType: 'CLASSIC1S' as const,
147
+ bootloaderMode: false,
148
+ },
149
+ {
150
+ context: 'Classic Pure normal mode',
151
+ deviceType: 'PURE' as const,
152
+ bootloaderMode: false,
153
+ },
154
+ {
155
+ context: 'initial bootloader mode',
156
+ deviceType: 'CLASSIC1S' as const,
157
+ bootloaderMode: true,
158
+ },
159
+ ])('blocks all firmware mutation after final download failure in $context', async options => {
160
+ mockGetBinary.mockRejectedValue(new Error('request failed'));
161
+ const { method, typedCall, acquire } = createMethod(options);
162
+
163
+ await expect(method.run()).rejects.toMatchObject({
164
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
165
+ });
166
+
167
+ expect(mockGetBinary).toHaveBeenCalledTimes(1);
168
+ expectNoFirmwareMutationCalls(typedCall);
169
+ expect(acquire).not.toHaveBeenCalled();
170
+ expect(mockUploadFirmware).not.toHaveBeenCalled();
171
+ });
172
+
173
+ it.each([
174
+ {
175
+ mode: 'firmware release.url',
176
+ updateType: 'firmware' as const,
177
+ isUpdateBootloader: undefined,
178
+ deviceType: 'CLASSIC1S' as const,
179
+ firmwareBinary: new ArrayBuffer(4),
180
+ },
181
+ {
182
+ mode: 'BLE webUpdate',
183
+ updateType: 'ble' as const,
184
+ isUpdateBootloader: undefined,
185
+ deviceType: 'PURE' as const,
186
+ firmwareBinary: Buffer.from([1, 2, 3, 4]),
187
+ },
188
+ {
189
+ mode: 'bootloaderResource',
190
+ updateType: 'firmware' as const,
191
+ isUpdateBootloader: true,
192
+ deviceType: 'CLASSIC1S' as const,
193
+ firmwareBinary: new ArrayBuffer(8),
194
+ },
195
+ ])(
196
+ 'acquires $mode before rebooting and preserves the response shape',
197
+ async ({ updateType, isUpdateBootloader, deviceType = 'CLASSIC1S', firmwareBinary }) => {
198
+ const callOrder: string[] = [];
199
+ const expectedResponse = {
200
+ success: true,
201
+ payload: {
202
+ firmwareVersion: '3.0.0',
203
+ },
204
+ };
205
+ mockGetBinary.mockImplementation(() => {
206
+ callOrder.push('download');
207
+ return Promise.resolve({
208
+ binary: firmwareBinary,
209
+ } as Awaited<ReturnType<typeof getBinary>>);
210
+ });
211
+ mockUploadFirmware.mockImplementation(() => {
212
+ callOrder.push('upload');
213
+ return Promise.resolve(expectedResponse as Awaited<ReturnType<typeof uploadFirmware>>);
214
+ });
215
+ const { method, typedCall } = createMethod({
216
+ updateType,
217
+ isUpdateBootloader,
218
+ deviceType,
219
+ });
220
+ typedCall.mockImplementation(type => {
221
+ if (type === 'DeviceBackToBoot') {
222
+ callOrder.push('reboot');
223
+ }
224
+ return Promise.resolve({ type: 'Success' });
225
+ });
226
+
227
+ await expect(method.run()).resolves.toBe(expectedResponse);
228
+
229
+ expect(callOrder).toEqual(['download', 'reboot', 'upload']);
230
+ expect(mockGetBinary).toHaveBeenCalledWith(
231
+ expect.objectContaining({
232
+ updateType,
233
+ isUpdateBootloader,
234
+ requestOptions: {
235
+ connectTimeoutMs: 60_000,
236
+ readTimeoutMs: 60_000,
237
+ overallTimeoutMs: 180_000,
238
+ maxRetries: 2,
239
+ retryDelayMs: 500,
240
+ },
241
+ })
242
+ );
243
+ expect(mockUploadFirmware).toHaveBeenCalledWith(
244
+ updateType,
245
+ expect.any(Function),
246
+ expect.any(Function),
247
+ method.device,
248
+ {
249
+ payload: firmwareBinary,
250
+ rebootOnSuccess: true,
251
+ },
252
+ isUpdateBootloader
253
+ );
254
+ }
255
+ );
256
+
257
+ it('downloads before acquiring and uploading when the device starts in bootloader mode', async () => {
258
+ const callOrder: string[] = [];
259
+ const firmwareBinary = new ArrayBuffer(4);
260
+ mockGetBinary.mockImplementation(() => {
261
+ callOrder.push('download');
262
+ return Promise.resolve({
263
+ binary: firmwareBinary,
264
+ } as Awaited<ReturnType<typeof getBinary>>);
265
+ });
266
+ mockUploadFirmware.mockImplementation(() => {
267
+ callOrder.push('upload');
268
+ return Promise.resolve({
269
+ success: true,
270
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
271
+ });
272
+ const { method, typedCall, acquire } = createMethod({
273
+ bootloaderMode: true,
274
+ });
275
+ acquire.mockImplementation(() => {
276
+ callOrder.push('acquire');
277
+ return Promise.resolve();
278
+ });
279
+
280
+ await method.run();
281
+
282
+ expect(callOrder).toEqual(['download', 'acquire', 'upload']);
283
+ expect(countCommandCalls(typedCall, 'DeviceBackToBoot')).toBe(0);
284
+ });
285
+
286
+ it('does not reboot while the firmware download is pending', async () => {
287
+ let resolveDownload!: (result: Awaited<ReturnType<typeof getBinary>>) => void;
288
+ const firmwareBinary = new ArrayBuffer(4);
289
+ mockGetBinary.mockImplementation(
290
+ () =>
291
+ new Promise(resolve => {
292
+ resolveDownload = resolve;
293
+ })
294
+ );
295
+ mockUploadFirmware.mockResolvedValue({
296
+ success: true,
297
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
298
+ const { method, typedCall } = createMethod();
299
+
300
+ const runPromise = method.run();
301
+
302
+ expect(mockGetBinary).toHaveBeenCalledTimes(1);
303
+ expect(countCommandCalls(typedCall, 'DeviceBackToBoot')).toBe(0);
304
+
305
+ resolveDownload({
306
+ binary: firmwareBinary,
307
+ } as Awaited<ReturnType<typeof getBinary>>);
308
+ await runPromise;
309
+
310
+ expect(countCommandCalls(typedCall, 'DeviceBackToBoot')).toBe(1);
311
+ });
312
+
313
+ it.each([
314
+ ['ArrayBuffer', new ArrayBuffer(0)],
315
+ ['Buffer', Buffer.alloc(0)],
316
+ ])('does not reboot or upload an empty downloaded %s', async (_kind, firmwareBinary) => {
317
+ mockGetBinary.mockResolvedValue({
318
+ binary: firmwareBinary,
319
+ } as Awaited<ReturnType<typeof getBinary>>);
320
+ const { method, typedCall, acquire } = createMethod();
321
+
322
+ await expect(method.run()).rejects.toMatchObject({
323
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
324
+ });
325
+
326
+ expectNoFirmwareMutationCalls(typedCall);
327
+ expect(acquire).not.toHaveBeenCalled();
328
+ expect(mockUploadFirmware).not.toHaveBeenCalled();
329
+ });
330
+
331
+ it.each([
332
+ ['ArrayBuffer', new ArrayBuffer(4)],
333
+ ['Buffer', Buffer.from([1, 2, 3, 4])],
334
+ ])(
335
+ 'keeps the non-empty %s binary overload and never downloads it again',
336
+ async (_kind, binary) => {
337
+ mockUploadFirmware.mockResolvedValue({
338
+ success: true,
339
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
340
+ const { method } = createMethod({ binary });
341
+
342
+ await method.run();
343
+
344
+ expect(mockGetBinary).not.toHaveBeenCalled();
345
+ expect(mockUploadFirmware).toHaveBeenCalledWith(
346
+ 'firmware',
347
+ expect.any(Function),
348
+ expect.any(Function),
349
+ method.device,
350
+ {
351
+ payload: binary,
352
+ rebootOnSuccess: true,
353
+ },
354
+ undefined
355
+ );
356
+ }
357
+ );
358
+
359
+ it.each([
360
+ ['Uint8Array subview', new Uint8Array([9, 1, 2, 9]).subarray(1, 3), [1, 2]],
361
+ ['DataView', new DataView(new Uint8Array([9, 3, 4, 9]).buffer, 1, 2), [3, 4]],
362
+ ['custom Buffer', createCustomBuffer(4), [1, 2, 3, 4]],
363
+ ])('normalizes a non-empty %s before rebooting', async (_kind, binary, expectedBytes) => {
364
+ mockUploadFirmware.mockResolvedValue({
365
+ success: true,
366
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
367
+ const { method } = createMethod({ binary });
368
+
369
+ await method.run();
370
+
371
+ expect(mockGetBinary).not.toHaveBeenCalled();
372
+ const normalizedBinary = mockUploadFirmware.mock.calls[0][4].payload;
373
+ expect(normalizedBinary).toBeInstanceOf(ArrayBuffer);
374
+ expect(Array.from(new Uint8Array(normalizedBinary))).toEqual(expectedBytes);
375
+ });
376
+
377
+ it.each([
378
+ ['ArrayBuffer', new ArrayBuffer(0)],
379
+ ['Buffer', Buffer.alloc(0)],
380
+ ['ArrayBufferView', new Uint8Array(0)],
381
+ ['custom Buffer', createCustomBuffer(0)],
382
+ ])('rejects an empty supplied %s before rebooting', async (_kind, binary) => {
383
+ const { method, typedCall, acquire } = createMethod({ binary });
384
+
385
+ await expect(method.run()).rejects.toMatchObject({
386
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
387
+ });
388
+
389
+ expect(mockGetBinary).not.toHaveBeenCalled();
390
+ expectNoFirmwareMutationCalls(typedCall);
391
+ expect(acquire).not.toHaveBeenCalled();
392
+ expect(mockUploadFirmware).not.toHaveBeenCalled();
393
+ });
394
+ });
@@ -454,6 +454,66 @@ describe('KaspaSignTransaction protocol negotiation', () => {
454
454
  // Back to the current transaction after the previous one is streamed.
455
455
  expect(mockTypedCall.mock.calls[4][0]).toBe('KaspaTxAckInput');
456
456
  });
457
+
458
+ describe('previous-transaction id mismatch', () => {
459
+ const PREV_ID = 'aa'.repeat(32);
460
+ const withRefTxs = {
461
+ outputs: [{ satoshis: 100000, script: SCRIPT, address: ADDRESS }],
462
+ refTxs: [
463
+ {
464
+ txId: PREV_ID,
465
+ version: 0,
466
+ inputs: [],
467
+ outputs: [{ satoshis: '200000', script: SCRIPT }],
468
+ },
469
+ ],
470
+ };
471
+
472
+ const streamThenReject = (error: unknown) =>
473
+ mockTypedCall
474
+ .mockResolvedValueOnce(
475
+ txRequest({ request_type: 'KASPA_TX_PREV_META', prev_tx_id: PREV_ID })
476
+ )
477
+ .mockRejectedValueOnce(error);
478
+
479
+ it('surfaces the device rejection under its own error code', async () => {
480
+ streamThenReject(
481
+ ERRORS.TypedError(
482
+ HardwareErrorCode.RuntimeError,
483
+ 'Failure_ProcessError,Kaspa previous transaction id mismatch'
484
+ )
485
+ );
486
+
487
+ await expect(runMethod(withRefTxs)).rejects.toMatchObject({
488
+ errorCode: HardwareErrorCode.KaspaPrevTxIdMismatch,
489
+ });
490
+ // Never silently re-signs without refTxs.
491
+ expect(mockTypedCall).toHaveBeenCalledTimes(2);
492
+ });
493
+
494
+ it('leaves an unrelated device error untouched', async () => {
495
+ streamThenReject(
496
+ ERRORS.TypedError(
497
+ HardwareErrorCode.RuntimeError,
498
+ 'Failure_ProcessError,some other device error'
499
+ )
500
+ );
501
+
502
+ await expect(runMethod(withRefTxs)).rejects.toMatchObject({
503
+ errorCode: HardwareErrorCode.RuntimeError,
504
+ });
505
+ expect(mockTypedCall).toHaveBeenCalledTimes(2);
506
+ });
507
+
508
+ it('leaves user cancellation untouched', async () => {
509
+ streamThenReject(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
510
+
511
+ await expect(runMethod(withRefTxs)).rejects.toMatchObject({
512
+ errorCode: HardwareErrorCode.ActionCancelled,
513
+ });
514
+ expect(mockTypedCall).toHaveBeenCalledTimes(2);
515
+ });
516
+ });
457
517
  });
458
518
 
459
519
  describe('KaspaSignTransaction wire encoding', () => {