@onekeyfe/hd-core 1.2.0-alpha.135 → 1.2.0-alpha.137
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 +1 -1
- package/__tests__/device-lifecycle-events.test.ts +22 -24
- package/__tests__/device-wallet-session-store.test.ts +147 -21
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +1 -37
- package/__tests__/protocol-v2-unlock-policy.test.ts +0 -59
- package/__tests__/protocol-v2.test.ts +20 -52
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts +0 -1
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +68 -39
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +68 -25
- package/src/api/device/DeviceChangePin.ts +1 -4
- package/src/api/device/DeviceVerify.ts +0 -9
- package/src/api/device/DeviceWipe.ts +1 -4
- package/src/device/Device.ts +57 -14
- package/src/device/DeviceCommands.ts +1 -1
- package/src/device/DeviceConnector.ts +4 -1
|
@@ -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'])(
|
|
489
489
|
'maps legacy Protocol V2 cancellation message "%s" without a subcode',
|
|
490
490
|
async message => {
|
|
491
491
|
const commands = createCommands();
|
|
@@ -128,7 +128,7 @@ describe('public device lifecycle events', () => {
|
|
|
128
128
|
|
|
129
129
|
await device.acquire();
|
|
130
130
|
|
|
131
|
-
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined);
|
|
131
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined, undefined);
|
|
132
132
|
expect(device.getProtocol()).toBe('V1');
|
|
133
133
|
expect(device.originalDescriptor.protocolType).toBe('V1');
|
|
134
134
|
});
|
|
@@ -146,7 +146,9 @@ describe('public device lifecycle events', () => {
|
|
|
146
146
|
|
|
147
147
|
await device.acquire(undefined, { forceProtocolDetection: true });
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
// Explicit active detection forwards forceProtocolDetection so the
|
|
150
|
+
// transport bypasses its protocol cache.
|
|
151
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, undefined, true);
|
|
150
152
|
expect(device.getProtocol()).toBe('V2');
|
|
151
153
|
expect(device.originalDescriptor.protocolType).toBe('V2');
|
|
152
154
|
});
|
|
@@ -247,7 +249,7 @@ describe('public device lifecycle events', () => {
|
|
|
247
249
|
|
|
248
250
|
await device.acquire('V2');
|
|
249
251
|
|
|
250
|
-
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined);
|
|
252
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined, undefined);
|
|
251
253
|
expect(device.getProtocol()).toBe('V2');
|
|
252
254
|
});
|
|
253
255
|
|
|
@@ -343,29 +345,25 @@ describe('public device lifecycle events', () => {
|
|
|
343
345
|
}
|
|
344
346
|
);
|
|
345
347
|
|
|
346
|
-
test
|
|
347
|
-
'
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
cancelDevice,
|
|
359
|
-
cancel,
|
|
360
|
-
} as never;
|
|
348
|
+
test('sends a fallback Cancel for an acquired Protocol V2 BLE call without a prompt callback', async () => {
|
|
349
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' 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 as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
355
|
+
device.commands = {
|
|
356
|
+
transport: { post },
|
|
357
|
+
cancelDevice,
|
|
358
|
+
cancel,
|
|
359
|
+
} as never;
|
|
361
360
|
|
|
362
|
-
|
|
361
|
+
await device.interruptionFromUser();
|
|
363
362
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
363
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
364
|
+
expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
|
|
365
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
366
|
+
});
|
|
369
367
|
|
|
370
368
|
test('waits for the canceled run to finish releasing before cancellation completes', async () => {
|
|
371
369
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
@@ -352,7 +352,75 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
352
352
|
jest.restoreAllMocks();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
test('
|
|
355
|
+
test('sends no wallet context before identity is proven on first contact', async () => {
|
|
356
|
+
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
357
|
+
// No cached features: there is no local evidence which device sits here.
|
|
358
|
+
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
359
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
360
|
+
type: 'Features',
|
|
361
|
+
message: { device_id: 'intruder-device', session_id: 'intruder-session' },
|
|
362
|
+
});
|
|
363
|
+
device.commands = { typedCall } as never;
|
|
364
|
+
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
365
|
+
|
|
366
|
+
await expect(
|
|
367
|
+
device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' })
|
|
368
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
369
|
+
|
|
370
|
+
// Exactly one context-free Initialize: neither the cached session_id nor
|
|
371
|
+
// the passphrase_state may reach an unproven device.
|
|
372
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
373
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
374
|
+
'Initialize',
|
|
375
|
+
'Features',
|
|
376
|
+
{ is_contains_attach: true },
|
|
377
|
+
expect.any(Object)
|
|
378
|
+
);
|
|
379
|
+
// The expected device's cached session is untouched.
|
|
380
|
+
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('proves identity first, then resumes the wallet session on first contact', async () => {
|
|
384
|
+
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
385
|
+
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
386
|
+
const typedCall = jest
|
|
387
|
+
.fn()
|
|
388
|
+
.mockResolvedValueOnce({
|
|
389
|
+
type: 'Features',
|
|
390
|
+
message: { device_id: 'device-a', session_id: 'standard-session' },
|
|
391
|
+
})
|
|
392
|
+
.mockResolvedValueOnce({
|
|
393
|
+
type: 'Features',
|
|
394
|
+
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
395
|
+
});
|
|
396
|
+
device.commands = { typedCall } as never;
|
|
397
|
+
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
398
|
+
|
|
399
|
+
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
400
|
+
|
|
401
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
402
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
403
|
+
1,
|
|
404
|
+
'Initialize',
|
|
405
|
+
'Features',
|
|
406
|
+
{ is_contains_attach: true },
|
|
407
|
+
expect.any(Object)
|
|
408
|
+
);
|
|
409
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
410
|
+
2,
|
|
411
|
+
'Initialize',
|
|
412
|
+
'Features',
|
|
413
|
+
expect.objectContaining({
|
|
414
|
+
session_id: 'session-a',
|
|
415
|
+
passphrase_state: 'hidden-a',
|
|
416
|
+
}),
|
|
417
|
+
expect.any(Object)
|
|
418
|
+
);
|
|
419
|
+
// The context-free identity call must not overwrite the cached session.
|
|
420
|
+
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('purges cached sessions and rejects when the live device identity changes', async () => {
|
|
356
424
|
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
357
425
|
device.features = {
|
|
358
426
|
protocol: 'V1',
|
|
@@ -363,7 +431,7 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
363
431
|
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
364
432
|
const typedCall = jest.fn().mockResolvedValue({
|
|
365
433
|
type: 'Features',
|
|
366
|
-
message: { device_id: 'live-device-b' },
|
|
434
|
+
message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
|
|
367
435
|
});
|
|
368
436
|
device.commands = { typedCall } as never;
|
|
369
437
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
@@ -371,11 +439,76 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
371
439
|
await expect(
|
|
372
440
|
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
373
441
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
442
|
+
// Identity is validated from the single Initialize round trip; no separate
|
|
443
|
+
// GetFeatures preflight.
|
|
374
444
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
375
|
-
expect(typedCall).toHaveBeenCalledWith(
|
|
445
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
446
|
+
'Initialize',
|
|
447
|
+
'Features',
|
|
448
|
+
expect.objectContaining({ passphrase_state: 'hidden-a' }),
|
|
449
|
+
expect.any(Object)
|
|
450
|
+
);
|
|
451
|
+
// Identity change purges the previous device's cached sessions (pre-existing
|
|
452
|
+
// reconcileDeviceIdentity behavior — never carry sessions across devices)…
|
|
453
|
+
expect(deviceWalletSessionStore.get('cached-device-a', 'hidden-a')).toBeUndefined();
|
|
454
|
+
// …and the session the mismatched Initialize cached under the wrong
|
|
455
|
+
// device's identity is dropped as well.
|
|
456
|
+
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
376
457
|
});
|
|
377
458
|
|
|
378
|
-
test('
|
|
459
|
+
test('drops the wrong-device session even when reconfigure fails after Initialize', async () => {
|
|
460
|
+
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
461
|
+
device.features = {
|
|
462
|
+
protocol: 'V1',
|
|
463
|
+
deviceId: 'cached-device-a',
|
|
464
|
+
unlocked: true,
|
|
465
|
+
passphraseProtection: true,
|
|
466
|
+
} as never;
|
|
467
|
+
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
468
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
469
|
+
type: 'Features',
|
|
470
|
+
message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
|
|
471
|
+
});
|
|
472
|
+
device.commands = { typedCall } as never;
|
|
473
|
+
// Pre-encode resync succeeds; the post-response reconfigure inside
|
|
474
|
+
// callInitialize rejects — the session write has already happened by then.
|
|
475
|
+
jest
|
|
476
|
+
.spyOn(TransportManager, 'reconfigure')
|
|
477
|
+
.mockResolvedValueOnce(undefined)
|
|
478
|
+
.mockRejectedValueOnce(new Error('configure failed'));
|
|
479
|
+
|
|
480
|
+
await expect(
|
|
481
|
+
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
482
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
483
|
+
// The wrong-device session must not survive the error path either.
|
|
484
|
+
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('re-syncs the V1 message schema for this device before encoding Initialize', async () => {
|
|
488
|
+
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
489
|
+
device.features = {
|
|
490
|
+
protocol: 'V1',
|
|
491
|
+
deviceId: 'device-a',
|
|
492
|
+
unlocked: true,
|
|
493
|
+
passphraseProtection: true,
|
|
494
|
+
} as never;
|
|
495
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
496
|
+
type: 'Features',
|
|
497
|
+
message: { device_id: 'device-a' },
|
|
498
|
+
});
|
|
499
|
+
device.commands = { typedCall } as never;
|
|
500
|
+
const reconfigure = jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
501
|
+
|
|
502
|
+
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
503
|
+
|
|
504
|
+
// A stale process-global schema (from another device) would silently strip
|
|
505
|
+
// passphrase_state from the wire message, so the resync must come first.
|
|
506
|
+
expect(reconfigure.mock.invocationCallOrder[0]).toBeLessThan(
|
|
507
|
+
typedCall.mock.invocationCallOrder[0]
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('resumes a cached V1 wallet with a single identity-validated Initialize', async () => {
|
|
379
512
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
380
513
|
device.features = {
|
|
381
514
|
protocol: 'V1',
|
|
@@ -384,22 +517,17 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
384
517
|
passphraseProtection: true,
|
|
385
518
|
} as never;
|
|
386
519
|
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
387
|
-
const typedCall = jest
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
type: 'Features',
|
|
392
|
-
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
393
|
-
});
|
|
520
|
+
const typedCall = jest.fn().mockResolvedValueOnce({
|
|
521
|
+
type: 'Features',
|
|
522
|
+
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
523
|
+
});
|
|
394
524
|
device.commands = { typedCall } as never;
|
|
395
525
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
396
526
|
|
|
397
527
|
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
398
528
|
|
|
399
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
400
|
-
expect(typedCall).
|
|
401
|
-
expect(typedCall).toHaveBeenNthCalledWith(
|
|
402
|
-
2,
|
|
529
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
530
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
403
531
|
'Initialize',
|
|
404
532
|
'Features',
|
|
405
533
|
expect.objectContaining({
|
|
@@ -408,9 +536,10 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
408
536
|
}),
|
|
409
537
|
expect.any(Object)
|
|
410
538
|
);
|
|
539
|
+
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
411
540
|
});
|
|
412
541
|
|
|
413
|
-
test('selects the standard V1 wallet
|
|
542
|
+
test('selects the standard V1 wallet with a single identity-validated Initialize', async () => {
|
|
414
543
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
415
544
|
device.features = {
|
|
416
545
|
protocol: 'V1',
|
|
@@ -420,17 +549,14 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
420
549
|
} as never;
|
|
421
550
|
const typedCall = jest
|
|
422
551
|
.fn()
|
|
423
|
-
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
|
|
424
552
|
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
|
|
425
553
|
device.commands = { typedCall } as never;
|
|
426
554
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
427
555
|
|
|
428
556
|
await device.initialize({ deviceId: 'device-a' });
|
|
429
557
|
|
|
430
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
431
|
-
expect(typedCall).
|
|
432
|
-
expect(typedCall).toHaveBeenNthCalledWith(
|
|
433
|
-
2,
|
|
558
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
559
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
434
560
|
'Initialize',
|
|
435
561
|
'Features',
|
|
436
562
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import FirmwareUpdateV4 from '../../src/api/FirmwareUpdateV4';
|
|
4
4
|
|
|
@@ -127,40 +127,4 @@ 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
|
-
});
|
|
166
130
|
});
|
|
@@ -1,12 +1,8 @@
|
|
|
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';
|
|
5
4
|
import DeviceLock from '../src/api/device/DeviceLock';
|
|
6
5
|
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';
|
|
10
6
|
import OpenWalletSession from '../src/api/OpenWalletSession';
|
|
11
7
|
import { runMethodWithUnlockPolicy } from '../src/protocols/protocol-v2/unlockPolicyRunner';
|
|
12
8
|
|
|
@@ -195,61 +191,6 @@ describe('Protocol V2 unlock semantics', () => {
|
|
|
195
191
|
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, expect.any(Object));
|
|
196
192
|
});
|
|
197
193
|
|
|
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
|
-
|
|
253
194
|
test('pre-unlocks a locked standard wallet before wallet-session preparation', async () => {
|
|
254
195
|
const calls: string[] = [];
|
|
255
196
|
const features = {
|
|
@@ -5501,6 +5501,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5501
5501
|
'DeviceFirmwareUpdateStatusGet',
|
|
5502
5502
|
'DeviceFirmwareUpdateStatusGet',
|
|
5503
5503
|
]);
|
|
5504
|
+
expect((method as any).protocolV2FinalStatusVerified).toBe(true);
|
|
5504
5505
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5505
5506
|
});
|
|
5506
5507
|
|
|
@@ -5648,7 +5649,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5648
5649
|
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
5649
5650
|
});
|
|
5650
5651
|
|
|
5651
|
-
test('
|
|
5652
|
+
test('rejects normal mode without install ACK, target status, or a version change', async () => {
|
|
5652
5653
|
const method = new FirmwareUpdateV4({
|
|
5653
5654
|
id: 1,
|
|
5654
5655
|
payload: {
|
|
@@ -5662,7 +5663,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5662
5663
|
let now = 0;
|
|
5663
5664
|
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
5664
5665
|
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
5665
|
-
now +=
|
|
5666
|
+
now += 31 * 1000;
|
|
5666
5667
|
callback();
|
|
5667
5668
|
return 0 as any;
|
|
5668
5669
|
}) as typeof setTimeout);
|
|
@@ -5679,14 +5680,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5679
5680
|
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5680
5681
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5681
5682
|
])
|
|
5682
|
-
).rejects.toMatchObject({
|
|
5683
|
-
errorCode: HardwareErrorCode.FirmwareError,
|
|
5684
|
-
message: 'Firmware installation failed',
|
|
5685
|
-
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
5686
|
-
});
|
|
5683
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.FirmwareError });
|
|
5687
5684
|
|
|
5688
5685
|
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5689
|
-
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
5690
5686
|
});
|
|
5691
5687
|
|
|
5692
5688
|
test('accepts ACK-less normal mode after the requested target version changes', async () => {
|
|
@@ -5903,8 +5899,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5903
5899
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5904
5900
|
])
|
|
5905
5901
|
).rejects.toMatchObject({
|
|
5906
|
-
errorCode: HardwareErrorCode.
|
|
5907
|
-
message: '
|
|
5902
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
5903
|
+
message: 'Protocol V2 install status conflicts with target 4',
|
|
5908
5904
|
params: {
|
|
5909
5905
|
firmwareUpdateCode: 'FirmwareInstallStatusConflict',
|
|
5910
5906
|
},
|
|
@@ -5941,7 +5937,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5941
5937
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
5942
5938
|
});
|
|
5943
5939
|
|
|
5944
|
-
test('
|
|
5940
|
+
test('rejects unobservable final versions after the status endpoint fallback', () => {
|
|
5945
5941
|
const method = new FirmwareUpdateV4({
|
|
5946
5942
|
id: 1,
|
|
5947
5943
|
payload: {
|
|
@@ -5961,30 +5957,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5961
5957
|
patch_version: 0,
|
|
5962
5958
|
};
|
|
5963
5959
|
|
|
5964
|
-
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
5965
|
-
});
|
|
5966
|
-
|
|
5967
|
-
test('still rejects an unobservable non-app version after the status endpoint fallback', () => {
|
|
5968
|
-
const method = new FirmwareUpdateV4({
|
|
5969
|
-
id: 1,
|
|
5970
|
-
payload: {
|
|
5971
|
-
method: 'firmwareUpdateV4',
|
|
5972
|
-
platform: 'desktop',
|
|
5973
|
-
targetsToUpdate: ['se01'],
|
|
5974
|
-
expectedTargetVersions: {
|
|
5975
|
-
se01: '1.0.0',
|
|
5976
|
-
},
|
|
5977
|
-
},
|
|
5978
|
-
});
|
|
5979
|
-
method.init();
|
|
5980
|
-
(method as any).protocolV2LatestFinalFeatures = {
|
|
5981
|
-
major_version: 3,
|
|
5982
|
-
minor_version: 0,
|
|
5983
|
-
patch_version: 0,
|
|
5984
|
-
};
|
|
5985
|
-
|
|
5986
5960
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
5987
|
-
'
|
|
5961
|
+
'has no observable final version after status fallback'
|
|
5988
5962
|
);
|
|
5989
5963
|
});
|
|
5990
5964
|
|
|
@@ -6030,7 +6004,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6030
6004
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6031
6005
|
});
|
|
6032
6006
|
|
|
6033
|
-
test('
|
|
6007
|
+
test('fails clearly when a requested target stays absent from the full status dump', async () => {
|
|
6034
6008
|
const method = new FirmwareUpdateV4({
|
|
6035
6009
|
id: 1,
|
|
6036
6010
|
payload: {
|
|
@@ -6046,7 +6020,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6046
6020
|
let now = 0;
|
|
6047
6021
|
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
6048
6022
|
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
6049
|
-
now +=
|
|
6023
|
+
now += 31 * 1000;
|
|
6050
6024
|
callback();
|
|
6051
6025
|
return 0 as any;
|
|
6052
6026
|
}) as typeof setTimeout);
|
|
@@ -6065,13 +6039,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6065
6039
|
])
|
|
6066
6040
|
).rejects.toMatchObject({
|
|
6067
6041
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
6068
|
-
message: '
|
|
6069
|
-
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6042
|
+
message: expect.stringContaining('targetIds=3'),
|
|
6070
6043
|
});
|
|
6071
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
6044
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
6072
6045
|
});
|
|
6073
6046
|
|
|
6074
|
-
test('
|
|
6047
|
+
test('resets the missing-record grace period after a Pro2 reboot interruption', async () => {
|
|
6075
6048
|
const method = new FirmwareUpdateV4({
|
|
6076
6049
|
id: 1,
|
|
6077
6050
|
payload: {
|
|
@@ -6124,7 +6097,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6124
6097
|
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
6125
6098
|
});
|
|
6126
6099
|
|
|
6127
|
-
test('uses
|
|
6100
|
+
test('uses an eight-minute Pro2 install status window', async () => {
|
|
6128
6101
|
const method = new FirmwareUpdateV4({
|
|
6129
6102
|
id: 1,
|
|
6130
6103
|
payload: {
|
|
@@ -6134,17 +6107,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6134
6107
|
jest
|
|
6135
6108
|
.spyOn(Date, 'now')
|
|
6136
6109
|
.mockReturnValueOnce(0)
|
|
6137
|
-
.mockReturnValue(
|
|
6110
|
+
.mockReturnValue(8 * 60 * 1000);
|
|
6138
6111
|
|
|
6139
6112
|
await expect(
|
|
6140
6113
|
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6141
6114
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6142
6115
|
])
|
|
6143
|
-
).rejects.
|
|
6144
|
-
errorCode: HardwareErrorCode.FirmwareError,
|
|
6145
|
-
message: 'Firmware installation failed',
|
|
6146
|
-
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6147
|
-
});
|
|
6116
|
+
).rejects.toThrow('within 480s');
|
|
6148
6117
|
});
|
|
6149
6118
|
|
|
6150
6119
|
test('uses a 90-second Pro2 bootloader reconnect window', async () => {
|
|
@@ -6222,7 +6191,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6222
6191
|
],
|
|
6223
6192
|
new Set([4, 10])
|
|
6224
6193
|
)
|
|
6225
|
-
).toThrow('
|
|
6194
|
+
).toThrow('Protocol V2 install status conflicts with target 4');
|
|
6226
6195
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
6227
6196
|
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6228
6197
|
|
|
@@ -6262,8 +6231,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6262
6231
|
throw new Error('Expected Protocol V2 failed firmware status to throw');
|
|
6263
6232
|
} catch (error: any) {
|
|
6264
6233
|
expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
|
|
6265
|
-
expect(error.message).
|
|
6266
|
-
expect(error.
|
|
6234
|
+
expect(error.message).toContain('FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY');
|
|
6235
|
+
expect(error.message).toContain('vol0:/application_p1.bin');
|
|
6267
6236
|
}
|
|
6268
6237
|
});
|
|
6269
6238
|
|
|
@@ -6300,8 +6269,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6300
6269
|
])
|
|
6301
6270
|
).rejects.toMatchObject({
|
|
6302
6271
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
6303
|
-
message: '
|
|
6304
|
-
params: { firmwareUpdateCode: 'FirmwareInstallFailed' },
|
|
6272
|
+
message: expect.stringContaining('vol0:/application_p1.bin'),
|
|
6305
6273
|
});
|
|
6306
6274
|
expect(typedCall).toHaveBeenCalledWith(
|
|
6307
6275
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkD,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceChangePin.d.ts","sourceRoot":"","sources":["../../../src/api/device/DeviceChangePin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,UAAU,CAAC,SAAS,CAAC;IAChE,qBAAqB;IAIrB,IAAI;
|
|
1
|
+
{"version":3,"file":"DeviceChangePin.d.ts","sourceRoot":"","sources":["../../../src/api/device/DeviceChangePin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,UAAU,CAAC,SAAS,CAAC;IAChE,qBAAqB;IAIrB,IAAI;IAoBE,GAAG;CAmBV"}
|
|
@@ -2,7 +2,6 @@ import { BaseMethod } from '../BaseMethod';
|
|
|
2
2
|
import type { BixinVerifyDeviceRequest } from '@onekeyfe/hd-transport';
|
|
3
3
|
import type { DeviceVerifySignature } from '../../types';
|
|
4
4
|
export default class DeviceVerify extends BaseMethod<BixinVerifyDeviceRequest> {
|
|
5
|
-
getSupportedProtocols(): readonly ["V1", "V2"];
|
|
6
5
|
init(): void;
|
|
7
6
|
run(): Promise<DeviceVerifySignature>;
|
|
8
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceVerify.d.ts","sourceRoot":"","sources":["../../../src/api/device/DeviceVerify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DeviceVerify.d.ts","sourceRoot":"","sources":["../../../src/api/device/DeviceVerify.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,UAAU,CAAC,wBAAwB,CAAC;IAC5E,IAAI;IAYE,GAAG;CAsCV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceWipe.d.ts","sourceRoot":"","sources":["../../../src/api/device/DeviceWipe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,UAAU,CAAC,UAAU,CAAC;IAC5D,qBAAqB;IAIrB,IAAI;
|
|
1
|
+
{"version":3,"file":"DeviceWipe.d.ts","sourceRoot":"","sources":["../../../src/api/device/DeviceWipe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,UAAU,CAAC,UAAU,CAAC;IAC5D,qBAAqB;IAIrB,IAAI;IAcE,GAAG;CAcV"}
|