@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.
- package/__tests__/protocol-v2-bootloader-mode.test.ts +37 -0
- package/__tests__/protocol-v2.test.ts +240 -82
- package/__tests__/protocolV2FileWrite.test.ts +67 -0
- package/dist/api/FileWrite.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +32 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +6 -0
- package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts +2 -2
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
- package/dist/index.d.ts +9 -11
- package/dist/index.js +736 -391
- package/dist/protocols/protocol-v2/features.d.ts +1 -0
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +1 -1
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/types/api/getDeviceInfo.d.ts +1 -1
- package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +2 -2
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +2 -5
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts +3 -2
- package/dist/types/device.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FileWrite.ts +18 -186
- package/src/api/FirmwareUpdateV4.ts +46 -6
- package/src/api/cardano/CardanoSignMessage.ts +1 -1
- package/src/api/cardano/CardanoSignTransaction.ts +1 -1
- package/src/api/helpers/protocolV2FileWrite.ts +168 -0
- package/src/api/index.ts +1 -1
- package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +18 -0
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +22 -52
- package/src/api/protocol-v2/helpers.ts +0 -3
- package/src/data/messages/messages-protocol-v2.json +422 -16
- package/src/data/messages/messages.json +0 -8
- package/src/device/Device.ts +7 -17
- package/src/deviceProfile/buildDeviceFeatures.ts +28 -12
- package/src/deviceProfile/buildDeviceProfile.ts +8 -6
- package/src/inject.ts +2 -2
- package/src/protocols/protocol-v2/features.ts +19 -1
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/types/api/getDeviceInfo.ts +1 -1
- package/src/types/api/index.ts +2 -2
- package/src/types/api/protocolV2.ts +6 -6
- package/src/types/device.ts +6 -1
- package/dist/api/protocol-v2/FilesystemDiskControl.d.ts +0 -13
- package/dist/api/protocol-v2/FilesystemDiskControl.d.ts.map +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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('
|
|
124
|
-
|
|
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('
|
|
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(
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
1755
|
-
|
|
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([
|
|
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
|
|
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: '
|
|
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
|
-
|
|
1810
|
-
|
|
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([
|
|
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', '
|
|
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('
|
|
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 (
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
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(
|
|
4019
|
+
.mockImplementationOnce(() => {
|
|
3857
4020
|
calls.push('run-1');
|
|
3858
|
-
|
|
4021
|
+
return Promise.reject(deviceLockedError());
|
|
3859
4022
|
})
|
|
3860
|
-
.mockImplementationOnce(
|
|
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(
|
|
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(
|
|
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(
|
|
3907
|
-
throw initialError;
|
|
3908
|
-
}),
|
|
4068
|
+
run: jest.fn().mockRejectedValue(initialError),
|
|
3909
4069
|
};
|
|
3910
4070
|
const unlockFailDevice = {
|
|
3911
4071
|
isProtocolV2: () => true,
|
|
3912
|
-
unlockDevice: jest.fn(
|
|
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(
|
|
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:
|
|
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":"
|
|
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<{
|