@onekeyfe/hd-core 1.2.0-alpha.33 → 1.2.0-alpha.35
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 +84 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +31 -0
- package/__tests__/core-dispose.test.ts +3 -3
- package/__tests__/device-lifecycle-events.test.ts +52 -0
- package/__tests__/device-pool-state.test.ts +14 -2
- package/__tests__/device-settings.test.ts +25 -0
- package/__tests__/device-state-mapper.test.ts +13 -2
- package/__tests__/device-state-projector.test.ts +2 -2
- package/__tests__/device-utils.test.ts +1 -0
- package/__tests__/device-wallet-session-store.test.ts +137 -17
- package/__tests__/evmSignTransaction.test.ts +69 -0
- package/__tests__/get-device-state.test.ts +78 -30
- package/__tests__/kaspaSignTransaction.test.ts +9 -0
- package/__tests__/known-device-public-types.type-test.ts +3 -0
- package/__tests__/logBlockEvent.test.ts +89 -5
- package/__tests__/open-wallet-session.test.ts +499 -60
- package/__tests__/pro2Wallpaper.test.ts +1 -1
- package/__tests__/protocol-v2.test.ts +1180 -169
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
- package/dist/api/evm/protocol.d.ts +3 -0
- package/dist/api/evm/protocol.d.ts.map +1 -0
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +10 -4
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -0
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/device/DeviceStateProjector.d.ts.map +1 -1
- package/dist/device/DeviceWalletSessionStore.d.ts +7 -0
- package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
- package/dist/events/logBlockEvent.d.ts +2 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +25 -11
- package/dist/index.js +1037 -838
- package/dist/protocols/protocol-v2/features.d.ts +17 -6
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts +1 -0
- package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +1 -10
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +2 -2
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts +6 -3
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +11 -81
- package/src/api/FirmwareUpdateV4.ts +59 -25
- package/src/api/GetPassphraseState.ts +11 -2
- package/src/api/OpenWalletSession.ts +16 -8
- package/src/api/device/DeviceSettings.ts +8 -1
- package/src/api/evm/EVMGetAddress.ts +2 -2
- package/src/api/evm/EVMGetPublicKey.ts +2 -2
- package/src/api/evm/EVMSignMessage.ts +2 -2
- package/src/api/evm/EVMSignTransaction.ts +2 -2
- package/src/api/evm/EVMSignTypedData.ts +35 -45
- package/src/api/evm/EVMVerifyMessage.ts +2 -2
- package/src/api/evm/protocol.ts +6 -0
- package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
- package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
- package/src/core/index.ts +7 -5
- package/src/data/messages/messages-protocol-v2.json +393 -485
- package/src/device/Device.ts +98 -44
- package/src/device/DeviceCommands.ts +12 -2
- package/src/device/DeviceConnector.ts +10 -7
- package/src/device/DevicePool.ts +5 -3
- package/src/device/DeviceStateMapper.ts +5 -3
- package/src/device/DeviceStateProjector.ts +0 -7
- package/src/device/DeviceWalletSessionStore.ts +43 -4
- package/src/deviceProfile/buildDeviceFeatures.ts +11 -4
- package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
- package/src/events/logBlockEvent.ts +84 -1
- package/src/protocols/protocol-v2/deviceSession.ts +1 -1
- package/src/protocols/protocol-v2/features.ts +58 -16
- package/src/protocols/protocol-v2/index.ts +5 -0
- package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
- package/src/protocols/protocol-v2/unlockRetry.ts +18 -7
- package/src/protocols/protocol-v2/walletSession.ts +144 -62
- package/src/types/api/protocolV2.ts +2 -2
- package/src/types/device.ts +8 -2
- package/src/types/params.ts +2 -1
- package/src/utils/deviceFeaturesCompat.ts +8 -1
- package/src/utils/pro2Wallpaper.ts +3 -9
- package/tsconfig.type-tests.json +5 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DeviceRebootType,
|
|
4
|
+
DeviceSessionPinType,
|
|
5
|
+
DeviceSettingsPage,
|
|
6
|
+
DeviceType,
|
|
7
|
+
} from '@onekeyfe/hd-transport';
|
|
3
8
|
|
|
4
9
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
5
10
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -54,15 +59,20 @@ import { getBitcoinForkSupportedProtocols } from '../src/api/btc/helpers/version
|
|
|
54
59
|
import { DataManager } from '../src/data-manager';
|
|
55
60
|
import { createCoreApi } from '../src/inject';
|
|
56
61
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
62
|
+
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
57
63
|
import { DevicePool } from '../src/device/DevicePool';
|
|
58
64
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
59
65
|
import {
|
|
60
66
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
67
|
+
PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE,
|
|
61
68
|
type ProtocolV2DeviceInfo,
|
|
62
69
|
type ProtocolV2RuntimeMode,
|
|
63
70
|
getProtocolV2RuntimeMode,
|
|
71
|
+
parseProtocolV2BuildFingerprint,
|
|
64
72
|
requestProtocolV2DeviceInfo,
|
|
73
|
+
requestProtocolV2ProtocolInfo,
|
|
65
74
|
requestProtocolV2DeviceStatus,
|
|
75
|
+
supportsProtocolV2Message,
|
|
66
76
|
} from '../src/protocols/protocol-v2/features';
|
|
67
77
|
import {
|
|
68
78
|
getProtocolV2WalletSession,
|
|
@@ -89,18 +99,26 @@ import {
|
|
|
89
99
|
|
|
90
100
|
import type { DeviceCommands } from '../src/device/DeviceCommands';
|
|
91
101
|
import type { Features } from '../src/types';
|
|
92
|
-
import type { DeviceStatus } from '@onekeyfe/hd-transport';
|
|
102
|
+
import type { DeviceStatus, ProtocolInfo } from '@onekeyfe/hd-transport';
|
|
93
103
|
|
|
94
104
|
jest.mock('../src/data/config', () => ({
|
|
95
105
|
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
96
106
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
97
107
|
}));
|
|
98
108
|
|
|
99
|
-
const createWalletSessionTypedCall = (
|
|
109
|
+
const createWalletSessionTypedCall = (
|
|
110
|
+
implementation = jest.fn(),
|
|
111
|
+
status: Partial<DeviceStatus> = { passphrase_enabled: true }
|
|
112
|
+
) =>
|
|
100
113
|
jest.fn((request: string, ...args: unknown[]) => {
|
|
101
114
|
if (request === 'ProtocolInfoRequest') {
|
|
102
115
|
return Promise.resolve({ message: { version: 2 } });
|
|
103
116
|
}
|
|
117
|
+
if (request === 'DeviceStatusGet') {
|
|
118
|
+
return Promise.resolve({
|
|
119
|
+
message: { unlocked: true, unlocked_attach_pin: false, ...status },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
104
122
|
return implementation(request, ...args);
|
|
105
123
|
});
|
|
106
124
|
|
|
@@ -129,7 +147,7 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
129
147
|
const result = await method.run();
|
|
130
148
|
|
|
131
149
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
132
|
-
expect(method.unlockPolicy).toBe('
|
|
150
|
+
expect(method.unlockPolicy).toBe('none');
|
|
133
151
|
expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
|
|
134
152
|
path: 'vol1:/wallpapers',
|
|
135
153
|
});
|
|
@@ -333,6 +351,24 @@ const descriptor = {
|
|
|
333
351
|
path: 'usb-path',
|
|
334
352
|
};
|
|
335
353
|
|
|
354
|
+
const protocolV2ApplicationInfo = {
|
|
355
|
+
version: 1,
|
|
356
|
+
build_fingerprint: 'application__1.2.3__abcdef0__PROD__RELEASE',
|
|
357
|
+
supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const protocolV2BootloaderInfo = {
|
|
361
|
+
version: 1,
|
|
362
|
+
build_fingerprint: 'bootloader__0.2.0__abcdef0__PROD__RELEASE',
|
|
363
|
+
supported_messages: [],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const protocolV2RomloaderInfo = {
|
|
367
|
+
version: 1,
|
|
368
|
+
build_fingerprint: 'romloader__1.0.0__abcdef0__PROD__RELEASE',
|
|
369
|
+
supported_messages: [],
|
|
370
|
+
};
|
|
371
|
+
|
|
336
372
|
/**
|
|
337
373
|
* Add Device protocol and getCurrent* accessors to plain-object stubs.
|
|
338
374
|
* Defaults match Device.ts and never replace an existing member such as jest.fn().
|
|
@@ -342,6 +378,8 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
342
378
|
d.updateState ??= jest.fn();
|
|
343
379
|
d.markProtocolV2Reboot ??= jest.fn();
|
|
344
380
|
d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
|
|
381
|
+
d.isBootloader ??= () => d.features?.mode === 'bootloader';
|
|
382
|
+
d.isRomloader ??= () => d.features?.mode === 'romloader';
|
|
345
383
|
d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1');
|
|
346
384
|
d.getCurrentDeviceType ??= () => getDeviceType(d.features);
|
|
347
385
|
d.getCurrentFirmwareType ??= () => getFirmwareType(d.features);
|
|
@@ -361,7 +399,8 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
361
399
|
d.updateProtocolV2Features ??= (
|
|
362
400
|
deviceInfo?: ProtocolV2DeviceInfo,
|
|
363
401
|
deviceStatus?: DeviceStatus,
|
|
364
|
-
runtimeMode?: ProtocolV2RuntimeMode
|
|
402
|
+
runtimeMode?: ProtocolV2RuntimeMode,
|
|
403
|
+
protocolInfo?: ProtocolInfo
|
|
365
404
|
) => {
|
|
366
405
|
d.features = buildProtocolV2FeaturesPayload({
|
|
367
406
|
deviceInfo,
|
|
@@ -369,44 +408,35 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
369
408
|
previous: d.features,
|
|
370
409
|
runtimeMode: runtimeMode ?? (deviceStatus ? 'normal' : undefined),
|
|
371
410
|
});
|
|
411
|
+
if (protocolInfo) {
|
|
412
|
+
d.features.raw = {
|
|
413
|
+
...d.features.raw,
|
|
414
|
+
protocolV2ProtocolInfo: protocolInfo,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
372
417
|
return d.features;
|
|
373
418
|
};
|
|
374
419
|
d.probeProtocolV2RuntimeState ??= async (
|
|
375
420
|
deviceInfo: ProtocolV2DeviceInfo,
|
|
376
421
|
timeoutMs?: number
|
|
377
422
|
) => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
features ??
|
|
390
|
-
buildProtocolV2FeaturesPayload({
|
|
391
|
-
deviceInfo,
|
|
392
|
-
deviceStatus: response.message,
|
|
393
|
-
previous: d.features,
|
|
394
|
-
runtimeMode: 'normal',
|
|
395
|
-
})
|
|
396
|
-
);
|
|
397
|
-
} catch {
|
|
398
|
-
const runtimeMode = getProtocolV2RuntimeMode({
|
|
399
|
-
deviceInfo,
|
|
400
|
-
deviceStatusAvailable: false,
|
|
401
|
-
});
|
|
402
|
-
const features = d.updateProtocolV2Features(deviceInfo, null, runtimeMode);
|
|
403
|
-
if (features?.mode === runtimeMode) return features;
|
|
404
|
-
return buildProtocolV2FeaturesPayload({
|
|
405
|
-
deviceInfo,
|
|
406
|
-
previous: d.features,
|
|
407
|
-
runtimeMode,
|
|
408
|
-
});
|
|
423
|
+
const protocolInfo = await requestProtocolV2ProtocolInfo({
|
|
424
|
+
commands: d.getCommands(),
|
|
425
|
+
timeoutMs,
|
|
426
|
+
});
|
|
427
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
|
|
428
|
+
if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
|
|
429
|
+
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
430
|
+
}
|
|
431
|
+
if (!supportsProtocolV2Message(protocolInfo, PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE)) {
|
|
432
|
+
if (!runtimeMode) throw new Error('Unknown Protocol V2 runtime mode');
|
|
433
|
+
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
409
434
|
}
|
|
435
|
+
const deviceStatus = await requestProtocolV2DeviceStatus({
|
|
436
|
+
commands: d.getCommands(),
|
|
437
|
+
timeoutMs,
|
|
438
|
+
});
|
|
439
|
+
return d.updateProtocolV2Features(deviceInfo, deviceStatus, 'normal', protocolInfo);
|
|
410
440
|
};
|
|
411
441
|
d.updateProtocolV2Status ??= (deviceStatus: DeviceStatus) => {
|
|
412
442
|
d.features = buildProtocolV2FeaturesPayload({
|
|
@@ -464,6 +494,10 @@ async function requestProtocolV2Features({
|
|
|
464
494
|
}
|
|
465
495
|
|
|
466
496
|
describe('Protocol V2 feature adapter', () => {
|
|
497
|
+
beforeEach(() => {
|
|
498
|
+
deviceWalletSessionStore.clear();
|
|
499
|
+
});
|
|
500
|
+
|
|
467
501
|
test('treats passphrase status as unknown until the Protocol V2 device is unlocked', () => {
|
|
468
502
|
const features = buildProtocolV2FeaturesPayload({
|
|
469
503
|
deviceInfo: {
|
|
@@ -575,6 +609,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
575
609
|
const features = normalizeProtocolV2Features(descriptor as any, {
|
|
576
610
|
protocol_version: 1,
|
|
577
611
|
hw: {
|
|
612
|
+
Device_type: DeviceType.PRO2,
|
|
578
613
|
serial_no: 'PR2SERIAL',
|
|
579
614
|
},
|
|
580
615
|
fw: {
|
|
@@ -661,7 +696,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
661
696
|
expect(features.bleEnabled).toBeNull();
|
|
662
697
|
});
|
|
663
698
|
|
|
664
|
-
test('
|
|
699
|
+
test('parses bootloader runtime mode from ProtocolInfo build fingerprint', () => {
|
|
665
700
|
const deviceInfo = {
|
|
666
701
|
protocol_version: 1,
|
|
667
702
|
hw: {
|
|
@@ -674,10 +709,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
674
709
|
},
|
|
675
710
|
se1: {},
|
|
676
711
|
};
|
|
677
|
-
const runtimeMode = getProtocolV2RuntimeMode(
|
|
678
|
-
deviceInfo: deviceInfo as ProtocolV2DeviceInfo,
|
|
679
|
-
deviceStatusAvailable: false,
|
|
680
|
-
});
|
|
712
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolV2BootloaderInfo);
|
|
681
713
|
const features = normalizeProtocolV2Features(
|
|
682
714
|
descriptor as any,
|
|
683
715
|
deviceInfo as ProtocolV2DeviceInfo,
|
|
@@ -699,7 +731,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
699
731
|
expect(features.bootloaderMode).toBe(false);
|
|
700
732
|
});
|
|
701
733
|
|
|
702
|
-
test('
|
|
734
|
+
test('keeps romloader distinct from bootloader using ProtocolInfo build fingerprint', () => {
|
|
703
735
|
const deviceInfo = {
|
|
704
736
|
protocol_version: 1,
|
|
705
737
|
hw: {
|
|
@@ -714,31 +746,50 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
714
746
|
},
|
|
715
747
|
},
|
|
716
748
|
};
|
|
717
|
-
const runtimeMode = getProtocolV2RuntimeMode(
|
|
718
|
-
deviceInfo: deviceInfo as ProtocolV2DeviceInfo,
|
|
719
|
-
deviceStatusAvailable: false,
|
|
720
|
-
});
|
|
749
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolV2RomloaderInfo);
|
|
721
750
|
const features = normalizeProtocolV2Features(
|
|
722
751
|
descriptor as any,
|
|
723
752
|
deviceInfo as ProtocolV2DeviceInfo,
|
|
724
753
|
runtimeMode
|
|
725
754
|
);
|
|
726
|
-
expect(features.mode).toBe('
|
|
755
|
+
expect(features.mode).toBe('romloader');
|
|
727
756
|
expect(features.bootloaderMode).toBe(true);
|
|
728
757
|
expect(features.boardVersion).toBe('1.0.0');
|
|
729
758
|
});
|
|
730
759
|
|
|
760
|
+
test('rejects malformed Protocol V2 build fingerprints', () => {
|
|
761
|
+
expect(parseProtocolV2BuildFingerprint('application__1.2.3')).toBeNull();
|
|
762
|
+
expect(
|
|
763
|
+
getProtocolV2RuntimeMode({
|
|
764
|
+
version: 1,
|
|
765
|
+
build_fingerprint: 'unknown__1.2.3__abcdef0__PROD__RELEASE',
|
|
766
|
+
supported_messages: [],
|
|
767
|
+
})
|
|
768
|
+
).toBeUndefined();
|
|
769
|
+
});
|
|
770
|
+
|
|
731
771
|
test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
|
|
732
772
|
const features = normalizeProtocolV2Features(descriptor as any);
|
|
733
773
|
features.firmwareVersion = '1.2.3';
|
|
734
774
|
features.passphraseProtection = true;
|
|
735
775
|
const typedCall = createWalletSessionTypedCall(
|
|
736
|
-
jest.fn()
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
776
|
+
jest.fn((request: string) => {
|
|
777
|
+
if (request === 'DeviceSessionAskPin') {
|
|
778
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
779
|
+
}
|
|
780
|
+
if (request === 'DeviceStatusGet') {
|
|
781
|
+
return { type: 'DeviceStatus', message: { unlocked: true } };
|
|
782
|
+
}
|
|
783
|
+
if (request === 'DeviceSessionGet') {
|
|
784
|
+
return {
|
|
785
|
+
type: 'DeviceSession',
|
|
786
|
+
message: {
|
|
787
|
+
btc_test_address: 'state-1',
|
|
788
|
+
session_id: 'session-1',
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
742
793
|
})
|
|
743
794
|
);
|
|
744
795
|
const commands = { typedCall } as unknown as DeviceCommands;
|
|
@@ -751,18 +802,233 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
751
802
|
onlyMainPin: true,
|
|
752
803
|
})
|
|
753
804
|
).resolves.toEqual({
|
|
754
|
-
passphraseState:
|
|
755
|
-
newSession:
|
|
756
|
-
unlockedAttachPin:
|
|
805
|
+
passphraseState: 'state-1',
|
|
806
|
+
newSession: 'session-1',
|
|
807
|
+
unlockedAttachPin: undefined,
|
|
757
808
|
resumed: false,
|
|
758
809
|
});
|
|
759
810
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
760
811
|
eventless_wallet_session: true,
|
|
761
812
|
});
|
|
762
|
-
expect(
|
|
813
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
814
|
+
type: DeviceSessionPinType.Main,
|
|
815
|
+
});
|
|
816
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
817
|
+
device.passphraseState = 'state-1';
|
|
818
|
+
expect(device.getInternalState()).toBe('session-1');
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test('reuses the cached Protocol V2 standard wallet instead of asking Main PIN again', async () => {
|
|
822
|
+
const deviceId = 'standard-reuse-device';
|
|
823
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
824
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
825
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
826
|
+
{
|
|
827
|
+
status: {
|
|
828
|
+
device_id: deviceId,
|
|
829
|
+
unlocked: true,
|
|
830
|
+
passphrase_enabled: true,
|
|
831
|
+
},
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
const typedCall = jest.fn((request: string) => {
|
|
836
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
837
|
+
if (request === 'DeviceSessionAskPin') {
|
|
838
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
839
|
+
}
|
|
840
|
+
if (request === 'DeviceStatusGet') {
|
|
841
|
+
return {
|
|
842
|
+
message: {
|
|
843
|
+
device_id: deviceId,
|
|
844
|
+
unlocked: true,
|
|
845
|
+
passphrase_enabled: true,
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (request === 'DeviceSessionGet') {
|
|
850
|
+
return {
|
|
851
|
+
message: {
|
|
852
|
+
btc_test_address: 'standard-state',
|
|
853
|
+
session_id: 'standard-session',
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
858
|
+
});
|
|
859
|
+
(device as any).commands = { typedCall };
|
|
860
|
+
|
|
861
|
+
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
862
|
+
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
863
|
+
|
|
864
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
|
|
865
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
866
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
867
|
+
session_id: 'standard-session',
|
|
868
|
+
});
|
|
869
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
870
|
+
passphraseState: 'standard-state',
|
|
871
|
+
sessionId: 'standard-session',
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test('reopens the Protocol V2 standard wallet when the cached session resolves to another wallet', async () => {
|
|
876
|
+
const deviceId = 'standard-invalid-device';
|
|
877
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
878
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
879
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
880
|
+
{
|
|
881
|
+
status: {
|
|
882
|
+
device_id: deviceId,
|
|
883
|
+
unlocked: true,
|
|
884
|
+
passphrase_enabled: true,
|
|
885
|
+
},
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
deviceWalletSessionStore.set(deviceId, 'hidden-state', 'hidden-session');
|
|
889
|
+
deviceWalletSessionStore.setStandard(deviceId, 'old-standard-state', 'old-standard-session');
|
|
890
|
+
|
|
891
|
+
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
892
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
893
|
+
if (request === 'DeviceSessionGet' && params.session_id) {
|
|
894
|
+
return {
|
|
895
|
+
message: {
|
|
896
|
+
btc_test_address: 'hidden-state',
|
|
897
|
+
session_id: 'hidden-session',
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
if (request === 'DeviceSessionGet') {
|
|
902
|
+
return {
|
|
903
|
+
message: {
|
|
904
|
+
btc_test_address: 'old-standard-state',
|
|
905
|
+
session_id: 'new-standard-session',
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (request === 'DeviceSessionAskPin') {
|
|
910
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
911
|
+
}
|
|
912
|
+
if (request === 'DeviceStatusGet') {
|
|
913
|
+
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
914
|
+
}
|
|
915
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
916
|
+
});
|
|
917
|
+
(device as any).commands = { typedCall };
|
|
918
|
+
|
|
919
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).resolves.toMatchObject({
|
|
920
|
+
passphraseState: 'old-standard-state',
|
|
921
|
+
newSession: 'new-standard-session',
|
|
922
|
+
resumed: false,
|
|
923
|
+
});
|
|
924
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
|
|
925
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
926
|
+
passphraseState: 'old-standard-state',
|
|
927
|
+
sessionId: 'new-standard-session',
|
|
928
|
+
});
|
|
929
|
+
expect(deviceWalletSessionStore.get(deviceId, 'hidden-state')).toBe('hidden-session');
|
|
763
930
|
});
|
|
764
931
|
|
|
765
|
-
test('
|
|
932
|
+
test('propagates DeviceLocked instead of retrying a cached standard session', async () => {
|
|
933
|
+
const deviceId = 'standard-locked-device';
|
|
934
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
935
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
936
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
937
|
+
{
|
|
938
|
+
status: {
|
|
939
|
+
device_id: deviceId,
|
|
940
|
+
unlocked: false,
|
|
941
|
+
passphrase_enabled: true,
|
|
942
|
+
},
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
deviceWalletSessionStore.setStandard(deviceId, 'standard-state', 'cached-standard-session');
|
|
946
|
+
|
|
947
|
+
let sessionGetCalls = 0;
|
|
948
|
+
const typedCall = jest.fn((request: string) => {
|
|
949
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
950
|
+
if (request === 'DeviceSessionAskPin') {
|
|
951
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
952
|
+
}
|
|
953
|
+
if (request === 'DeviceStatusGet') {
|
|
954
|
+
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
955
|
+
}
|
|
956
|
+
if (request === 'DeviceSessionGet') {
|
|
957
|
+
sessionGetCalls += 1;
|
|
958
|
+
if (sessionGetCalls === 1) {
|
|
959
|
+
throw Object.assign(new Error('Device is locked'), {
|
|
960
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
message: {
|
|
965
|
+
btc_test_address: 'standard-state',
|
|
966
|
+
session_id: 'cached-standard-session',
|
|
967
|
+
},
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
971
|
+
});
|
|
972
|
+
(device as any).commands = { typedCall };
|
|
973
|
+
|
|
974
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).rejects.toMatchObject({
|
|
975
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
976
|
+
});
|
|
977
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
978
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toBeUndefined();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test('recovers a mismatched cached standard wallet without deleting hidden sessions', async () => {
|
|
982
|
+
const deviceId = 'standard-mismatch-device';
|
|
983
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
984
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
985
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
986
|
+
{
|
|
987
|
+
status: {
|
|
988
|
+
device_id: deviceId,
|
|
989
|
+
unlocked: true,
|
|
990
|
+
passphrase_enabled: true,
|
|
991
|
+
},
|
|
992
|
+
}
|
|
993
|
+
);
|
|
994
|
+
deviceWalletSessionStore.set(deviceId, 'hidden-state', 'hidden-session');
|
|
995
|
+
deviceWalletSessionStore.setStandard(deviceId, 'standard-state', 'standard-session');
|
|
996
|
+
let sessionGetCalls = 0;
|
|
997
|
+
const typedCall = jest.fn((request: string) => {
|
|
998
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
999
|
+
if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
|
|
1000
|
+
if (request === 'DeviceStatusGet') {
|
|
1001
|
+
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
1002
|
+
}
|
|
1003
|
+
if (request === 'DeviceSessionGet') {
|
|
1004
|
+
sessionGetCalls += 1;
|
|
1005
|
+
return {
|
|
1006
|
+
message: {
|
|
1007
|
+
btc_test_address: sessionGetCalls === 1 ? 'unexpected-wallet-state' : 'standard-state',
|
|
1008
|
+
session_id: sessionGetCalls === 1 ? 'unexpected-session' : 'renewed-standard-session',
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1013
|
+
});
|
|
1014
|
+
(device as any).commands = { typedCall };
|
|
1015
|
+
|
|
1016
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).resolves.toMatchObject({
|
|
1017
|
+
passphraseState: 'standard-state',
|
|
1018
|
+
newSession: 'renewed-standard-session',
|
|
1019
|
+
resumed: false,
|
|
1020
|
+
});
|
|
1021
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
1022
|
+
type: DeviceSessionPinType.Main,
|
|
1023
|
+
});
|
|
1024
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
1025
|
+
passphraseState: 'standard-state',
|
|
1026
|
+
sessionId: 'renewed-standard-session',
|
|
1027
|
+
});
|
|
1028
|
+
expect(deviceWalletSessionStore.get(deviceId, 'hidden-state')).toBe('hidden-session');
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test('selects a Protocol V2 wallet explicitly even when passphrase protection was disabled', async () => {
|
|
766
1032
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
767
1033
|
(device as any).features = normalizeProtocolV2Features(
|
|
768
1034
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
@@ -783,13 +1049,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
783
1049
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
784
1050
|
|
|
785
1051
|
await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
|
|
786
|
-
passphraseState:
|
|
787
|
-
newSession:
|
|
788
|
-
unlockedAttachPin:
|
|
1052
|
+
passphraseState: 'main-wallet-state',
|
|
1053
|
+
newSession: 'main-wallet-session',
|
|
1054
|
+
unlockedAttachPin: undefined,
|
|
789
1055
|
resumed: false,
|
|
790
1056
|
});
|
|
791
1057
|
|
|
792
|
-
expect(promptPassphrase).
|
|
1058
|
+
expect(promptPassphrase).toHaveBeenCalled();
|
|
793
1059
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
794
1060
|
eventless_wallet_session: true,
|
|
795
1061
|
});
|
|
@@ -835,7 +1101,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
835
1101
|
});
|
|
836
1102
|
});
|
|
837
1103
|
|
|
838
|
-
test('
|
|
1104
|
+
test('does not implicitly unlock inside the Protocol V2 wallet-session coordinator', async () => {
|
|
839
1105
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
840
1106
|
(device as any).features = normalizeProtocolV2Features(
|
|
841
1107
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
@@ -847,14 +1113,15 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
847
1113
|
}
|
|
848
1114
|
);
|
|
849
1115
|
const typedCall = createWalletSessionTypedCall(
|
|
850
|
-
jest
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1116
|
+
jest
|
|
1117
|
+
.fn()
|
|
1118
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1119
|
+
.mockResolvedValueOnce({
|
|
1120
|
+
message: {
|
|
1121
|
+
btc_test_address: 'hidden-after-unlock-state',
|
|
1122
|
+
session_id: 'hidden-after-unlock-session',
|
|
1123
|
+
},
|
|
1124
|
+
})
|
|
858
1125
|
);
|
|
859
1126
|
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden-after-unlock' });
|
|
860
1127
|
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockImplementation(() => {
|
|
@@ -868,14 +1135,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
868
1135
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
869
1136
|
|
|
870
1137
|
await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
|
|
871
|
-
passphraseState:
|
|
872
|
-
newSession:
|
|
873
|
-
unlockedAttachPin:
|
|
1138
|
+
passphraseState: 'hidden-after-unlock-state',
|
|
1139
|
+
newSession: 'hidden-after-unlock-session',
|
|
1140
|
+
unlockedAttachPin: undefined,
|
|
874
1141
|
resumed: false,
|
|
875
1142
|
});
|
|
876
1143
|
|
|
877
|
-
expect(unlockDevice).
|
|
878
|
-
expect(promptPassphrase).
|
|
1144
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1145
|
+
expect(promptPassphrase).toHaveBeenCalled();
|
|
879
1146
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
880
1147
|
eventless_wallet_session: true,
|
|
881
1148
|
});
|
|
@@ -905,8 +1172,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
905
1172
|
onlyMainPin: true,
|
|
906
1173
|
})
|
|
907
1174
|
).resolves.toMatchObject({
|
|
908
|
-
passphraseState:
|
|
909
|
-
newSession:
|
|
1175
|
+
passphraseState: 'main-wallet-state',
|
|
1176
|
+
newSession: 'main-wallet-session',
|
|
910
1177
|
});
|
|
911
1178
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
912
1179
|
eventless_wallet_session: true,
|
|
@@ -933,12 +1200,20 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
933
1200
|
}
|
|
934
1201
|
);
|
|
935
1202
|
const typedCall = createWalletSessionTypedCall(
|
|
936
|
-
jest.fn()
|
|
937
|
-
|
|
938
|
-
|
|
1203
|
+
jest.fn((request: string) => {
|
|
1204
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
1205
|
+
return { type: 'Success', message: { message: 'Passphrase prepared' } };
|
|
1206
|
+
}
|
|
1207
|
+
if (request === 'DeviceSessionGet') {
|
|
1208
|
+
return {
|
|
1209
|
+
type: 'DeviceSession',
|
|
1210
|
+
message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
939
1214
|
})
|
|
940
1215
|
);
|
|
941
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1216
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
942
1217
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
943
1218
|
|
|
944
1219
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
@@ -948,7 +1223,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
948
1223
|
|
|
949
1224
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
950
1225
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
951
|
-
|
|
1226
|
+
passphrase: 'host hidden wallet',
|
|
1227
|
+
on_device: false,
|
|
952
1228
|
});
|
|
953
1229
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
954
1230
|
});
|
|
@@ -979,7 +1255,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
979
1255
|
|
|
980
1256
|
test('deviceGetOnboardingStatus returns the current Protocol V2 onboarding status', async () => {
|
|
981
1257
|
const typedCall = jest.fn().mockResolvedValue({
|
|
982
|
-
type: '
|
|
1258
|
+
type: 'OnboardingStatus',
|
|
983
1259
|
message: {
|
|
984
1260
|
step: 4,
|
|
985
1261
|
phase: 4,
|
|
@@ -1007,7 +1283,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1007
1283
|
pin_set: true,
|
|
1008
1284
|
wallet_initialized: false,
|
|
1009
1285
|
});
|
|
1010
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
1286
|
+
expect(typedCall).toHaveBeenCalledWith('OnboardingStatusGet', 'OnboardingStatus', {});
|
|
1011
1287
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
1012
1288
|
});
|
|
1013
1289
|
|
|
@@ -1148,15 +1424,18 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1148
1424
|
}
|
|
1149
1425
|
);
|
|
1150
1426
|
const typedCall = createWalletSessionTypedCall(
|
|
1151
|
-
jest
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1427
|
+
jest
|
|
1428
|
+
.fn()
|
|
1429
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1430
|
+
.mockRejectedValueOnce(
|
|
1431
|
+
Object.assign(new Error('Invalid session'), {
|
|
1432
|
+
errorCode: HardwareErrorCode.WalletSessionInvalid,
|
|
1433
|
+
})
|
|
1434
|
+
)
|
|
1156
1435
|
);
|
|
1157
1436
|
(device as any).commands = {
|
|
1158
1437
|
typedCall,
|
|
1159
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1438
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1160
1439
|
};
|
|
1161
1440
|
|
|
1162
1441
|
await expect(getProtocolV2WalletSession(device)).rejects.toMatchObject({
|
|
@@ -1164,11 +1443,17 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1164
1443
|
});
|
|
1165
1444
|
expect(typedCall.mock.calls).toEqual([
|
|
1166
1445
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1167
|
-
[
|
|
1446
|
+
[
|
|
1447
|
+
'DeviceSessionAskPassphrase',
|
|
1448
|
+
'Success',
|
|
1449
|
+
{ passphrase: 'host hidden wallet', on_device: false },
|
|
1450
|
+
],
|
|
1451
|
+
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1452
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1168
1453
|
]);
|
|
1169
1454
|
});
|
|
1170
1455
|
|
|
1171
|
-
test('
|
|
1456
|
+
test('reselects a hidden wallet when the cached Pro2 session resolves to another wallet', async () => {
|
|
1172
1457
|
const device = Device.fromDescriptor({
|
|
1173
1458
|
id: 'cache-device-mismatch',
|
|
1174
1459
|
path: 'cache-path-mismatch',
|
|
@@ -1186,26 +1471,148 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1186
1471
|
);
|
|
1187
1472
|
device.passphraseState = 'state-a';
|
|
1188
1473
|
preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a');
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1474
|
+
let sessionGetCalls = 0;
|
|
1475
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1476
|
+
jest.fn((request: string) => {
|
|
1477
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: { message: 'prepared' } };
|
|
1478
|
+
if (request === 'DeviceSessionGet') {
|
|
1479
|
+
sessionGetCalls += 1;
|
|
1480
|
+
return {
|
|
1481
|
+
type: 'DeviceSession',
|
|
1482
|
+
message:
|
|
1483
|
+
sessionGetCalls === 1
|
|
1484
|
+
? { session_id: 'session-b', btc_test_address: 'state-b' }
|
|
1485
|
+
: { session_id: 'session-a-new', btc_test_address: 'state-a' },
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1489
|
+
})
|
|
1490
|
+
);
|
|
1491
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1492
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1197
1493
|
|
|
1198
1494
|
await expect(
|
|
1199
1495
|
getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
|
|
1200
|
-
).
|
|
1496
|
+
).resolves.toMatchObject({
|
|
1497
|
+
passphraseState: 'state-a',
|
|
1498
|
+
newSession: 'session-a-new',
|
|
1499
|
+
resumed: false,
|
|
1500
|
+
});
|
|
1501
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1201
1502
|
expect.objectContaining({
|
|
1202
|
-
|
|
1503
|
+
source: 'wallet-session-coordinator',
|
|
1504
|
+
reason: 'session-recovery',
|
|
1505
|
+
expectedPassphraseState: 'state-a',
|
|
1506
|
+
}),
|
|
1507
|
+
{ cancelDeviceOnReject: false }
|
|
1508
|
+
);
|
|
1509
|
+
expect(device.getInternalState()).toBe('session-a-new');
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
test('rejects a hidden wallet when one recovery still returns the wrong state', async () => {
|
|
1513
|
+
const device = Device.fromDescriptor({
|
|
1514
|
+
id: 'cache-device-final-mismatch',
|
|
1515
|
+
path: 'cache-path-final-mismatch',
|
|
1516
|
+
protocolType: 'V2',
|
|
1517
|
+
} as any);
|
|
1518
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1519
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1520
|
+
{
|
|
1521
|
+
status: {
|
|
1522
|
+
device_id: 'stable-device-final-mismatch',
|
|
1523
|
+
unlocked: true,
|
|
1524
|
+
passphrase_enabled: true,
|
|
1525
|
+
},
|
|
1526
|
+
}
|
|
1527
|
+
);
|
|
1528
|
+
device.passphraseState = 'state-a';
|
|
1529
|
+
preloadSessionCache('stable-device-final-mismatch', 'state-a', 'session-a');
|
|
1530
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1531
|
+
jest.fn((request: string) => {
|
|
1532
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: { message: 'prepared' } };
|
|
1533
|
+
if (request === 'DeviceSessionGet') {
|
|
1534
|
+
return {
|
|
1535
|
+
type: 'DeviceSession',
|
|
1536
|
+
message: { session_id: 'wrong-session', btc_test_address: 'wrong-state' },
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1203
1540
|
})
|
|
1204
1541
|
);
|
|
1542
|
+
(device as any).commands = {
|
|
1543
|
+
typedCall,
|
|
1544
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
await expect(
|
|
1548
|
+
getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
|
|
1549
|
+
).rejects.toMatchObject({
|
|
1550
|
+
errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
|
|
1551
|
+
});
|
|
1205
1552
|
expect(device.getInternalState()).toBeUndefined();
|
|
1206
1553
|
});
|
|
1207
1554
|
|
|
1208
|
-
test('
|
|
1555
|
+
test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
|
|
1556
|
+
const device = Device.fromDescriptor({
|
|
1557
|
+
id: 'cache-device-attach-recovery',
|
|
1558
|
+
path: 'cache-path-attach-recovery',
|
|
1559
|
+
protocolType: 'V2',
|
|
1560
|
+
} as any);
|
|
1561
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1562
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1563
|
+
{
|
|
1564
|
+
status: {
|
|
1565
|
+
device_id: 'stable-device-attach-recovery',
|
|
1566
|
+
unlocked: true,
|
|
1567
|
+
passphrase_enabled: true,
|
|
1568
|
+
attach_to_pin_enabled: true,
|
|
1569
|
+
},
|
|
1570
|
+
}
|
|
1571
|
+
);
|
|
1572
|
+
device.passphraseState = 'attach-state';
|
|
1573
|
+
preloadSessionCache('stable-device-attach-recovery', 'attach-state', 'expired-attach-session');
|
|
1574
|
+
let sessionGetCalls = 0;
|
|
1575
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1576
|
+
jest.fn((request: string) => {
|
|
1577
|
+
if (request === 'DeviceSessionGet') {
|
|
1578
|
+
sessionGetCalls += 1;
|
|
1579
|
+
return {
|
|
1580
|
+
type: 'DeviceSession',
|
|
1581
|
+
message:
|
|
1582
|
+
sessionGetCalls === 1
|
|
1583
|
+
? { session_id: 'current-standard-session', btc_test_address: 'standard-state' }
|
|
1584
|
+
: { session_id: 'new-attach-session', btc_test_address: 'attach-state' },
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
1591
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1592
|
+
const unlockDevice = jest
|
|
1593
|
+
.spyOn(device, 'unlockDevice')
|
|
1594
|
+
.mockResolvedValue(device.features as any);
|
|
1595
|
+
|
|
1596
|
+
await expect(
|
|
1597
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'attach-state' })
|
|
1598
|
+
).resolves.toMatchObject({
|
|
1599
|
+
passphraseState: 'attach-state',
|
|
1600
|
+
newSession: 'new-attach-session',
|
|
1601
|
+
resumed: false,
|
|
1602
|
+
});
|
|
1603
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1604
|
+
expect.objectContaining({
|
|
1605
|
+
reason: 'session-recovery',
|
|
1606
|
+
expectedPassphraseState: 'attach-state',
|
|
1607
|
+
existsAttachPinUser: true,
|
|
1608
|
+
}),
|
|
1609
|
+
{ cancelDeviceOnReject: false }
|
|
1610
|
+
);
|
|
1611
|
+
expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
|
|
1612
|
+
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
test('propagates DeviceLocked without retrying DeviceSessionAskPassphrase', async () => {
|
|
1209
1616
|
const device = Device.fromDescriptor({
|
|
1210
1617
|
id: 'cache-device-4',
|
|
1211
1618
|
path: 'cache-path-4',
|
|
@@ -1222,7 +1629,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1222
1629
|
jest
|
|
1223
1630
|
.fn()
|
|
1224
1631
|
.mockRejectedValueOnce(lockedError)
|
|
1225
|
-
.mockResolvedValueOnce({ message: { message: '
|
|
1632
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1226
1633
|
.mockResolvedValueOnce({
|
|
1227
1634
|
message: {
|
|
1228
1635
|
session_id: 'session-after-unlock',
|
|
@@ -1232,16 +1639,61 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1232
1639
|
);
|
|
1233
1640
|
(device as any).commands = {
|
|
1234
1641
|
typedCall,
|
|
1235
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1642
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1236
1643
|
};
|
|
1237
1644
|
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockResolvedValue(undefined as any);
|
|
1238
1645
|
|
|
1646
|
+
await expect(getProtocolV2WalletSession(device)).rejects.toBe(lockedError);
|
|
1647
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1648
|
+
expect(
|
|
1649
|
+
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
1650
|
+
).toHaveLength(1);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
test('does not act on cached lock state inside the wallet-session coordinator', async () => {
|
|
1654
|
+
const calls: string[] = [];
|
|
1655
|
+
const device = Device.fromDescriptor({
|
|
1656
|
+
id: 'cache-device-pre-unlock',
|
|
1657
|
+
path: 'cache-path-pre-unlock',
|
|
1658
|
+
protocolType: 'V2',
|
|
1659
|
+
} as any);
|
|
1660
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1661
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1662
|
+
{ status: { device_id: 'stable-device-pre-unlock', unlocked: false } }
|
|
1663
|
+
);
|
|
1664
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1665
|
+
jest.fn((request: string) => {
|
|
1666
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
1667
|
+
calls.push('ask-passphrase');
|
|
1668
|
+
return Promise.resolve({ message: { message: 'Passphrase prepared' } });
|
|
1669
|
+
}
|
|
1670
|
+
if (request === 'DeviceSessionGet') {
|
|
1671
|
+
calls.push('get-session');
|
|
1672
|
+
return Promise.resolve({
|
|
1673
|
+
message: {
|
|
1674
|
+
session_id: 'session-after-pre-unlock',
|
|
1675
|
+
btc_test_address: 'state-after-pre-unlock',
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1680
|
+
})
|
|
1681
|
+
);
|
|
1682
|
+
(device as any).commands = {
|
|
1683
|
+
typedCall,
|
|
1684
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
|
|
1685
|
+
};
|
|
1686
|
+
const unlockDevice = jest.spyOn(device, 'unlockDevice');
|
|
1687
|
+
|
|
1239
1688
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
1240
|
-
newSession: 'session-after-unlock',
|
|
1241
|
-
passphraseState: 'state-after-unlock',
|
|
1689
|
+
newSession: 'session-after-pre-unlock',
|
|
1690
|
+
passphraseState: 'state-after-pre-unlock',
|
|
1242
1691
|
});
|
|
1243
|
-
expect(unlockDevice).
|
|
1244
|
-
expect(
|
|
1692
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1693
|
+
expect(
|
|
1694
|
+
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
1695
|
+
).toHaveLength(1);
|
|
1696
|
+
expect(calls).toEqual(['ask-passphrase', 'get-session']);
|
|
1245
1697
|
});
|
|
1246
1698
|
|
|
1247
1699
|
test('does not request a Pro2 wallet session before features are initialized', async () => {
|
|
@@ -1384,8 +1836,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1384
1836
|
};
|
|
1385
1837
|
const typedCall = jest
|
|
1386
1838
|
.fn()
|
|
1839
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1387
1840
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1388
|
-
.mockResolvedValueOnce({ message: { message: '
|
|
1841
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1842
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1389
1843
|
.mockResolvedValueOnce({
|
|
1390
1844
|
type: 'DeviceSession',
|
|
1391
1845
|
message: {
|
|
@@ -1395,7 +1849,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1395
1849
|
});
|
|
1396
1850
|
const updateInternalState = jest.fn();
|
|
1397
1851
|
const getFeatures = jest.fn().mockResolvedValue(features);
|
|
1398
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1852
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1399
1853
|
const method = new GetPassphraseState({
|
|
1400
1854
|
payload: {
|
|
1401
1855
|
method: 'getPassphraseState',
|
|
@@ -1438,6 +1892,51 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1438
1892
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
1439
1893
|
});
|
|
1440
1894
|
|
|
1895
|
+
test('marks an interactive cache miss as a wallet-session recovery', async () => {
|
|
1896
|
+
const typedCall = jest
|
|
1897
|
+
.fn()
|
|
1898
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1899
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1900
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1901
|
+
.mockResolvedValueOnce({
|
|
1902
|
+
message: {
|
|
1903
|
+
btc_test_address: 'expected-state',
|
|
1904
|
+
session_id: 'recovered-session',
|
|
1905
|
+
},
|
|
1906
|
+
});
|
|
1907
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1908
|
+
const device = stubDevice({
|
|
1909
|
+
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
1910
|
+
passphraseState: 'expected-state',
|
|
1911
|
+
features: {
|
|
1912
|
+
unlocked: true,
|
|
1913
|
+
passphraseProtection: true,
|
|
1914
|
+
attachToPinEnabled: false,
|
|
1915
|
+
},
|
|
1916
|
+
commands: { typedCall, promptPassphrase },
|
|
1917
|
+
getInternalState: jest.fn().mockReturnValue(undefined),
|
|
1918
|
+
getCurrentDeviceId: () => 'pro2-device-id',
|
|
1919
|
+
getCurrentPassphraseProtection: () => true,
|
|
1920
|
+
updateInternalState: jest.fn(),
|
|
1921
|
+
clearInternalState: jest.fn(),
|
|
1922
|
+
emit: jest.fn(),
|
|
1923
|
+
}) as any;
|
|
1924
|
+
|
|
1925
|
+
await expect(
|
|
1926
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1927
|
+
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
1928
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1929
|
+
{
|
|
1930
|
+
existsAttachPinUser: false,
|
|
1931
|
+
deviceOnly: false,
|
|
1932
|
+
source: 'wallet-session-coordinator',
|
|
1933
|
+
reason: 'session-recovery',
|
|
1934
|
+
expectedPassphraseState: 'expected-state',
|
|
1935
|
+
},
|
|
1936
|
+
{ cancelDeviceOnReject: false }
|
|
1937
|
+
);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1441
1940
|
test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
|
|
1442
1941
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1443
1942
|
const typedCall = jest.fn().mockImplementation((request: string) => {
|
|
@@ -1445,7 +1944,16 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1445
1944
|
return Promise.resolve({ message: { version: 2 } });
|
|
1446
1945
|
}
|
|
1447
1946
|
if (request === 'DeviceSessionAskPassphrase') {
|
|
1448
|
-
return Promise.resolve({
|
|
1947
|
+
return Promise.resolve({
|
|
1948
|
+
type: 'Success',
|
|
1949
|
+
message: { message: 'Passphrase prepared' },
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
if (request === 'DeviceStatusGet') {
|
|
1953
|
+
return Promise.resolve({
|
|
1954
|
+
type: 'DeviceStatus',
|
|
1955
|
+
message: { unlocked: true, unlocked_attach_pin: false, passphrase_enabled: true },
|
|
1956
|
+
});
|
|
1449
1957
|
}
|
|
1450
1958
|
if (request === 'DeviceSessionGet') {
|
|
1451
1959
|
return Promise.resolve({
|
|
@@ -1471,7 +1979,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1471
1979
|
);
|
|
1472
1980
|
(device as any).commands = {
|
|
1473
1981
|
typedCall,
|
|
1474
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1982
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1475
1983
|
};
|
|
1476
1984
|
|
|
1477
1985
|
const method = new GetPassphraseState({
|
|
@@ -1497,7 +2005,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1497
2005
|
]);
|
|
1498
2006
|
});
|
|
1499
2007
|
|
|
1500
|
-
test('
|
|
2008
|
+
test('returns the Pro2 standard-wallet passphraseState when passphrase is disabled', async () => {
|
|
1501
2009
|
const features = {
|
|
1502
2010
|
deviceId: 'pro2-device-id',
|
|
1503
2011
|
deviceType: 'pro2',
|
|
@@ -1505,7 +2013,15 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1505
2013
|
passphraseProtection: false,
|
|
1506
2014
|
unlocked: true,
|
|
1507
2015
|
};
|
|
1508
|
-
const typedCall = createWalletSessionTypedCall(
|
|
2016
|
+
const typedCall = createWalletSessionTypedCall(
|
|
2017
|
+
jest.fn().mockResolvedValue({
|
|
2018
|
+
type: 'DeviceSession',
|
|
2019
|
+
message: {
|
|
2020
|
+
btc_test_address: 'main-wallet-state',
|
|
2021
|
+
session_id: 'main-wallet-session',
|
|
2022
|
+
},
|
|
2023
|
+
})
|
|
2024
|
+
);
|
|
1509
2025
|
const updateInternalState = jest.fn();
|
|
1510
2026
|
const method = new GetPassphraseState({
|
|
1511
2027
|
payload: {
|
|
@@ -1514,24 +2030,26 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1514
2030
|
useEmptyPassphrase: true,
|
|
1515
2031
|
},
|
|
1516
2032
|
});
|
|
1517
|
-
|
|
2033
|
+
const device = stubDevice({
|
|
1518
2034
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
1519
2035
|
features,
|
|
1520
2036
|
commands: {
|
|
1521
2037
|
typedCall,
|
|
1522
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2038
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1523
2039
|
},
|
|
1524
2040
|
updateInternalState,
|
|
1525
2041
|
getCurrentDeviceType: () => 'pro2',
|
|
1526
2042
|
getCurrentDeviceId: () => 'pro2-device-id',
|
|
1527
2043
|
getCurrentPassphraseProtection: () => false,
|
|
1528
2044
|
}) as any;
|
|
2045
|
+
device.unlockDevice = jest.fn().mockResolvedValue(features);
|
|
2046
|
+
method.device = device;
|
|
1529
2047
|
|
|
1530
|
-
await expect(method.run()).resolves.
|
|
2048
|
+
await expect(method.run()).resolves.toBe('main-wallet-state');
|
|
1531
2049
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
1532
2050
|
eventless_wallet_session: true,
|
|
1533
2051
|
});
|
|
1534
|
-
expect(updateInternalState).
|
|
2052
|
+
expect(updateInternalState).toHaveBeenCalled();
|
|
1535
2053
|
});
|
|
1536
2054
|
|
|
1537
2055
|
test('honors initSession when getting Pro2 passphrase state', async () => {
|
|
@@ -1545,8 +2063,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1545
2063
|
};
|
|
1546
2064
|
const typedCall = jest
|
|
1547
2065
|
.fn()
|
|
2066
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1548
2067
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1549
|
-
.mockResolvedValueOnce({ message: { message: '
|
|
2068
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
2069
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1550
2070
|
.mockResolvedValueOnce({
|
|
1551
2071
|
type: 'DeviceSession',
|
|
1552
2072
|
message: {
|
|
@@ -1569,7 +2089,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1569
2089
|
passphraseState: 'state-pro2-old',
|
|
1570
2090
|
commands: {
|
|
1571
2091
|
typedCall,
|
|
1572
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2092
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1573
2093
|
},
|
|
1574
2094
|
emit: jest.fn(),
|
|
1575
2095
|
clearInternalState,
|
|
@@ -1612,7 +2132,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1612
2132
|
};
|
|
1613
2133
|
(device as any).commands = {
|
|
1614
2134
|
typedCall,
|
|
1615
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2135
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1616
2136
|
};
|
|
1617
2137
|
|
|
1618
2138
|
await getPassphraseStateWithRefreshDeviceInfo(device);
|
|
@@ -1652,7 +2172,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1652
2172
|
);
|
|
1653
2173
|
(device as any).commands = {
|
|
1654
2174
|
typedCall,
|
|
1655
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2175
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1656
2176
|
};
|
|
1657
2177
|
|
|
1658
2178
|
await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({
|
|
@@ -1666,6 +2186,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1666
2186
|
expect(device.getInternalState()).toBeUndefined();
|
|
1667
2187
|
device.passphraseState = 'state-auto';
|
|
1668
2188
|
expect(device.getInternalState()).toBe('session-auto');
|
|
2189
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2190
|
+
passphrase: 'host hidden wallet',
|
|
2191
|
+
on_device: false,
|
|
2192
|
+
});
|
|
1669
2193
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1670
2194
|
});
|
|
1671
2195
|
|
|
@@ -1678,7 +2202,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1678
2202
|
session_id: 'main-pin-session',
|
|
1679
2203
|
btc_test_address: 'main-wallet-state',
|
|
1680
2204
|
},
|
|
1681
|
-
})
|
|
2205
|
+
}),
|
|
2206
|
+
{ passphrase_enabled: false }
|
|
1682
2207
|
);
|
|
1683
2208
|
|
|
1684
2209
|
(device as any).features = normalizeProtocolV2Features(
|
|
@@ -1700,8 +2225,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1700
2225
|
await expect(
|
|
1701
2226
|
getPassphraseStateWithRefreshDeviceInfo(device, { onlyMainPin: true })
|
|
1702
2227
|
).resolves.toMatchObject({
|
|
1703
|
-
passphraseState:
|
|
1704
|
-
newSession:
|
|
2228
|
+
passphraseState: 'main-wallet-state',
|
|
2229
|
+
newSession: 'main-pin-session',
|
|
1705
2230
|
});
|
|
1706
2231
|
|
|
1707
2232
|
expect(device.features?.passphraseProtection).toBe(false);
|
|
@@ -1733,7 +2258,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1733
2258
|
(device as any).features.unlocked = true;
|
|
1734
2259
|
(device as any).commands = {
|
|
1735
2260
|
typedCall,
|
|
1736
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2261
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1737
2262
|
};
|
|
1738
2263
|
|
|
1739
2264
|
await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
|
|
@@ -1744,8 +2269,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1744
2269
|
|
|
1745
2270
|
expect(device.getInternalState()).toBeUndefined();
|
|
1746
2271
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
1747
|
-
|
|
2272
|
+
passphrase: 'host hidden wallet',
|
|
2273
|
+
on_device: false,
|
|
1748
2274
|
});
|
|
2275
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2276
|
+
expect(typedCall).toHaveBeenNthCalledWith(4, 'DeviceSessionGet', 'DeviceSession', {});
|
|
1749
2277
|
});
|
|
1750
2278
|
|
|
1751
2279
|
test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
|
|
@@ -1778,7 +2306,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1778
2306
|
await expect(
|
|
1779
2307
|
device.checkPassphraseStateSafety('stale-hidden-state', true, false)
|
|
1780
2308
|
).resolves.toBe(true);
|
|
1781
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2309
|
+
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
1782
2310
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
1783
2311
|
eventless_wallet_session: true,
|
|
1784
2312
|
});
|
|
@@ -1836,7 +2364,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1836
2364
|
test('uses Protocol V2 features directly when profile is absent', () => {
|
|
1837
2365
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1838
2366
|
(device as any).features = {
|
|
1839
|
-
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any
|
|
2367
|
+
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2368
|
+
hw: { Device_type: DeviceType.PRO2 },
|
|
2369
|
+
}),
|
|
1840
2370
|
serialNo: 'CACHED-SERIAL',
|
|
1841
2371
|
label: 'Cached Label',
|
|
1842
2372
|
bleName: 'Cached BLE',
|
|
@@ -1845,6 +2375,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1845
2375
|
|
|
1846
2376
|
const message = device.toMessageObject();
|
|
1847
2377
|
expect(message).toMatchObject({
|
|
2378
|
+
connectProtocol: 'V2',
|
|
1848
2379
|
serialNo: 'CACHED-SERIAL',
|
|
1849
2380
|
uuid: 'CACHED-SERIAL',
|
|
1850
2381
|
deviceId: null,
|
|
@@ -1916,7 +2447,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1916
2447
|
typedCall: jest.fn().mockResolvedValueOnce({
|
|
1917
2448
|
type: 'DeviceInfo',
|
|
1918
2449
|
message: {
|
|
1919
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2450
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
1920
2451
|
fw: { application: { version: '1.2.3' } },
|
|
1921
2452
|
},
|
|
1922
2453
|
}),
|
|
@@ -2097,10 +2628,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2097
2628
|
.mockResolvedValueOnce({
|
|
2098
2629
|
type: 'DeviceInfo',
|
|
2099
2630
|
message: {
|
|
2100
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2631
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2101
2632
|
fw: { application: { version: '1.2.3' } },
|
|
2102
2633
|
},
|
|
2103
2634
|
})
|
|
2635
|
+
.mockResolvedValueOnce({
|
|
2636
|
+
type: 'ProtocolInfo',
|
|
2637
|
+
message: protocolV2ApplicationInfo,
|
|
2638
|
+
})
|
|
2104
2639
|
.mockResolvedValueOnce({
|
|
2105
2640
|
type: 'DeviceStatus',
|
|
2106
2641
|
message: { init_states: true, unlocked: true },
|
|
@@ -2121,7 +2656,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2121
2656
|
expect(device.features?.passphraseProtection).toBeNull();
|
|
2122
2657
|
expect(device.features?.label).toBeNull();
|
|
2123
2658
|
expect(device.features?.firmwareVersion).toBe('1.2.3');
|
|
2124
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2659
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
2125
2660
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2126
2661
|
1,
|
|
2127
2662
|
'DeviceInfoGet',
|
|
@@ -2145,10 +2680,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2145
2680
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
2146
2681
|
}
|
|
2147
2682
|
);
|
|
2148
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, '
|
|
2683
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
2684
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2149
2685
|
});
|
|
2150
2686
|
|
|
2151
|
-
test('recognizes bootloader
|
|
2687
|
+
test('recognizes bootloader from ProtocolInfo without calling DeviceStatusGet', async () => {
|
|
2152
2688
|
const device = Device.fromDescriptor({
|
|
2153
2689
|
path: 'usb-path',
|
|
2154
2690
|
protocolType: 'V2',
|
|
@@ -2163,7 +2699,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2163
2699
|
se1: {},
|
|
2164
2700
|
},
|
|
2165
2701
|
})
|
|
2166
|
-
.
|
|
2702
|
+
.mockResolvedValueOnce({
|
|
2703
|
+
type: 'ProtocolInfo',
|
|
2704
|
+
message: protocolV2BootloaderInfo,
|
|
2705
|
+
});
|
|
2167
2706
|
|
|
2168
2707
|
(device as any).commands = { typedCall };
|
|
2169
2708
|
|
|
@@ -2179,23 +2718,166 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2179
2718
|
}),
|
|
2180
2719
|
expect.anything()
|
|
2181
2720
|
);
|
|
2182
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, '
|
|
2721
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
2722
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2183
2723
|
expect(device.features).toMatchObject({
|
|
2184
2724
|
deviceId: null,
|
|
2185
2725
|
bootloaderMode: true,
|
|
2726
|
+
mode: 'bootloader',
|
|
2186
2727
|
initialized: null,
|
|
2187
2728
|
});
|
|
2729
|
+
expect(device.isBootloader()).toBe(true);
|
|
2730
|
+
expect(device.isRomloader()).toBe(false);
|
|
2188
2731
|
});
|
|
2189
2732
|
|
|
2190
|
-
test('
|
|
2733
|
+
test('recognizes romloader from ProtocolInfo without calling DeviceStatusGet', async () => {
|
|
2191
2734
|
const device = Device.fromDescriptor({
|
|
2192
2735
|
path: 'usb-path',
|
|
2193
2736
|
protocolType: 'V2',
|
|
2194
2737
|
} as any);
|
|
2195
2738
|
const typedCall = jest.fn().mockResolvedValue({
|
|
2196
|
-
type: '
|
|
2197
|
-
message:
|
|
2739
|
+
type: 'ProtocolInfo',
|
|
2740
|
+
message: protocolV2RomloaderInfo,
|
|
2741
|
+
});
|
|
2742
|
+
(device as any).commands = { typedCall };
|
|
2743
|
+
|
|
2744
|
+
await device.probeProtocolV2RuntimeState({
|
|
2745
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2746
|
+
fw: { romloader: { version: '1.0.0' } },
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2750
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2751
|
+
expect(device.features).toMatchObject({
|
|
2752
|
+
mode: 'romloader',
|
|
2753
|
+
bootloaderMode: true,
|
|
2754
|
+
initialized: null,
|
|
2755
|
+
});
|
|
2756
|
+
expect(device.isBootloader()).toBe(false);
|
|
2757
|
+
expect(device.isRomloader()).toBe(true);
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
test('rejects Protocol V2 romloader mode for non-Pro2 devices', async () => {
|
|
2761
|
+
const device = Device.fromDescriptor({
|
|
2762
|
+
path: 'pro-v2-path',
|
|
2763
|
+
protocolType: 'V2',
|
|
2764
|
+
} as any);
|
|
2765
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
2766
|
+
type: 'ProtocolInfo',
|
|
2767
|
+
message: protocolV2RomloaderInfo,
|
|
2198
2768
|
});
|
|
2769
|
+
(device as any).commands = { typedCall };
|
|
2770
|
+
|
|
2771
|
+
await expect(
|
|
2772
|
+
device.probeProtocolV2RuntimeState({
|
|
2773
|
+
hw: { Device_type: DeviceType.PRO, serial_no: 'PRO-SERIAL' },
|
|
2774
|
+
fw: { romloader: { version: '1.0.0' } },
|
|
2775
|
+
})
|
|
2776
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
2777
|
+
|
|
2778
|
+
expect(device.isRomloader()).not.toBe(true);
|
|
2779
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
test('keeps application mode without calling an unadvertised DeviceStatusGet', async () => {
|
|
2783
|
+
const device = Device.fromDescriptor({
|
|
2784
|
+
path: 'usb-path',
|
|
2785
|
+
protocolType: 'V2',
|
|
2786
|
+
} as any);
|
|
2787
|
+
const protocolInfo = {
|
|
2788
|
+
...protocolV2ApplicationInfo,
|
|
2789
|
+
supported_messages: [],
|
|
2790
|
+
};
|
|
2791
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
2792
|
+
type: 'ProtocolInfo',
|
|
2793
|
+
message: protocolInfo,
|
|
2794
|
+
});
|
|
2795
|
+
(device as any).commands = { typedCall };
|
|
2796
|
+
|
|
2797
|
+
await device.probeProtocolV2RuntimeState({
|
|
2798
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
2799
|
+
fw: { application: { version: '1.2.3' } },
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2803
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2804
|
+
expect(device.features).toMatchObject({
|
|
2805
|
+
mode: 'normal',
|
|
2806
|
+
bootloaderMode: false,
|
|
2807
|
+
initialized: null,
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
test('uses advertised DeviceStatusGet as compatibility fallback for an unknown fingerprint', async () => {
|
|
2812
|
+
const device = Device.fromDescriptor({
|
|
2813
|
+
path: 'usb-path',
|
|
2814
|
+
protocolType: 'V2',
|
|
2815
|
+
} as any);
|
|
2816
|
+
const protocolInfo = {
|
|
2817
|
+
...protocolV2ApplicationInfo,
|
|
2818
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2819
|
+
};
|
|
2820
|
+
const typedCall = jest
|
|
2821
|
+
.fn()
|
|
2822
|
+
.mockResolvedValueOnce({ type: 'ProtocolInfo', message: protocolInfo })
|
|
2823
|
+
.mockResolvedValueOnce({
|
|
2824
|
+
type: 'DeviceStatus',
|
|
2825
|
+
message: { init_states: true, unlocked: true },
|
|
2826
|
+
});
|
|
2827
|
+
(device as any).commands = { typedCall };
|
|
2828
|
+
|
|
2829
|
+
await device.probeProtocolV2RuntimeState({
|
|
2830
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
2831
|
+
fw: { application: { version: '1.2.3' } },
|
|
2832
|
+
});
|
|
2833
|
+
|
|
2834
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
2835
|
+
expect(device.features).toMatchObject({
|
|
2836
|
+
mode: 'normal',
|
|
2837
|
+
initialized: true,
|
|
2838
|
+
unlocked: true,
|
|
2839
|
+
});
|
|
2840
|
+
});
|
|
2841
|
+
|
|
2842
|
+
test('fails safely when runtime fingerprint and capabilities are both unknown', async () => {
|
|
2843
|
+
const device = Device.fromDescriptor({
|
|
2844
|
+
path: 'usb-path',
|
|
2845
|
+
protocolType: 'V2',
|
|
2846
|
+
} as any);
|
|
2847
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
2848
|
+
type: 'ProtocolInfo',
|
|
2849
|
+
message: {
|
|
2850
|
+
version: 1,
|
|
2851
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2852
|
+
supported_messages: [],
|
|
2853
|
+
},
|
|
2854
|
+
});
|
|
2855
|
+
(device as any).commands = { typedCall };
|
|
2856
|
+
|
|
2857
|
+
await expect(
|
|
2858
|
+
device.probeProtocolV2RuntimeState({
|
|
2859
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
2860
|
+
fw: { application: { version: '1.2.3' } },
|
|
2861
|
+
})
|
|
2862
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
2863
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
test('does not reinterpret a state update error as runtime detection failure', async () => {
|
|
2867
|
+
const device = Device.fromDescriptor({
|
|
2868
|
+
path: 'usb-path',
|
|
2869
|
+
protocolType: 'V2',
|
|
2870
|
+
} as any);
|
|
2871
|
+
const typedCall = jest
|
|
2872
|
+
.fn()
|
|
2873
|
+
.mockResolvedValueOnce({
|
|
2874
|
+
type: 'ProtocolInfo',
|
|
2875
|
+
message: protocolV2ApplicationInfo,
|
|
2876
|
+
})
|
|
2877
|
+
.mockResolvedValueOnce({
|
|
2878
|
+
type: 'DeviceStatus',
|
|
2879
|
+
message: { init_states: true, unlocked: true },
|
|
2880
|
+
});
|
|
2199
2881
|
const updateProtocolV2Features = jest
|
|
2200
2882
|
.spyOn(device, 'updateProtocolV2Features')
|
|
2201
2883
|
.mockImplementation(() => {
|
|
@@ -2210,12 +2892,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2210
2892
|
})
|
|
2211
2893
|
).rejects.toThrow('state update failed');
|
|
2212
2894
|
|
|
2213
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2895
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
2214
2896
|
expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
|
|
2215
2897
|
expect(updateProtocolV2Features).toHaveBeenCalledWith(
|
|
2216
2898
|
expect.anything(),
|
|
2217
2899
|
expect.objectContaining({ init_states: true }),
|
|
2218
|
-
'normal'
|
|
2900
|
+
'normal',
|
|
2901
|
+
protocolV2ApplicationInfo
|
|
2219
2902
|
);
|
|
2220
2903
|
});
|
|
2221
2904
|
|
|
@@ -2229,10 +2912,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2229
2912
|
.mockResolvedValueOnce({
|
|
2230
2913
|
type: 'DeviceInfo',
|
|
2231
2914
|
message: {
|
|
2232
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2915
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2233
2916
|
fw: { application: { version: '1.2.3' } },
|
|
2234
2917
|
},
|
|
2235
2918
|
})
|
|
2919
|
+
.mockResolvedValueOnce({
|
|
2920
|
+
type: 'ProtocolInfo',
|
|
2921
|
+
message: protocolV2ApplicationInfo,
|
|
2922
|
+
})
|
|
2236
2923
|
.mockResolvedValueOnce({
|
|
2237
2924
|
type: 'DeviceStatus',
|
|
2238
2925
|
message: { init_states: true, unlocked: true },
|
|
@@ -2240,10 +2927,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2240
2927
|
.mockResolvedValueOnce({
|
|
2241
2928
|
type: 'DeviceInfo',
|
|
2242
2929
|
message: {
|
|
2243
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2930
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2244
2931
|
fw: { application: { version: '1.2.4' } },
|
|
2245
2932
|
},
|
|
2246
2933
|
})
|
|
2934
|
+
.mockResolvedValueOnce({
|
|
2935
|
+
type: 'ProtocolInfo',
|
|
2936
|
+
message: protocolV2ApplicationInfo,
|
|
2937
|
+
})
|
|
2247
2938
|
.mockResolvedValueOnce({
|
|
2248
2939
|
type: 'DeviceStatus',
|
|
2249
2940
|
message: { init_states: true, unlocked: true },
|
|
@@ -2265,9 +2956,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2265
2956
|
firmwareVersion: '1.2.4',
|
|
2266
2957
|
passphraseProtection: null,
|
|
2267
2958
|
});
|
|
2268
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2959
|
+
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
2269
2960
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2270
|
-
|
|
2961
|
+
4,
|
|
2271
2962
|
'DeviceInfoGet',
|
|
2272
2963
|
'DeviceInfo',
|
|
2273
2964
|
{
|
|
@@ -2292,14 +2983,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2292
2983
|
|
|
2293
2984
|
test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
|
|
2294
2985
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2295
|
-
const typedCall = jest
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2986
|
+
const typedCall = jest
|
|
2987
|
+
.fn()
|
|
2988
|
+
.mockResolvedValueOnce({
|
|
2989
|
+
type: 'DeviceInfo',
|
|
2990
|
+
message: {
|
|
2991
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
2992
|
+
fw: { application: { version: '1.2.4' } },
|
|
2993
|
+
},
|
|
2994
|
+
})
|
|
2995
|
+
.mockResolvedValueOnce({
|
|
2996
|
+
type: 'ProtocolInfo',
|
|
2997
|
+
message: protocolV2ApplicationInfo,
|
|
2998
|
+
})
|
|
2999
|
+
.mockResolvedValueOnce({
|
|
3000
|
+
type: 'DeviceStatus',
|
|
3001
|
+
message: { init_states: true, passphrase_enabled: true },
|
|
3002
|
+
});
|
|
2303
3003
|
(device as any).commands = { typedCall };
|
|
2304
3004
|
|
|
2305
3005
|
await device.initialize();
|
|
@@ -2343,7 +3043,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2343
3043
|
if (requestType === 'DeviceSessionAskPin') {
|
|
2344
3044
|
return {
|
|
2345
3045
|
type: 'Success',
|
|
2346
|
-
message: { message: '
|
|
3046
|
+
message: { message: 'PIN verified' },
|
|
2347
3047
|
};
|
|
2348
3048
|
}
|
|
2349
3049
|
if (requestType === 'DeviceStatusGet') {
|
|
@@ -2365,7 +3065,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2365
3065
|
(device as any).features = normalizeProtocolV2Features(
|
|
2366
3066
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
2367
3067
|
{
|
|
2368
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
3068
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2369
3069
|
fw: { application: { version: '1.2.3' } },
|
|
2370
3070
|
status: { init_states: true },
|
|
2371
3071
|
}
|
|
@@ -2400,7 +3100,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2400
3100
|
if (requestType === 'DeviceSessionAskPin') {
|
|
2401
3101
|
return {
|
|
2402
3102
|
type: 'Success',
|
|
2403
|
-
message: { message: '
|
|
3103
|
+
message: { message: 'PIN verified' },
|
|
2404
3104
|
};
|
|
2405
3105
|
}
|
|
2406
3106
|
if (requestType === 'DeviceStatusGet') {
|
|
@@ -2899,6 +3599,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2899
3599
|
message: { init_states: true, unlocked: true },
|
|
2900
3600
|
});
|
|
2901
3601
|
}
|
|
3602
|
+
if (name === 'ProtocolInfoRequest') {
|
|
3603
|
+
return Promise.resolve({
|
|
3604
|
+
type: 'ProtocolInfo',
|
|
3605
|
+
message: protocolV2ApplicationInfo,
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
2902
3608
|
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
2903
3609
|
});
|
|
2904
3610
|
const commands = { typedCall };
|
|
@@ -2943,7 +3649,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2943
3649
|
},
|
|
2944
3650
|
{ timeoutMs: 5000 }
|
|
2945
3651
|
);
|
|
2946
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3652
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
3653
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3654
|
+
'DeviceInfoGet',
|
|
3655
|
+
'ProtocolInfoRequest',
|
|
3656
|
+
'DeviceStatusGet',
|
|
3657
|
+
]);
|
|
2947
3658
|
expect(typedCall).not.toHaveBeenCalledWith('Initialize', 'Features', {});
|
|
2948
3659
|
expect(versions).toEqual({
|
|
2949
3660
|
bootloaderVersion: '0.0.0',
|
|
@@ -3023,7 +3734,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3023
3734
|
message: protocolV2BootloaderDeviceInfo,
|
|
3024
3735
|
});
|
|
3025
3736
|
}
|
|
3026
|
-
|
|
3737
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
3738
|
+
return Promise.resolve({
|
|
3739
|
+
type: 'ProtocolInfo',
|
|
3740
|
+
message: protocolV2BootloaderInfo,
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
return Promise.reject(new Error(`unexpected call ${requestType}`));
|
|
3027
3744
|
});
|
|
3028
3745
|
const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
|
|
3029
3746
|
(method as any).device.isBootloader = () => true;
|
|
@@ -3050,10 +3767,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3050
3767
|
method: 'firmwareUpdateV4',
|
|
3051
3768
|
},
|
|
3052
3769
|
});
|
|
3770
|
+
const isRomloader = jest.fn(() => true);
|
|
3053
3771
|
(method as any).device = stubDevice({
|
|
3054
3772
|
originalDescriptor: { id: 'usb-id', path: 'romloader-path', protocolType: 'V2' },
|
|
3055
|
-
features: {
|
|
3773
|
+
features: {
|
|
3774
|
+
deviceType: 'pro2',
|
|
3775
|
+
mode: 'romloader',
|
|
3776
|
+
bootloaderMode: false,
|
|
3777
|
+
capabilities: [],
|
|
3778
|
+
},
|
|
3056
3779
|
isBootloader: () => false,
|
|
3780
|
+
isRomloader,
|
|
3057
3781
|
});
|
|
3058
3782
|
(method as any).protocolV2Reboot = jest
|
|
3059
3783
|
.fn()
|
|
@@ -3062,7 +3786,49 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3062
3786
|
|
|
3063
3787
|
await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
|
|
3064
3788
|
|
|
3789
|
+
expect(isRomloader).toHaveBeenCalledTimes(1);
|
|
3790
|
+
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
3791
|
+
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
3792
|
+
});
|
|
3793
|
+
|
|
3794
|
+
test('starts Protocol V2 firmware transfer directly from romloader mode', async () => {
|
|
3795
|
+
const method = new FirmwareUpdateV4({
|
|
3796
|
+
id: 1,
|
|
3797
|
+
payload: {
|
|
3798
|
+
method: 'firmwareUpdateV4',
|
|
3799
|
+
platform: 'web',
|
|
3800
|
+
coprocessorBinary: new Uint8Array([1]).buffer,
|
|
3801
|
+
},
|
|
3802
|
+
});
|
|
3803
|
+
method.init();
|
|
3804
|
+
(method as any).device = stubDevice({
|
|
3805
|
+
originalDescriptor: { id: 'usb-id', path: 'romloader-path', protocolType: 'V2' },
|
|
3806
|
+
features: {
|
|
3807
|
+
deviceType: 'pro2',
|
|
3808
|
+
firmwareVersion: '0.0.0',
|
|
3809
|
+
mode: 'romloader',
|
|
3810
|
+
bootloaderMode: true,
|
|
3811
|
+
capabilities: [],
|
|
3812
|
+
},
|
|
3813
|
+
});
|
|
3814
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
3815
|
+
(method as any).protocolV2Reboot = jest.fn();
|
|
3816
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
3817
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
3818
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
3819
|
+
bootloaderVersion: '1.0.0',
|
|
3820
|
+
bleVersion: '0.0.0',
|
|
3821
|
+
firmwareVersion: '1.0.0',
|
|
3822
|
+
});
|
|
3823
|
+
method.postTipMessage = jest.fn();
|
|
3824
|
+
|
|
3825
|
+
await method.run();
|
|
3826
|
+
|
|
3065
3827
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
3828
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
3829
|
+
bootloaderBinary: null,
|
|
3830
|
+
fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
|
|
3831
|
+
});
|
|
3066
3832
|
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
3067
3833
|
});
|
|
3068
3834
|
|
|
@@ -3085,7 +3851,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3085
3851
|
message: protocolV2BootloaderDeviceInfo,
|
|
3086
3852
|
});
|
|
3087
3853
|
}
|
|
3088
|
-
|
|
3854
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
3855
|
+
return Promise.resolve({
|
|
3856
|
+
type: 'ProtocolInfo',
|
|
3857
|
+
message: protocolV2BootloaderInfo,
|
|
3858
|
+
});
|
|
3859
|
+
}
|
|
3860
|
+
return Promise.reject(new Error(`unexpected call ${requestType}`));
|
|
3089
3861
|
});
|
|
3090
3862
|
const reconnectProtocolV2Device = jest
|
|
3091
3863
|
.fn()
|
|
@@ -3102,8 +3874,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3102
3874
|
await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
|
|
3103
3875
|
|
|
3104
3876
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
|
|
3105
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3106
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3877
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
3878
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3879
|
+
'DeviceInfoGet',
|
|
3880
|
+
'ProtocolInfoRequest',
|
|
3881
|
+
]);
|
|
3107
3882
|
});
|
|
3108
3883
|
|
|
3109
3884
|
test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
|
|
@@ -3396,7 +4171,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3396
4171
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
3397
4172
|
});
|
|
3398
4173
|
|
|
3399
|
-
test('accepts a missing firmware status handler
|
|
4174
|
+
test('accepts a missing firmware status handler only after confirming normal mode', async () => {
|
|
3400
4175
|
const method = new FirmwareUpdateV4({
|
|
3401
4176
|
id: 1,
|
|
3402
4177
|
payload: {
|
|
@@ -3412,7 +4187,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3412
4187
|
getCommands: () => ({ typedCall }),
|
|
3413
4188
|
});
|
|
3414
4189
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
3415
|
-
|
|
4190
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4191
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4192
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
3416
4193
|
method.postProgressMessage = jest.fn();
|
|
3417
4194
|
|
|
3418
4195
|
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
@@ -3421,6 +4198,70 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3421
4198
|
|
|
3422
4199
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
3423
4200
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
|
|
4201
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
4202
|
+
});
|
|
4203
|
+
|
|
4204
|
+
test.each([
|
|
4205
|
+
[{ mode: 'normal', bootloaderMode: false }, true],
|
|
4206
|
+
[{ mode: 'bootloader', bootloaderMode: true }, false],
|
|
4207
|
+
])('derives firmware completion from the runtime-state probe: %o', async (features, expected) => {
|
|
4208
|
+
const method = new FirmwareUpdateV4({
|
|
4209
|
+
id: 1,
|
|
4210
|
+
payload: {
|
|
4211
|
+
method: 'firmwareUpdateV4',
|
|
4212
|
+
},
|
|
4213
|
+
});
|
|
4214
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4215
|
+
const probeProtocolV2RuntimeState = jest.fn().mockResolvedValue(features);
|
|
4216
|
+
(method as any).device = stubDevice({ probeProtocolV2RuntimeState });
|
|
4217
|
+
|
|
4218
|
+
await expect((method as any).probeProtocolV2NormalMode(deviceInfo)).resolves.toBe(expected);
|
|
4219
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
|
|
4220
|
+
});
|
|
4221
|
+
|
|
4222
|
+
test('keeps polling when the firmware status handler is missing in loader mode', async () => {
|
|
4223
|
+
const method = new FirmwareUpdateV4({
|
|
4224
|
+
id: 1,
|
|
4225
|
+
payload: {
|
|
4226
|
+
method: 'firmwareUpdateV4',
|
|
4227
|
+
},
|
|
4228
|
+
});
|
|
4229
|
+
const typedCall = jest
|
|
4230
|
+
.fn()
|
|
4231
|
+
.mockRejectedValueOnce(new Error('Failure: Handler not registered for this message'))
|
|
4232
|
+
.mockResolvedValueOnce({
|
|
4233
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
4234
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
4235
|
+
});
|
|
4236
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4237
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
4238
|
+
callback: () => void
|
|
4239
|
+
) => {
|
|
4240
|
+
callback();
|
|
4241
|
+
return 0 as any;
|
|
4242
|
+
}) as typeof setTimeout);
|
|
4243
|
+
|
|
4244
|
+
(method as any).device = stubDevice({
|
|
4245
|
+
getCommands: () => ({ typedCall }),
|
|
4246
|
+
});
|
|
4247
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4248
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4249
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(false);
|
|
4250
|
+
method.postProgressMessage = jest.fn();
|
|
4251
|
+
|
|
4252
|
+
try {
|
|
4253
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
4254
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
4255
|
+
]);
|
|
4256
|
+
} finally {
|
|
4257
|
+
setTimeoutSpy.mockRestore();
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
4261
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
4262
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
4263
|
+
]);
|
|
4264
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
3424
4265
|
});
|
|
3425
4266
|
|
|
3426
4267
|
test('polls target status after update ACK and finishes when all targets complete', async () => {
|
|
@@ -3926,6 +4767,62 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3926
4767
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
3927
4768
|
});
|
|
3928
4769
|
|
|
4770
|
+
test('ignores an unsupported remote ROMLOADER component when installing selected targets', async () => {
|
|
4771
|
+
const method = new FirmwareUpdateV4({
|
|
4772
|
+
id: 1,
|
|
4773
|
+
payload: {
|
|
4774
|
+
method: 'firmwareUpdateV4',
|
|
4775
|
+
platform: 'web',
|
|
4776
|
+
targetsToUpdate: ['app_v1'],
|
|
4777
|
+
},
|
|
4778
|
+
});
|
|
4779
|
+
method.init();
|
|
4780
|
+
const applicationBinary = new Uint8Array([1]).buffer;
|
|
4781
|
+
const downloadRemoteComponent = jest.fn().mockResolvedValue({
|
|
4782
|
+
fileName: 'application_p1.bin',
|
|
4783
|
+
binary: applicationBinary,
|
|
4784
|
+
targetId: 4,
|
|
4785
|
+
kind: 'firmware',
|
|
4786
|
+
});
|
|
4787
|
+
(method as any).downloadRemoteProtocolV2Component = downloadRemoteComponent;
|
|
4788
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
4789
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
4790
|
+
.mockReturnValue({
|
|
4791
|
+
required: false,
|
|
4792
|
+
version: [1, 0, 0],
|
|
4793
|
+
url: '',
|
|
4794
|
+
installOrder: ['romloader', 'applicationP1'],
|
|
4795
|
+
components: {
|
|
4796
|
+
romloader: {
|
|
4797
|
+
target: 'ROMLOADER',
|
|
4798
|
+
url: 'https://example.com/romloader.bin',
|
|
4799
|
+
},
|
|
4800
|
+
applicationP1: {
|
|
4801
|
+
target: 'APPLICATION_P1',
|
|
4802
|
+
url: 'https://example.com/application-p1.bin',
|
|
4803
|
+
},
|
|
4804
|
+
},
|
|
4805
|
+
fingerprint: '',
|
|
4806
|
+
changelog: { 'zh-CN': '', 'en-US': '' },
|
|
4807
|
+
});
|
|
4808
|
+
|
|
4809
|
+
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
|
|
4810
|
+
deviceType: 'pro2',
|
|
4811
|
+
firmwareVersion: '0.0.0',
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4814
|
+
expect(downloadRemoteComponent).toHaveBeenCalledTimes(1);
|
|
4815
|
+
expect(downloadRemoteComponent).toHaveBeenCalledWith(
|
|
4816
|
+
'applicationP1',
|
|
4817
|
+
expect.objectContaining({ target: 'APPLICATION_P1' })
|
|
4818
|
+
);
|
|
4819
|
+
expect(remoteBinaries.fwBinaryMap).toEqual([
|
|
4820
|
+
{ fileName: 'application_p1.bin', binary: applicationBinary, targetId: 4 },
|
|
4821
|
+
]);
|
|
4822
|
+
|
|
4823
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
4824
|
+
});
|
|
4825
|
+
|
|
3929
4826
|
test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
|
|
3930
4827
|
const method = new FirmwareUpdateV4({
|
|
3931
4828
|
id: 1,
|
|
@@ -4496,8 +5393,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4496
5393
|
return 0 as any;
|
|
4497
5394
|
}) as typeof setTimeout);
|
|
4498
5395
|
|
|
5396
|
+
const initialize = jest.fn().mockResolvedValue(undefined);
|
|
4499
5397
|
(method as any).device = stubDevice({
|
|
4500
5398
|
getCommands: () => ({ typedCall }),
|
|
5399
|
+
initialize,
|
|
5400
|
+
});
|
|
5401
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5402
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue({
|
|
5403
|
+
hw: { serial_no: 'PRO2-PHYSICAL-1' },
|
|
4501
5404
|
});
|
|
4502
5405
|
method.postProgressMessage = jest.fn();
|
|
4503
5406
|
|
|
@@ -4513,6 +5416,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4513
5416
|
}
|
|
4514
5417
|
|
|
4515
5418
|
expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
|
|
5419
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
5420
|
+
expect((method as any).verifyProtocolV2ReconnectIdentity).toHaveBeenCalledTimes(1);
|
|
5421
|
+
expect(initialize).toHaveBeenCalledTimes(1);
|
|
4516
5422
|
});
|
|
4517
5423
|
|
|
4518
5424
|
test('rejects a chunk-relative processed_byte during firmware staging', async () => {
|
|
@@ -5213,6 +6119,45 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5213
6119
|
]);
|
|
5214
6120
|
});
|
|
5215
6121
|
|
|
6122
|
+
test('does not replay a business method when the hidden-wallet session cannot be restored after unlock', async () => {
|
|
6123
|
+
const initialError = deviceLockedError();
|
|
6124
|
+
const restoreError = new Error('Invalid wallet session');
|
|
6125
|
+
const method = {
|
|
6126
|
+
name: 'evmSignMessage',
|
|
6127
|
+
payload: { passphraseState: 'hidden-state' },
|
|
6128
|
+
useDevicePassphraseState: true,
|
|
6129
|
+
unlockPolicy: 'retry-on-locked',
|
|
6130
|
+
run: jest.fn().mockRejectedValueOnce(initialError),
|
|
6131
|
+
};
|
|
6132
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
6133
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
6134
|
+
return Promise.resolve({ message: { version: 2 } });
|
|
6135
|
+
}
|
|
6136
|
+
if (requestType === 'DeviceSessionGet') {
|
|
6137
|
+
return Promise.reject(restoreError);
|
|
6138
|
+
}
|
|
6139
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
6140
|
+
});
|
|
6141
|
+
const device = {
|
|
6142
|
+
features: { unlocked: true, passphraseProtection: true },
|
|
6143
|
+
passphraseState: 'hidden-state',
|
|
6144
|
+
commands: { typedCall },
|
|
6145
|
+
isProtocolV2: () => true,
|
|
6146
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6147
|
+
getCurrentPassphraseProtection: () => true,
|
|
6148
|
+
getInternalState: () => 'hidden-session',
|
|
6149
|
+
clearInternalState: jest.fn(),
|
|
6150
|
+
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6151
|
+
updateInternalState: jest.fn(),
|
|
6152
|
+
};
|
|
6153
|
+
|
|
6154
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(restoreError);
|
|
6155
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6156
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6157
|
+
expect(device.clearInternalState).toHaveBeenCalledTimes(1);
|
|
6158
|
+
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
6159
|
+
});
|
|
6160
|
+
|
|
5216
6161
|
test('restores the expected hidden-wallet session after pre-unlock without selecting Attach PIN', async () => {
|
|
5217
6162
|
const calls: string[] = [];
|
|
5218
6163
|
const method = {
|
|
@@ -5300,7 +6245,7 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5300
6245
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5301
6246
|
});
|
|
5302
6247
|
|
|
5303
|
-
test('
|
|
6248
|
+
test('refreshes status and unlocks before a protected Protocol V2 method', async () => {
|
|
5304
6249
|
const calls: string[] = [];
|
|
5305
6250
|
const method = {
|
|
5306
6251
|
name: 'deviceSettingsPageShow',
|
|
@@ -5311,11 +6256,23 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5311
6256
|
return Promise.resolve({ message: 'ok' });
|
|
5312
6257
|
}),
|
|
5313
6258
|
};
|
|
6259
|
+
const features = { unlocked: true };
|
|
5314
6260
|
const device = {
|
|
5315
|
-
features
|
|
6261
|
+
features,
|
|
6262
|
+
commands: {
|
|
6263
|
+
typedCall: jest.fn(() => {
|
|
6264
|
+
calls.push('refresh-status');
|
|
6265
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
6266
|
+
}),
|
|
6267
|
+
},
|
|
5316
6268
|
isProtocolV2: () => true,
|
|
6269
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6270
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6271
|
+
return features;
|
|
6272
|
+
}),
|
|
5317
6273
|
unlockDevice: jest.fn(() => {
|
|
5318
6274
|
calls.push('unlock');
|
|
6275
|
+
features.unlocked = true;
|
|
5319
6276
|
return Promise.resolve();
|
|
5320
6277
|
}),
|
|
5321
6278
|
};
|
|
@@ -5328,12 +6285,66 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5328
6285
|
await expect(
|
|
5329
6286
|
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
5330
6287
|
).resolves.toEqual({ message: 'ok' });
|
|
5331
|
-
expect(calls).toEqual(['unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6288
|
+
expect(calls).toEqual(['refresh-status', 'unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6289
|
+
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
5332
6290
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5333
6291
|
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
5334
6292
|
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
5335
6293
|
});
|
|
5336
6294
|
|
|
6295
|
+
test('uses refreshed unlocked status instead of a stale locked cache', async () => {
|
|
6296
|
+
const features = { unlocked: false };
|
|
6297
|
+
const method = {
|
|
6298
|
+
name: 'deviceSettings',
|
|
6299
|
+
unlockPolicy: 'unlock-before-run',
|
|
6300
|
+
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6301
|
+
};
|
|
6302
|
+
const device = {
|
|
6303
|
+
features,
|
|
6304
|
+
commands: {
|
|
6305
|
+
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
|
|
6306
|
+
},
|
|
6307
|
+
isProtocolV2: () => true,
|
|
6308
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6309
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6310
|
+
return features;
|
|
6311
|
+
}),
|
|
6312
|
+
unlockDevice: jest.fn(),
|
|
6313
|
+
};
|
|
6314
|
+
|
|
6315
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6316
|
+
message: 'ok',
|
|
6317
|
+
});
|
|
6318
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6319
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6320
|
+
});
|
|
6321
|
+
|
|
6322
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
6323
|
+
'skips DeviceStatus and unlock in Protocol V2 %s mode',
|
|
6324
|
+
async mode => {
|
|
6325
|
+
const method = {
|
|
6326
|
+
name: 'firmwareUpdateV4',
|
|
6327
|
+
unlockPolicy: 'unlock-before-run',
|
|
6328
|
+
run: jest.fn().mockResolvedValue({ message: 'updating' }),
|
|
6329
|
+
};
|
|
6330
|
+
const device = {
|
|
6331
|
+
features: { unlocked: false },
|
|
6332
|
+
commands: { typedCall: jest.fn() },
|
|
6333
|
+
isProtocolV2: () => true,
|
|
6334
|
+
isBootloader: () => mode === 'bootloader',
|
|
6335
|
+
isRomloader: () => mode === 'romloader',
|
|
6336
|
+
unlockDevice: jest.fn(),
|
|
6337
|
+
};
|
|
6338
|
+
|
|
6339
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6340
|
+
message: 'updating',
|
|
6341
|
+
});
|
|
6342
|
+
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
6343
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6344
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6345
|
+
}
|
|
6346
|
+
);
|
|
6347
|
+
|
|
5337
6348
|
test('infers a signing interaction before running a Protocol V2 business method', async () => {
|
|
5338
6349
|
const method = {
|
|
5339
6350
|
name: 'evmSignMessage',
|