@onekeyfe/hd-core 1.2.0-alpha.134 → 1.2.0-alpha.135
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 +8 -1
- package/__tests__/device-lifecycle-events.test.ts +95 -2
- package/__tests__/device-settings.test.ts +167 -9
- package/__tests__/device-wallet-session-store.test.ts +21 -79
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +37 -1
- package/__tests__/protocol-v2-unlock-policy.test.ts +59 -0
- package/__tests__/protocol-v2.test.ts +86 -21
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts +1 -0
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +2 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +296 -247
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +25 -68
- package/src/api/device/DeviceChangePin.ts +4 -1
- package/src/api/device/DeviceSettings.ts +2 -16
- package/src/api/device/DeviceVerify.ts +9 -0
- package/src/api/device/DeviceWipe.ts +4 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +11 -0
- package/src/core/index.ts +67 -25
- package/src/device/Device.ts +45 -42
- package/src/device/DeviceCommands.ts +15 -2
|
@@ -485,7 +485,7 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
485
485
|
});
|
|
486
486
|
});
|
|
487
487
|
|
|
488
|
-
it.each(['Cancelled on device', 'Confirm dismissed'])(
|
|
488
|
+
it.each(['Cancelled on device', 'Confirm dismissed', 'Update cancelled'])(
|
|
489
489
|
'maps legacy Protocol V2 cancellation message "%s" without a subcode',
|
|
490
490
|
async message => {
|
|
491
491
|
const commands = createCommands();
|
|
@@ -800,7 +800,13 @@ describe('DeviceCommands cancellation', () => {
|
|
|
800
800
|
try {
|
|
801
801
|
const commands = createCommands();
|
|
802
802
|
commands.disposed = false;
|
|
803
|
+
commands.mainId = 'main-id';
|
|
803
804
|
commands.callPromise = new Promise(() => {});
|
|
805
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
806
|
+
commands.transport = {
|
|
807
|
+
name: 'ReactNativeBleTransport',
|
|
808
|
+
disconnect,
|
|
809
|
+
} as any;
|
|
804
810
|
const dispose = jest.fn().mockResolvedValue(undefined);
|
|
805
811
|
commands.dispose = dispose;
|
|
806
812
|
|
|
@@ -810,6 +816,7 @@ describe('DeviceCommands cancellation', () => {
|
|
|
810
816
|
|
|
811
817
|
await expect(cancellation).resolves.toBeUndefined();
|
|
812
818
|
expect(dispose).toHaveBeenCalledWith(true);
|
|
819
|
+
expect(disconnect).toHaveBeenCalledWith('main-id');
|
|
813
820
|
expect(commands.callPromise).toBeUndefined();
|
|
814
821
|
} finally {
|
|
815
822
|
jest.useRealTimers();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { EDeviceType, ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
2
2
|
import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
|
-
import { initConnector, initCore } from '../src/core';
|
|
4
|
+
import { initConnector, initCore, isMissingDetectedProtocolV2Error } 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';
|
|
@@ -82,6 +82,23 @@ describe('public device lifecycle events', () => {
|
|
|
82
82
|
expect(DevicePool.emitter.listenerCount(DEVICE.DISCONNECT)).toBe(1);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test('isolates pending cancellation cleanup by connect id', () => {
|
|
86
|
+
core = initCore();
|
|
87
|
+
const context = (core as any).getCoreContext();
|
|
88
|
+
const firstCleanup = createDeferred<void>();
|
|
89
|
+
const replacementCleanup = createDeferred<void>();
|
|
90
|
+
|
|
91
|
+
context.setPrePendingCallPromise('device-a', firstCleanup.promise);
|
|
92
|
+
|
|
93
|
+
expect(context.getPrePendingCallPromise('device-a')).toBe(firstCleanup.promise);
|
|
94
|
+
expect(context.getPrePendingCallPromise('device-b')).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
context.setPrePendingCallPromise('device-a', replacementCleanup.promise);
|
|
97
|
+
context.removePrePendingCallPromise('device-a', firstCleanup.promise);
|
|
98
|
+
|
|
99
|
+
expect(context.getPrePendingCallPromise('device-a')).toBe(replacementCleanup.promise);
|
|
100
|
+
});
|
|
101
|
+
|
|
85
102
|
test('keeps shared device lifecycle listeners across a device cache reset', () => {
|
|
86
103
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
87
104
|
core = initCore();
|
|
@@ -234,6 +251,22 @@ describe('public device lifecycle events', () => {
|
|
|
234
251
|
expect(device.getProtocol()).toBe('V2');
|
|
235
252
|
});
|
|
236
253
|
|
|
254
|
+
test.each([
|
|
255
|
+
['V2', true],
|
|
256
|
+
['V1', false],
|
|
257
|
+
] as const)(
|
|
258
|
+
'retries a missing detected protocol only for an explicit Protocol %s connection',
|
|
259
|
+
(connectProtocol, expected) => {
|
|
260
|
+
const method = { payload: { connectProtocol } } as never;
|
|
261
|
+
const error = {
|
|
262
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
263
|
+
message: 'Device protocol has not been detected for ble-id',
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
expect(isMissingDetectedProtocolV2Error(method, error)).toBe(expected);
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
237
270
|
test('converts an internal transport disconnect into a public KnownDevice snapshot', () => {
|
|
238
271
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
239
272
|
core = initCore();
|
|
@@ -310,6 +343,66 @@ describe('public device lifecycle events', () => {
|
|
|
310
343
|
}
|
|
311
344
|
);
|
|
312
345
|
|
|
346
|
+
test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
|
|
347
|
+
'sends a fallback Cancel for an acquired Protocol V2 %s call without a prompt callback',
|
|
348
|
+
async env => {
|
|
349
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
350
|
+
const device = createInitializedDevice('V2');
|
|
351
|
+
const post = jest.fn().mockResolvedValue(undefined);
|
|
352
|
+
const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
|
|
353
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
354
|
+
device.originalDescriptor.session = device.mainId;
|
|
355
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
356
|
+
device.commands = {
|
|
357
|
+
transport: { post },
|
|
358
|
+
cancelDevice,
|
|
359
|
+
cancel,
|
|
360
|
+
} as never;
|
|
361
|
+
|
|
362
|
+
await device.interruptionFromUser();
|
|
363
|
+
|
|
364
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
365
|
+
expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
|
|
366
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
test('waits for the canceled run to finish releasing before cancellation completes', async () => {
|
|
371
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
372
|
+
const device = createInitializedDevice('V2');
|
|
373
|
+
const operation = createDeferred<void>();
|
|
374
|
+
const releaseGate = createDeferred<void>();
|
|
375
|
+
const cancelError = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
376
|
+
const release = jest.spyOn(device, 'release').mockImplementation(() => releaseGate.promise);
|
|
377
|
+
device.commands = {
|
|
378
|
+
disposed: false,
|
|
379
|
+
cancel: jest.fn(() => {
|
|
380
|
+
operation.reject(cancelError);
|
|
381
|
+
return Promise.resolve();
|
|
382
|
+
}),
|
|
383
|
+
} as never;
|
|
384
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
385
|
+
|
|
386
|
+
const runResult = device.run(() => operation.promise).catch(error => error);
|
|
387
|
+
const cancellation = device.interruptionFromUser();
|
|
388
|
+
let cancellationCompleted = false;
|
|
389
|
+
cancellation.then(() => {
|
|
390
|
+
cancellationCompleted = true;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await new Promise(resolve => {
|
|
394
|
+
setImmediate(resolve);
|
|
395
|
+
});
|
|
396
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
397
|
+
expect(cancellationCompleted).toBe(false);
|
|
398
|
+
|
|
399
|
+
releaseGate.resolve();
|
|
400
|
+
await cancellation;
|
|
401
|
+
await expect(runResult).resolves.toMatchObject({
|
|
402
|
+
errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
313
406
|
test.each([
|
|
314
407
|
[EDeviceType.Pro2, 'webusb', false, DeviceType.PRO2],
|
|
315
408
|
[EDeviceType.Neo, 'webusb', false, DeviceType.NEO],
|
|
@@ -2,6 +2,9 @@ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
|
2
2
|
import { DeviceSettingsPage } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
4
|
import DeviceSettings from '../src/api/device/DeviceSettings';
|
|
5
|
+
import { Device } from '../src/device/Device';
|
|
6
|
+
import { DEVICE } from '../src/events';
|
|
7
|
+
import { PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE } from '../src/protocols/protocol-v2';
|
|
5
8
|
import { DEVICE_SETTINGS_NEVER_TIMEOUT_MS } from '../src/utils/deviceSettings';
|
|
6
9
|
|
|
7
10
|
import type { Features } from '../src/types';
|
|
@@ -21,6 +24,9 @@ function createDevice({ protocol }: { protocol: 'V1' | 'V2' }) {
|
|
|
21
24
|
const typedCall = jest.fn().mockResolvedValue({ message: { message: 'Success' } });
|
|
22
25
|
const updateState = jest.fn();
|
|
23
26
|
const getDeviceState = jest.fn();
|
|
27
|
+
const refreshProtocolV2SettingsAfterMutation = jest.fn(() =>
|
|
28
|
+
getDeviceState({ refreshSections: ['status', 'settings'] })
|
|
29
|
+
);
|
|
24
30
|
return {
|
|
25
31
|
device: {
|
|
26
32
|
features,
|
|
@@ -29,6 +35,7 @@ function createDevice({ protocol }: { protocol: 'V1' | 'V2' }) {
|
|
|
29
35
|
commands: { typedCall },
|
|
30
36
|
updateState,
|
|
31
37
|
getDeviceState,
|
|
38
|
+
refreshProtocolV2SettingsAfterMutation,
|
|
32
39
|
},
|
|
33
40
|
typedCall,
|
|
34
41
|
updateState,
|
|
@@ -181,7 +188,98 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
181
188
|
expect(updateState).not.toHaveBeenCalled();
|
|
182
189
|
});
|
|
183
190
|
|
|
184
|
-
it('
|
|
191
|
+
it('reconciles stale passphrase and label after an unrelated Protocol V2 setting change', async () => {
|
|
192
|
+
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
193
|
+
if (requestType === 'DeviceSettingsSet') {
|
|
194
|
+
return { message: { message: 'Success' } };
|
|
195
|
+
}
|
|
196
|
+
if (requestType === 'DeviceStatusGet') {
|
|
197
|
+
return {
|
|
198
|
+
message: {
|
|
199
|
+
init_states: true,
|
|
200
|
+
unlocked: false,
|
|
201
|
+
passphrase_enabled: false,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (requestType === 'DeviceSettingsGet') {
|
|
206
|
+
return {
|
|
207
|
+
message: {
|
|
208
|
+
label: 'Current Label',
|
|
209
|
+
brightness: 80,
|
|
210
|
+
passphrase_enable: false,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
215
|
+
});
|
|
216
|
+
const device = Device.fromDescriptor({
|
|
217
|
+
id: 'pro2',
|
|
218
|
+
path: 'pro2',
|
|
219
|
+
protocolType: 'V2',
|
|
220
|
+
} as never);
|
|
221
|
+
(device as any).commands = { typedCall };
|
|
222
|
+
device.updateState(
|
|
223
|
+
{
|
|
224
|
+
protocol: 'V2',
|
|
225
|
+
identity: {
|
|
226
|
+
deviceType: EDeviceType.Pro2,
|
|
227
|
+
label: 'Stale Label',
|
|
228
|
+
},
|
|
229
|
+
status: {
|
|
230
|
+
mode: 'normal',
|
|
231
|
+
unlocked: false,
|
|
232
|
+
passphraseProtection: true,
|
|
233
|
+
},
|
|
234
|
+
settings: { brightness: 20 },
|
|
235
|
+
raw: {
|
|
236
|
+
protocolV2ProtocolInfo: {
|
|
237
|
+
version: 1,
|
|
238
|
+
build_fingerprint: 'application__5.0.0__abcdef0__PROD__RELEASE',
|
|
239
|
+
supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
'initialize'
|
|
244
|
+
);
|
|
245
|
+
const onState = jest.fn();
|
|
246
|
+
device.on(DEVICE.STATE, onState);
|
|
247
|
+
const method = new DeviceSettings({
|
|
248
|
+
id: 3,
|
|
249
|
+
payload: {
|
|
250
|
+
method: 'deviceSettings',
|
|
251
|
+
brightness: 80,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
method.init();
|
|
255
|
+
(method as any).device = device;
|
|
256
|
+
|
|
257
|
+
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
258
|
+
|
|
259
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
260
|
+
'DeviceSettingsSet',
|
|
261
|
+
'DeviceStatusGet',
|
|
262
|
+
'DeviceSettingsGet',
|
|
263
|
+
]);
|
|
264
|
+
expect(device.state).toMatchObject({
|
|
265
|
+
identity: { label: 'Current Label' },
|
|
266
|
+
status: { passphraseProtection: false },
|
|
267
|
+
settings: { brightness: 80 },
|
|
268
|
+
});
|
|
269
|
+
expect(onState).toHaveBeenLastCalledWith(
|
|
270
|
+
device,
|
|
271
|
+
expect.objectContaining({
|
|
272
|
+
source: 'settings-read',
|
|
273
|
+
state: expect.objectContaining({
|
|
274
|
+
identity: expect.objectContaining({ label: 'Current Label' }),
|
|
275
|
+
status: expect.objectContaining({ passphraseProtection: false }),
|
|
276
|
+
settings: expect.objectContaining({ brightness: 80 }),
|
|
277
|
+
}),
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('uses the Pro2 passphrase page and refreshes device state after the toggle', async () => {
|
|
185
283
|
const { device, typedCall, getDeviceState } = createDevice({ protocol: 'V2' });
|
|
186
284
|
getDeviceState
|
|
187
285
|
.mockResolvedValueOnce({
|
|
@@ -263,8 +361,8 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
263
361
|
expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['settings'] });
|
|
264
362
|
});
|
|
265
363
|
|
|
266
|
-
it('
|
|
267
|
-
const { device, getDeviceState } = createDevice({ protocol: 'V2' });
|
|
364
|
+
it('does not overwrite Pro2 passphrase state when refreshed state does not match', async () => {
|
|
365
|
+
const { device, getDeviceState, updateState } = createDevice({ protocol: 'V2' });
|
|
268
366
|
getDeviceState.mockResolvedValue({
|
|
269
367
|
status: { passphraseProtection: false },
|
|
270
368
|
});
|
|
@@ -278,14 +376,12 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
278
376
|
method.init();
|
|
279
377
|
(method as any).device = device;
|
|
280
378
|
|
|
281
|
-
await expect(method.run()).
|
|
282
|
-
|
|
283
|
-
message: 'Protocol V2 passphrase setting did not reach the requested value.',
|
|
284
|
-
});
|
|
379
|
+
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
380
|
+
expect(updateState).not.toHaveBeenCalled();
|
|
285
381
|
});
|
|
286
382
|
|
|
287
|
-
it('
|
|
288
|
-
const { device, getDeviceState } = createDevice({ protocol: 'V2' });
|
|
383
|
+
it('keeps the previous Pro2 passphrase state when refreshed state is unavailable after locking', async () => {
|
|
384
|
+
const { device, getDeviceState, updateState } = createDevice({ protocol: 'V2' });
|
|
289
385
|
getDeviceState
|
|
290
386
|
.mockResolvedValueOnce({
|
|
291
387
|
status: { unlocked: true, passphraseProtection: true },
|
|
@@ -304,6 +400,68 @@ describe('DeviceSettings protocol routing', () => {
|
|
|
304
400
|
(method as any).device = device;
|
|
305
401
|
|
|
306
402
|
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
403
|
+
expect(updateState).not.toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('preserves confirmed Pro2 passphrase state when the post-toggle status is locked', async () => {
|
|
407
|
+
let statusReadCount = 0;
|
|
408
|
+
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
409
|
+
if (requestType === 'DeviceStatusGet') {
|
|
410
|
+
statusReadCount += 1;
|
|
411
|
+
return {
|
|
412
|
+
message:
|
|
413
|
+
statusReadCount === 1
|
|
414
|
+
? { init_states: true, unlocked: true, passphrase_enabled: true }
|
|
415
|
+
: { init_states: true, unlocked: false, passphrase_enabled: false },
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (requestType === 'DeviceSettingsPageShow') {
|
|
419
|
+
return { message: { message: 'Success' } };
|
|
420
|
+
}
|
|
421
|
+
if (requestType === 'DeviceSettingsGet') {
|
|
422
|
+
return { message: { label: 'Pro 2' } };
|
|
423
|
+
}
|
|
424
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
425
|
+
});
|
|
426
|
+
const device = Device.fromDescriptor({
|
|
427
|
+
id: 'pro2-passphrase',
|
|
428
|
+
path: 'pro2-passphrase',
|
|
429
|
+
protocolType: 'V2',
|
|
430
|
+
} as never);
|
|
431
|
+
(device as any).commands = { typedCall };
|
|
432
|
+
device.updateState(
|
|
433
|
+
{
|
|
434
|
+
protocol: 'V2',
|
|
435
|
+
identity: { deviceType: EDeviceType.Pro2 },
|
|
436
|
+
status: {
|
|
437
|
+
mode: 'normal',
|
|
438
|
+
unlocked: true,
|
|
439
|
+
passphraseProtection: true,
|
|
440
|
+
},
|
|
441
|
+
raw: {
|
|
442
|
+
protocolV2ProtocolInfo: {
|
|
443
|
+
version: 1,
|
|
444
|
+
supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
'initialize'
|
|
449
|
+
);
|
|
450
|
+
const method = new DeviceSettings({
|
|
451
|
+
id: 6,
|
|
452
|
+
payload: {
|
|
453
|
+
method: 'deviceSettings',
|
|
454
|
+
usePassphrase: false,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
method.init();
|
|
458
|
+
(method as any).device = device;
|
|
459
|
+
|
|
460
|
+
await expect(method.run()).resolves.toEqual({ message: 'Success' });
|
|
461
|
+
expect(device.state?.status).toMatchObject({
|
|
462
|
+
unlocked: false,
|
|
463
|
+
passphraseProtection: true,
|
|
464
|
+
});
|
|
307
465
|
});
|
|
308
466
|
|
|
309
467
|
it('keeps Protocol V1 passphrase settings on ApplySettings', async () => {
|
|
@@ -352,7 +352,7 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
352
352
|
jest.restoreAllMocks();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
test('
|
|
355
|
+
test('verifies the live device id without resetting the cached wallet session', async () => {
|
|
356
356
|
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
357
357
|
device.features = {
|
|
358
358
|
protocol: 'V1',
|
|
@@ -363,7 +363,7 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
363
363
|
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
364
364
|
const typedCall = jest.fn().mockResolvedValue({
|
|
365
365
|
type: 'Features',
|
|
366
|
-
message: { device_id: 'live-device-b'
|
|
366
|
+
message: { device_id: 'live-device-b' },
|
|
367
367
|
});
|
|
368
368
|
device.commands = { typedCall } as never;
|
|
369
369
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
@@ -371,76 +371,11 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
371
371
|
await expect(
|
|
372
372
|
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
373
373
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
374
|
-
// Identity is validated from the single Initialize round trip; no separate
|
|
375
|
-
// GetFeatures preflight.
|
|
376
374
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
377
|
-
expect(typedCall).toHaveBeenCalledWith(
|
|
378
|
-
'Initialize',
|
|
379
|
-
'Features',
|
|
380
|
-
expect.objectContaining({ passphrase_state: 'hidden-a' }),
|
|
381
|
-
expect.any(Object)
|
|
382
|
-
);
|
|
383
|
-
// Identity change purges the previous device's cached sessions (pre-existing
|
|
384
|
-
// reconcileDeviceIdentity behavior — never carry sessions across devices)…
|
|
385
|
-
expect(deviceWalletSessionStore.get('cached-device-a', 'hidden-a')).toBeUndefined();
|
|
386
|
-
// …and the session the mismatched Initialize cached under the wrong
|
|
387
|
-
// device's identity is dropped as well.
|
|
388
|
-
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
375
|
+
expect(typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
|
|
389
376
|
});
|
|
390
377
|
|
|
391
|
-
test('
|
|
392
|
-
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
393
|
-
device.features = {
|
|
394
|
-
protocol: 'V1',
|
|
395
|
-
deviceId: 'cached-device-a',
|
|
396
|
-
unlocked: true,
|
|
397
|
-
passphraseProtection: true,
|
|
398
|
-
} as never;
|
|
399
|
-
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
400
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
401
|
-
type: 'Features',
|
|
402
|
-
message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
|
|
403
|
-
});
|
|
404
|
-
device.commands = { typedCall } as never;
|
|
405
|
-
// Pre-encode resync succeeds; the post-response reconfigure inside
|
|
406
|
-
// callInitialize rejects — the session write has already happened by then.
|
|
407
|
-
jest
|
|
408
|
-
.spyOn(TransportManager, 'reconfigure')
|
|
409
|
-
.mockResolvedValueOnce(undefined)
|
|
410
|
-
.mockRejectedValueOnce(new Error('configure failed'));
|
|
411
|
-
|
|
412
|
-
await expect(
|
|
413
|
-
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
414
|
-
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
415
|
-
// The wrong-device session must not survive the error path either.
|
|
416
|
-
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test('re-syncs the V1 message schema for this device before encoding Initialize', async () => {
|
|
420
|
-
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
421
|
-
device.features = {
|
|
422
|
-
protocol: 'V1',
|
|
423
|
-
deviceId: 'device-a',
|
|
424
|
-
unlocked: true,
|
|
425
|
-
passphraseProtection: true,
|
|
426
|
-
} as never;
|
|
427
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
428
|
-
type: 'Features',
|
|
429
|
-
message: { device_id: 'device-a' },
|
|
430
|
-
});
|
|
431
|
-
device.commands = { typedCall } as never;
|
|
432
|
-
const reconfigure = jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
433
|
-
|
|
434
|
-
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
435
|
-
|
|
436
|
-
// A stale process-global schema (from another device) would silently strip
|
|
437
|
-
// passphrase_state from the wire message, so the resync must come first.
|
|
438
|
-
expect(reconfigure.mock.invocationCallOrder[0]).toBeLessThan(
|
|
439
|
-
typedCall.mock.invocationCallOrder[0]
|
|
440
|
-
);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test('resumes a cached V1 wallet with a single identity-validated Initialize', async () => {
|
|
378
|
+
test('resumes a cached V1 wallet after a non-destructive live identity read', async () => {
|
|
444
379
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
445
380
|
device.features = {
|
|
446
381
|
protocol: 'V1',
|
|
@@ -449,17 +384,22 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
449
384
|
passphraseProtection: true,
|
|
450
385
|
} as never;
|
|
451
386
|
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
452
|
-
const typedCall = jest
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
387
|
+
const typedCall = jest
|
|
388
|
+
.fn()
|
|
389
|
+
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
|
|
390
|
+
.mockResolvedValueOnce({
|
|
391
|
+
type: 'Features',
|
|
392
|
+
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
393
|
+
});
|
|
456
394
|
device.commands = { typedCall } as never;
|
|
457
395
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
458
396
|
|
|
459
397
|
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
460
398
|
|
|
461
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
462
|
-
expect(typedCall).
|
|
399
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
400
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
|
|
401
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
402
|
+
2,
|
|
463
403
|
'Initialize',
|
|
464
404
|
'Features',
|
|
465
405
|
expect.objectContaining({
|
|
@@ -468,10 +408,9 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
468
408
|
}),
|
|
469
409
|
expect.any(Object)
|
|
470
410
|
);
|
|
471
|
-
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
472
411
|
});
|
|
473
412
|
|
|
474
|
-
test('selects the standard V1 wallet
|
|
413
|
+
test('selects the standard V1 wallet after a non-destructive live identity read', async () => {
|
|
475
414
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
476
415
|
device.features = {
|
|
477
416
|
protocol: 'V1',
|
|
@@ -481,14 +420,17 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
481
420
|
} as never;
|
|
482
421
|
const typedCall = jest
|
|
483
422
|
.fn()
|
|
423
|
+
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
|
|
484
424
|
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
|
|
485
425
|
device.commands = { typedCall } as never;
|
|
486
426
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
487
427
|
|
|
488
428
|
await device.initialize({ deviceId: 'device-a' });
|
|
489
429
|
|
|
490
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
491
|
-
expect(typedCall).
|
|
430
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
431
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
|
|
432
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
433
|
+
2,
|
|
492
434
|
'Initialize',
|
|
493
435
|
'Features',
|
|
494
436
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import FirmwareUpdateV4 from '../../src/api/FirmwareUpdateV4';
|
|
4
4
|
|
|
@@ -127,4 +127,40 @@ describe('FirmwareUpdateV4 install polling', () => {
|
|
|
127
127
|
errorCode: HardwareErrorCode.CallQueueActionCancelled,
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
test('stops polling when the device cancels firmware installation', async () => {
|
|
132
|
+
const method = new FirmwareUpdateV4({
|
|
133
|
+
id: 1,
|
|
134
|
+
payload: {
|
|
135
|
+
method: 'firmwareUpdateV4',
|
|
136
|
+
connectId: 'pro2-ble',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const typedCall = jest
|
|
140
|
+
.fn()
|
|
141
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
|
|
142
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
143
|
+
|
|
144
|
+
method.device = {
|
|
145
|
+
getCommands: () => ({ typedCall }),
|
|
146
|
+
} as unknown as Device;
|
|
147
|
+
const firmwareUpdate = method as unknown as {
|
|
148
|
+
waitForProtocolV2FirmwareUpdateComplete: (
|
|
149
|
+
targets: Array<{ target_id: number; path: string }>
|
|
150
|
+
) => Promise<void>;
|
|
151
|
+
reconnectProtocolV2Device: () => Promise<void>;
|
|
152
|
+
};
|
|
153
|
+
firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete([
|
|
157
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
158
|
+
])
|
|
159
|
+
).rejects.toMatchObject({
|
|
160
|
+
errorCode: HardwareErrorCode.ActionCancelled,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
130
166
|
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
2
2
|
|
|
3
3
|
import ConfluxSignMessageCIP23 from '../src/api/conflux/ConfluxSignMessageCIP23';
|
|
4
|
+
import DeviceChangePin from '../src/api/device/DeviceChangePin';
|
|
4
5
|
import DeviceLock from '../src/api/device/DeviceLock';
|
|
5
6
|
import DeviceSettings from '../src/api/device/DeviceSettings';
|
|
7
|
+
import DeviceVerify from '../src/api/device/DeviceVerify';
|
|
8
|
+
import DeviceWipe from '../src/api/device/DeviceWipe';
|
|
9
|
+
import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4';
|
|
6
10
|
import OpenWalletSession from '../src/api/OpenWalletSession';
|
|
7
11
|
import { runMethodWithUnlockPolicy } from '../src/protocols/protocol-v2/unlockPolicyRunner';
|
|
8
12
|
|
|
@@ -191,6 +195,61 @@ describe('Protocol V2 unlock semantics', () => {
|
|
|
191
195
|
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, expect.any(Object));
|
|
192
196
|
});
|
|
193
197
|
|
|
198
|
+
test.each([
|
|
199
|
+
[
|
|
200
|
+
'PIN changes',
|
|
201
|
+
() =>
|
|
202
|
+
new DeviceChangePin({
|
|
203
|
+
id: 1,
|
|
204
|
+
payload: { method: 'deviceChangePin', remove: false },
|
|
205
|
+
}),
|
|
206
|
+
],
|
|
207
|
+
['device wipe', () => new DeviceWipe({ id: 1, payload: { method: 'deviceWipe' } })],
|
|
208
|
+
[
|
|
209
|
+
'firmware updates',
|
|
210
|
+
() =>
|
|
211
|
+
new FirmwareUpdateV4({
|
|
212
|
+
id: 1,
|
|
213
|
+
payload: { method: 'firmwareUpdateV4', platform: 'desktop' } as any,
|
|
214
|
+
}),
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
'genuine-device verification',
|
|
218
|
+
() =>
|
|
219
|
+
new DeviceVerify({
|
|
220
|
+
id: 1,
|
|
221
|
+
payload: { method: 'deviceVerify', dataHex: '00' },
|
|
222
|
+
}),
|
|
223
|
+
],
|
|
224
|
+
])('allows either PIN type when pre-unlocking %s', async (_name, createMethod) => {
|
|
225
|
+
const method = createMethod();
|
|
226
|
+
method.init();
|
|
227
|
+
const features = { unlocked: false };
|
|
228
|
+
const device = {
|
|
229
|
+
features,
|
|
230
|
+
commands: {
|
|
231
|
+
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
|
|
232
|
+
},
|
|
233
|
+
isProtocolV2: () => true,
|
|
234
|
+
isBootloader: () => false,
|
|
235
|
+
isRomloader: () => false,
|
|
236
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
237
|
+
unlockDevice: jest.fn().mockImplementation(() => {
|
|
238
|
+
features.unlocked = true;
|
|
239
|
+
return Promise.resolve(features);
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
method.run = jest.fn().mockResolvedValue({ message: 'ok' });
|
|
243
|
+
|
|
244
|
+
await expect(runMethodWithUnlockPolicy(method, device as any)).resolves.toEqual({
|
|
245
|
+
message: 'ok',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
249
|
+
expect(method.getSupportedProtocols()).toContain('V2');
|
|
250
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, expect.any(Object));
|
|
251
|
+
});
|
|
252
|
+
|
|
194
253
|
test('pre-unlocks a locked standard wallet before wallet-session preparation', async () => {
|
|
195
254
|
const calls: string[] = [];
|
|
196
255
|
const features = {
|