@onekeyfe/hd-core 1.2.0-alpha.104 → 1.2.0-alpha.106
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__/DeviceCommands.test.ts +100 -28
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
- package/__tests__/device-lifecycle-events.test.ts +113 -2
- package/__tests__/device-pool-state.test.ts +1 -5
- package/__tests__/deviceSettings.test.ts +0 -1
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +145 -0
- package/__tests__/get-device-state.test.ts +0 -50
- package/__tests__/logBlockEvent.test.ts +13 -1
- package/__tests__/protocol-v2.test.ts +466 -126
- package/__tests__/refresh-device-state.test.ts +0 -1
- package/__tests__/search-devices.test.ts +0 -2
- package/dist/api/FirmwareUpdateV4.d.ts +7 -3
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceState.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -3
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +1 -6
- package/dist/index.js +260 -196
- package/dist/protocols/protocol-v2/features.d.ts +1 -1
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/types/api/getDeviceState.d.ts +0 -1
- package/dist/types/api/getDeviceState.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +262 -172
- package/src/api/GetDeviceState.ts +0 -1
- package/src/api/SearchDevices.ts +0 -2
- package/src/api/firmware/protocolV2Release.ts +0 -1
- package/src/core/index.ts +6 -6
- package/src/device/Device.ts +27 -63
- package/src/device/DeviceCommands.ts +4 -14
- package/src/device/DevicePool.ts +3 -11
- package/src/events/logBlockEvent.ts +14 -1
- package/src/protocols/protocol-v2/features.ts +9 -2
- package/src/types/api/getDeviceState.ts +0 -2
- package/src/utils/deviceSettings.ts +1 -1
- package/__tests__/protocol-v2-legacy-recovery.test.ts +0 -29
- package/dist/core/protocolV2LegacyRecovery.d.ts +0 -5
- package/dist/core/protocolV2LegacyRecovery.d.ts.map +0 -1
- package/src/core/protocolV2LegacyRecovery.ts +0 -12
|
@@ -19,12 +19,12 @@ const createCommands = () => {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
describe('DeviceCommands failure mapping', () => {
|
|
22
|
-
it('logs passphrase
|
|
22
|
+
it('logs passphrase request and canonical response without exposing the passphrase', async () => {
|
|
23
23
|
const commands = createCommands();
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
25
|
+
const coreLog = getLogger(LoggerNames.Core);
|
|
26
|
+
log.messages.length = 0;
|
|
27
|
+
coreLog.messages.length = 0;
|
|
28
28
|
commands.mainId = 'main-id';
|
|
29
29
|
commands.transport = {
|
|
30
30
|
call: jest.fn().mockResolvedValue({
|
|
@@ -36,13 +36,13 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
36
36
|
} as any;
|
|
37
37
|
|
|
38
38
|
await expect(
|
|
39
|
-
commands.
|
|
39
|
+
commands._commonCall('DeviceSessionAskPassphrase', {
|
|
40
40
|
passphrase: 'hidden-wallet-secret',
|
|
41
41
|
on_device: false,
|
|
42
42
|
})
|
|
43
43
|
).resolves.toMatchObject({ type: 'Success' });
|
|
44
44
|
|
|
45
|
-
expect(
|
|
45
|
+
expect(log.messages.at(-2)?.message).toEqual([
|
|
46
46
|
'[DeviceCommands] [call] Sending',
|
|
47
47
|
'DeviceSessionAskPassphrase',
|
|
48
48
|
{
|
|
@@ -50,21 +50,27 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
50
50
|
on_device: false,
|
|
51
51
|
},
|
|
52
52
|
]);
|
|
53
|
-
expect(
|
|
54
|
-
'
|
|
55
|
-
'Success',
|
|
53
|
+
expect(log.messages.at(-1)?.message).toEqual([
|
|
54
|
+
'_filterCommonTypes: ',
|
|
56
55
|
{
|
|
57
|
-
|
|
56
|
+
request: 'DeviceSessionAskPassphrase',
|
|
57
|
+
response: {
|
|
58
|
+
type: 'Success',
|
|
59
|
+
message: {
|
|
60
|
+
message: 'Passphrase accepted',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
58
63
|
},
|
|
59
64
|
]);
|
|
60
|
-
expect(
|
|
61
|
-
'
|
|
62
|
-
);
|
|
65
|
+
expect(
|
|
66
|
+
coreLog.messages.some(entry => entry.message[0] === '[DeviceCommands] [call] Received')
|
|
67
|
+
).toBe(false);
|
|
68
|
+
expect(JSON.stringify(log.messages)).not.toContain('hidden-wallet-secret');
|
|
63
69
|
});
|
|
64
70
|
|
|
65
|
-
it('logs DeviceStatus response fields without exposing the device ID', async () => {
|
|
71
|
+
it('logs canonical DeviceStatus response fields without exposing the device ID', async () => {
|
|
66
72
|
const commands = createCommands();
|
|
67
|
-
const log = getLogger(LoggerNames.
|
|
73
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
68
74
|
log.messages.length = 0;
|
|
69
75
|
commands.mainId = 'main-id';
|
|
70
76
|
commands.transport = {
|
|
@@ -82,22 +88,27 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
82
88
|
}),
|
|
83
89
|
} as any;
|
|
84
90
|
|
|
85
|
-
await expect(commands.
|
|
91
|
+
await expect(commands._commonCall('DeviceStatusGet', {})).resolves.toMatchObject({
|
|
86
92
|
type: 'DeviceStatus',
|
|
87
93
|
});
|
|
88
94
|
|
|
89
95
|
const receivedLog = log.messages.at(-1)?.message;
|
|
90
96
|
expect(receivedLog).toEqual([
|
|
91
|
-
'
|
|
92
|
-
'DeviceStatus',
|
|
97
|
+
'_filterCommonTypes: ',
|
|
93
98
|
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
request: 'DeviceStatusGet',
|
|
100
|
+
response: {
|
|
101
|
+
type: 'DeviceStatus',
|
|
102
|
+
message: {
|
|
103
|
+
device_id: '[REDACTED]',
|
|
104
|
+
unlocked: true,
|
|
105
|
+
init_states: true,
|
|
106
|
+
backup_required: false,
|
|
107
|
+
passphrase_enabled: true,
|
|
108
|
+
attach_to_pin_enabled: false,
|
|
109
|
+
unlocked_by_attach_to_pin: false,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
101
112
|
},
|
|
102
113
|
]);
|
|
103
114
|
expect(JSON.stringify(receivedLog)).not.toContain('sensitive-device-id');
|
|
@@ -125,6 +136,59 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
125
136
|
expect(JSON.stringify(log.messages)).not.toContain('secret-wallet-address');
|
|
126
137
|
});
|
|
127
138
|
|
|
139
|
+
it('logs the sanitized DeviceInfo response payload', async () => {
|
|
140
|
+
const commands = createCommands();
|
|
141
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
142
|
+
log.messages.length = 0;
|
|
143
|
+
|
|
144
|
+
await expect(
|
|
145
|
+
commands._filterCommonTypes(
|
|
146
|
+
{
|
|
147
|
+
type: 'DeviceInfo',
|
|
148
|
+
message: {
|
|
149
|
+
protocol_version: 2,
|
|
150
|
+
hw: {
|
|
151
|
+
Device_type: 7,
|
|
152
|
+
hardware_version: '1.0.0',
|
|
153
|
+
},
|
|
154
|
+
fw: {
|
|
155
|
+
application: {
|
|
156
|
+
version: '5.0.0',
|
|
157
|
+
build_id: '20260811',
|
|
158
|
+
hash: 'firmware-hash',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
} as any,
|
|
163
|
+
'DeviceInfoGet'
|
|
164
|
+
)
|
|
165
|
+
).resolves.toMatchObject({ type: 'DeviceInfo' });
|
|
166
|
+
|
|
167
|
+
expect(log.messages.at(-1)?.message).toEqual([
|
|
168
|
+
'_filterCommonTypes: ',
|
|
169
|
+
{
|
|
170
|
+
request: 'DeviceInfoGet',
|
|
171
|
+
response: {
|
|
172
|
+
type: 'DeviceInfo',
|
|
173
|
+
message: {
|
|
174
|
+
protocol_version: 2,
|
|
175
|
+
hw: {
|
|
176
|
+
Device_type: 7,
|
|
177
|
+
hardware_version: '1.0.0',
|
|
178
|
+
},
|
|
179
|
+
fw: {
|
|
180
|
+
application: {
|
|
181
|
+
version: '5.0.0',
|
|
182
|
+
build_id: '20260811',
|
|
183
|
+
hash: 'firmware-hash',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
128
192
|
it('logs the sanitized DeviceFirmwareUpdateStatus response payload', async () => {
|
|
129
193
|
const commands = createCommands();
|
|
130
194
|
const log = getLogger(LoggerNames.DeviceCommands);
|
|
@@ -561,17 +625,22 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
561
625
|
});
|
|
562
626
|
|
|
563
627
|
describe('DeviceCommands Protocol V2 interactive response compatibility', () => {
|
|
564
|
-
it('
|
|
628
|
+
it('auto-acks Pro2 ButtonRequest and registers Cancel instead of Initialize', async () => {
|
|
565
629
|
const commands = createCommands();
|
|
566
630
|
const emit = jest.fn();
|
|
631
|
+
const cancelDevice = jest.spyOn(commands, 'cancelDevice').mockResolvedValue(undefined);
|
|
632
|
+
const cancelDeviceOnOneKeyDevice = jest
|
|
633
|
+
.spyOn(commands, 'cancelDeviceOnOneKeyDevice')
|
|
634
|
+
.mockResolvedValue(undefined);
|
|
567
635
|
const commonCall = jest.fn().mockResolvedValue({
|
|
568
636
|
type: 'Success',
|
|
569
637
|
message: {},
|
|
570
638
|
});
|
|
639
|
+
const setCancelableAction = jest.fn();
|
|
571
640
|
commands.device = {
|
|
572
641
|
...commands.device,
|
|
573
642
|
getCurrentDeviceType: jest.fn(() => 'pro2'),
|
|
574
|
-
setCancelableAction
|
|
643
|
+
setCancelableAction,
|
|
575
644
|
emit,
|
|
576
645
|
listenerCount: jest.fn(() => 0),
|
|
577
646
|
} as any;
|
|
@@ -593,6 +662,9 @@ describe('DeviceCommands Protocol V2 interactive response compatibility', () =>
|
|
|
593
662
|
expect.objectContaining({ code: 'ButtonRequest_PinEntry' })
|
|
594
663
|
);
|
|
595
664
|
expect(commonCall).toHaveBeenCalledWith('ButtonAck', {}, undefined);
|
|
665
|
+
await setCancelableAction.mock.calls[0][0]();
|
|
666
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
667
|
+
expect(cancelDeviceOnOneKeyDevice).not.toHaveBeenCalled();
|
|
596
668
|
});
|
|
597
669
|
|
|
598
670
|
it('rejects PassphraseRequest when the Pro2 UI listener is not registered', async () => {
|
|
@@ -346,7 +346,6 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
346
346
|
expect(typedCall).not.toHaveBeenCalled();
|
|
347
347
|
expect(getDeviceState).toHaveBeenCalledWith({
|
|
348
348
|
refreshSections: ['identity', 'versions'],
|
|
349
|
-
allowLegacyProtocolV2ProtocolInfo: true,
|
|
350
349
|
});
|
|
351
350
|
expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
|
|
352
351
|
});
|
|
@@ -726,7 +725,6 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
726
725
|
});
|
|
727
726
|
expect(getDeviceState).toHaveBeenCalledWith({
|
|
728
727
|
refreshSections: ['identity', 'versions', 'verification'],
|
|
729
|
-
allowLegacyProtocolV2ProtocolInfo: true,
|
|
730
728
|
});
|
|
731
729
|
});
|
|
732
730
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import { TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
1
|
+
import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
4
|
import { initConnector, initCore } from '../src/core';
|
|
5
5
|
import { DataManager } from '../src/data-manager';
|
|
6
6
|
import TransportManager from '../src/data-manager/TransportManager';
|
|
7
7
|
import { Device } from '../src/device/Device';
|
|
8
|
+
import { cancelDeviceInPrompt, cancelDeviceWithInitialize } from '../src/device/DeviceCommands';
|
|
8
9
|
import { DevicePool } from '../src/device/DevicePool';
|
|
9
10
|
import { CORE_EVENT, DEVICE, IFRAME } from '../src/events';
|
|
11
|
+
import { PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE } from '../src/protocols/protocol-v2';
|
|
10
12
|
|
|
11
13
|
import type Core from '../src/core';
|
|
12
14
|
import type { CoreMessage } from '../src/events';
|
|
@@ -271,6 +273,115 @@ describe('public device lifecycle events', () => {
|
|
|
271
273
|
expect(device.isUsedHere()).toBe(false);
|
|
272
274
|
});
|
|
273
275
|
|
|
276
|
+
test.each([
|
|
277
|
+
['V2', 'Cancel', 'webusb'],
|
|
278
|
+
['V2', 'Cancel', 'desktop-web-ble'],
|
|
279
|
+
['V2', 'Cancel', 'react-native'],
|
|
280
|
+
['V1', 'Initialize', 'webusb'],
|
|
281
|
+
['V1', 'Cancel', 'desktop-web-ble'],
|
|
282
|
+
] as const)(
|
|
283
|
+
'sends Protocol %s %s without treating the acquired %s session as disconnected',
|
|
284
|
+
async (protocol, messageType, env) => {
|
|
285
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
286
|
+
const device = createInitializedDevice(protocol);
|
|
287
|
+
const post = jest.fn().mockResolvedValue(undefined);
|
|
288
|
+
const call = jest.fn().mockResolvedValue(undefined);
|
|
289
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
290
|
+
device.originalDescriptor.session = device.mainId;
|
|
291
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
292
|
+
(device as any).commands = { transport: { post, call }, cancel };
|
|
293
|
+
device.setCancelableAction(() =>
|
|
294
|
+
messageType === 'Cancel'
|
|
295
|
+
? cancelDeviceInPrompt(device, false)
|
|
296
|
+
: cancelDeviceWithInitialize(device)
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await device.interruptionFromUser();
|
|
300
|
+
|
|
301
|
+
if (messageType === 'Cancel') {
|
|
302
|
+
expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
|
|
303
|
+
expect(call).not.toHaveBeenCalled();
|
|
304
|
+
} else {
|
|
305
|
+
expect(call).toHaveBeenCalledWith(device.mainId, 'Initialize', {});
|
|
306
|
+
expect(post).not.toHaveBeenCalled();
|
|
307
|
+
}
|
|
308
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(device.hasDeviceAcquire()).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
test.each([
|
|
314
|
+
[EDeviceType.Pro2, 'webusb', false, DeviceType.PRO2],
|
|
315
|
+
[EDeviceType.Neo, 'webusb', false, DeviceType.NEO],
|
|
316
|
+
[EDeviceType.Pro2, 'desktop-web-ble', true, DeviceType.PRO2],
|
|
317
|
+
[EDeviceType.Neo, 'desktop-web-ble', true, DeviceType.NEO],
|
|
318
|
+
[EDeviceType.Pro2, 'react-native', true, DeviceType.PRO2],
|
|
319
|
+
[EDeviceType.Neo, 'react-native', true, DeviceType.NEO],
|
|
320
|
+
[EDeviceType.Pro2, 'lowlevel', false, DeviceType.PRO2],
|
|
321
|
+
] as const)(
|
|
322
|
+
'refreshes %s runtime state after acquiring a fresh %s session',
|
|
323
|
+
async (deviceType, env, isBle, protocolV2DeviceType) => {
|
|
324
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
325
|
+
const device = createInitializedDevice('V2');
|
|
326
|
+
const staleProtocolInfo = {
|
|
327
|
+
version: 1,
|
|
328
|
+
build_fingerprint: 'bootloader__1.0.0__abcdef0__PROD__RELEASE',
|
|
329
|
+
supported_messages: [],
|
|
330
|
+
};
|
|
331
|
+
const freshProtocolInfo = {
|
|
332
|
+
version: 1,
|
|
333
|
+
build_fingerprint: 'application__5.0.0__abcdef0__PROD__RELEASE',
|
|
334
|
+
supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
|
|
335
|
+
};
|
|
336
|
+
device.updateState(
|
|
337
|
+
{
|
|
338
|
+
identity: { deviceType },
|
|
339
|
+
status: { mode: 'bootloader' },
|
|
340
|
+
raw: { protocolV2ProtocolInfo: staleProtocolInfo },
|
|
341
|
+
},
|
|
342
|
+
'initialize'
|
|
343
|
+
);
|
|
344
|
+
const acquire = jest
|
|
345
|
+
.fn()
|
|
346
|
+
.mockResolvedValue(isBle ? { uuid: 'fresh-session', protocolType: 'V2' } : 'fresh-session');
|
|
347
|
+
device.deviceConnector = { acquire } as never;
|
|
348
|
+
|
|
349
|
+
await device.acquire('V2', { throwOnRunPromiseError: true });
|
|
350
|
+
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
351
|
+
if (requestType === 'DeviceInfoGet') {
|
|
352
|
+
return {
|
|
353
|
+
message: {
|
|
354
|
+
protocol_version: 2,
|
|
355
|
+
hw: { Device_type: protocolV2DeviceType, serial_no: 'SERIAL-001' },
|
|
356
|
+
fw: { application: { version: '5.0.0' } },
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
361
|
+
return { message: freshProtocolInfo };
|
|
362
|
+
}
|
|
363
|
+
if (requestType === 'DeviceStatusGet') {
|
|
364
|
+
return {
|
|
365
|
+
message: { init_states: true, unlocked: true, device_id: 'wallet-device-id' },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
369
|
+
});
|
|
370
|
+
device.commands.typedCall = typedCall as never;
|
|
371
|
+
|
|
372
|
+
await device.initialize();
|
|
373
|
+
|
|
374
|
+
expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
|
|
375
|
+
'DeviceInfoGet',
|
|
376
|
+
'ProtocolInfoRequest',
|
|
377
|
+
'DeviceStatusGet',
|
|
378
|
+
]);
|
|
379
|
+
expect(device.state?.status.mode).toBe('normal');
|
|
380
|
+
expect(device.state?.identity.deviceType).toBe(deviceType);
|
|
381
|
+
expect(device.hasDeviceAcquire()).toBe(true);
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
274
385
|
test('exposes the current transport usage state in public device snapshots', () => {
|
|
275
386
|
const getSettings = jest
|
|
276
387
|
.spyOn(DataManager, 'getSettings')
|
|
@@ -38,7 +38,7 @@ describe('DevicePool state lifecycle', () => {
|
|
|
38
38
|
expect(getDeviceState).toHaveBeenNthCalledWith(2, { refreshSections: ['settings'] });
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('
|
|
41
|
+
test('refreshes Protocol V2 discovery state without legacy-specific options', async () => {
|
|
42
42
|
const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
|
|
43
43
|
const run = jest.fn(async (callback: () => Promise<void>) => callback());
|
|
44
44
|
const device = {
|
|
@@ -49,21 +49,17 @@ describe('DevicePool state lifecycle', () => {
|
|
|
49
49
|
|
|
50
50
|
await (DevicePool as any)._refreshRuntimeState(device, {
|
|
51
51
|
refreshRuntimeState: true,
|
|
52
|
-
allowLegacyProtocolV2ProtocolInfo: true,
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
expect(run).toHaveBeenCalledWith(expect.any(Function), {
|
|
56
55
|
connectProtocol: undefined,
|
|
57
56
|
forceProtocolDetection: undefined,
|
|
58
|
-
allowLegacyProtocolV2ProtocolInfo: true,
|
|
59
57
|
});
|
|
60
58
|
expect(getDeviceState).toHaveBeenNthCalledWith(1, {
|
|
61
59
|
refreshSections: ['status'],
|
|
62
|
-
allowLegacyProtocolV2ProtocolInfo: true,
|
|
63
60
|
});
|
|
64
61
|
expect(getDeviceState).toHaveBeenNthCalledWith(2, {
|
|
65
62
|
refreshSections: ['settings'],
|
|
66
|
-
allowLegacyProtocolV2ProtocolInfo: true,
|
|
67
63
|
});
|
|
68
64
|
});
|
|
69
65
|
|
|
@@ -67,7 +67,6 @@ describe('Pro 2 device settings options', () => {
|
|
|
67
67
|
{ label: '2 minutes', valueMs: 120_000 },
|
|
68
68
|
{ label: '5 minutes', valueMs: 300_000 },
|
|
69
69
|
{ label: '10 minutes', valueMs: 600_000 },
|
|
70
|
-
{ label: '30 minutes', valueMs: 1_800_000 },
|
|
71
70
|
{ label: 'Never', valueMs: PROTOCOL_V2_NEVER_TIMEOUT_MS },
|
|
72
71
|
]);
|
|
73
72
|
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import FirmwareUpdateV4 from '../../src/api/FirmwareUpdateV4';
|
|
4
|
+
import { DataManager } from '../../src/data-manager';
|
|
5
|
+
|
|
6
|
+
import type { Device } from '../../src/device/Device';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../src/data/config', () => ({
|
|
9
|
+
DEFAULT_DOMAIN: 'https://example.com/',
|
|
10
|
+
getSDKVersion: () => '0.0.0-test',
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('FirmwareUpdateV4 install polling', () => {
|
|
14
|
+
let getSettingsSpy: jest.SpyInstance;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getSettingsSpy = jest
|
|
18
|
+
.spyOn(DataManager, 'getSettings')
|
|
19
|
+
.mockReturnValue('react-native' as never);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
jest.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('polls status instead of replaying install after React Native BLE releases', async () => {
|
|
27
|
+
const method = new FirmwareUpdateV4({
|
|
28
|
+
id: 1,
|
|
29
|
+
payload: {
|
|
30
|
+
method: 'firmwareUpdateV4',
|
|
31
|
+
connectId: 'pro2-ble',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
|
|
35
|
+
const typedCall = jest
|
|
36
|
+
.fn()
|
|
37
|
+
.mockRejectedValueOnce(new Error('React Native BLE transport released'))
|
|
38
|
+
.mockResolvedValueOnce({
|
|
39
|
+
message: {
|
|
40
|
+
records: [
|
|
41
|
+
{
|
|
42
|
+
target_id: 4,
|
|
43
|
+
status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
|
|
44
|
+
path: 'vol0:/application_p1.bin',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
method.device = {
|
|
51
|
+
getCommands: () => ({ typedCall }),
|
|
52
|
+
} as unknown as Device;
|
|
53
|
+
method.postTipMessage = jest.fn();
|
|
54
|
+
method.postProgressMessage = jest.fn();
|
|
55
|
+
|
|
56
|
+
const firmwareUpdate = method as unknown as {
|
|
57
|
+
protocolV2StartFirmwareUpdate: (params: { targets: typeof targets }) => Promise<unknown>;
|
|
58
|
+
waitForProtocolV2FirmwareUpdateComplete: (value: typeof targets) => Promise<void>;
|
|
59
|
+
reconnectProtocolV2Device: () => Promise<void>;
|
|
60
|
+
verifyProtocolV2ReconnectIdentity: () => Promise<Record<string, never>>;
|
|
61
|
+
};
|
|
62
|
+
firmwareUpdate.reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
63
|
+
firmwareUpdate.verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue({});
|
|
64
|
+
|
|
65
|
+
await expect(
|
|
66
|
+
firmwareUpdate.protocolV2StartFirmwareUpdate({ targets })
|
|
67
|
+
).resolves.toBeUndefined();
|
|
68
|
+
await expect(
|
|
69
|
+
firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets)
|
|
70
|
+
).resolves.toBeUndefined();
|
|
71
|
+
|
|
72
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
73
|
+
expect(typedCall.mock.calls[0]?.[0]).toBe('DeviceFirmwareUpdateRequest');
|
|
74
|
+
expect(typedCall.mock.calls[1]?.[0]).toBe('DeviceFirmwareUpdateStatusGet');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('does not treat an explicit workflow cancellation as an install reboot', async () => {
|
|
78
|
+
const method = new FirmwareUpdateV4({
|
|
79
|
+
id: 1,
|
|
80
|
+
payload: {
|
|
81
|
+
method: 'firmwareUpdateV4',
|
|
82
|
+
connectId: 'pro2-ble',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
|
|
86
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
87
|
+
const abortController = new AbortController();
|
|
88
|
+
abortController.abort();
|
|
89
|
+
|
|
90
|
+
method.abortSignal = abortController.signal;
|
|
91
|
+
method.device = {
|
|
92
|
+
getCommands: () => ({ typedCall }),
|
|
93
|
+
} as unknown as Device;
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
(
|
|
97
|
+
method as unknown as {
|
|
98
|
+
protocolV2StartFirmwareUpdate: (params: { targets: typeof targets }) => Promise<unknown>;
|
|
99
|
+
}
|
|
100
|
+
).protocolV2StartFirmwareUpdate({ targets })
|
|
101
|
+
).rejects.toMatchObject({
|
|
102
|
+
errorCode: HardwareErrorCode.CallQueueActionCancelled,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test.each([
|
|
109
|
+
['webusb', 'React Native BLE transport released'],
|
|
110
|
+
['react-native', 'Connection error has occured: Device disconnected'],
|
|
111
|
+
])(
|
|
112
|
+
'requires both a BLE environment and an explicit transport release signal: %s / %s',
|
|
113
|
+
async (env, message) => {
|
|
114
|
+
getSettingsSpy.mockReturnValue(env);
|
|
115
|
+
const method = new FirmwareUpdateV4({
|
|
116
|
+
id: 1,
|
|
117
|
+
payload: {
|
|
118
|
+
method: 'firmwareUpdateV4',
|
|
119
|
+
connectId: 'pro2-device',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
|
|
123
|
+
const typedCall = jest.fn().mockRejectedValue(new Error(message));
|
|
124
|
+
|
|
125
|
+
method.device = {
|
|
126
|
+
getCommands: () => ({ typedCall }),
|
|
127
|
+
} as unknown as Device;
|
|
128
|
+
method.postTipMessage = jest.fn();
|
|
129
|
+
method.postProgressMessage = jest.fn();
|
|
130
|
+
|
|
131
|
+
await expect(
|
|
132
|
+
(
|
|
133
|
+
method as unknown as {
|
|
134
|
+
protocolV2StartFirmwareUpdate: (params: {
|
|
135
|
+
targets: typeof targets;
|
|
136
|
+
}) => Promise<unknown>;
|
|
137
|
+
}
|
|
138
|
+
).protocolV2StartFirmwareUpdate({ targets })
|
|
139
|
+
).rejects.toThrow(message);
|
|
140
|
+
|
|
141
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
142
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
});
|
|
@@ -252,56 +252,6 @@ describe('getDeviceState', () => {
|
|
|
252
252
|
}
|
|
253
253
|
);
|
|
254
254
|
|
|
255
|
-
test.each([
|
|
256
|
-
[EDeviceType.Pro2, DeviceType.PRO2],
|
|
257
|
-
[EDeviceType.Neo, DeviceType.NEO],
|
|
258
|
-
] as const)('invalidates cached loader state when %s is cancelled', async (deviceType, type) => {
|
|
259
|
-
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
260
|
-
if (requestType === 'DeviceInfoGet') {
|
|
261
|
-
return {
|
|
262
|
-
message: {
|
|
263
|
-
protocol_version: 2,
|
|
264
|
-
hw: { Device_type: type, serial_no: 'SERIAL-1' },
|
|
265
|
-
fw: { application: { version: '5.0.0' } },
|
|
266
|
-
},
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
270
|
-
return { message: protocolV2ApplicationInfo };
|
|
271
|
-
}
|
|
272
|
-
if (requestType === 'DeviceStatusGet') {
|
|
273
|
-
return {
|
|
274
|
-
message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
278
|
-
});
|
|
279
|
-
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
280
|
-
const device = createV2Device(typedCall);
|
|
281
|
-
(device as any).commands = { cancel, typedCall };
|
|
282
|
-
device.updateState(
|
|
283
|
-
{
|
|
284
|
-
protocol: 'V2',
|
|
285
|
-
identity: { deviceType },
|
|
286
|
-
status: { mode: 'bootloader' },
|
|
287
|
-
raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo('bootloader') },
|
|
288
|
-
},
|
|
289
|
-
'initialize'
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
await device.interruptionFromUser();
|
|
293
|
-
await device.initialize();
|
|
294
|
-
|
|
295
|
-
expect(cancel).toHaveBeenCalledTimes(1);
|
|
296
|
-
expect(device.state?.status.mode).toBe('normal');
|
|
297
|
-
expect(device.state?.identity.deviceId).toBe('wallet-1');
|
|
298
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
299
|
-
'DeviceInfoGet',
|
|
300
|
-
'ProtocolInfoRequest',
|
|
301
|
-
'DeviceStatusGet',
|
|
302
|
-
]);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
255
|
test.each(['bootloader', 'romloader'] as const)(
|
|
306
256
|
'keeps live %s mode after refreshing cached loader state',
|
|
307
257
|
async mode => {
|
|
@@ -104,7 +104,19 @@ describe('getLogBlockLabel', () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio'
|
|
107
|
+
it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio'])(
|
|
108
|
+
'skips large Base64 resource payload logging for %s',
|
|
109
|
+
method => {
|
|
110
|
+
const request = { method, path: 'resource.bin', data: 'A'.repeat(1024 * 1024) };
|
|
111
|
+
expect(getLogBlockLabel(request)).toBe(method);
|
|
112
|
+
expect(getSafeLogPayload(request, method)).toEqual({
|
|
113
|
+
method,
|
|
114
|
+
payload: '[REDACTED]',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
it.each(['fileWrite', 'fileRead'])(
|
|
108
120
|
'keeps metadata and replaces binary payloads with their size for %s',
|
|
109
121
|
method => {
|
|
110
122
|
const request = { method, path: 'resource.bin', data: new Uint8Array(1024) };
|