@onekeyfe/hd-core 1.2.0-alpha.34 → 1.2.0-alpha.36
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__/AllNetworkGetAddressBase.tracing.test.ts +74 -0
- package/__tests__/DeviceCommands.test.ts +84 -0
- package/__tests__/DeviceEventRegistration.test.ts +6 -0
- package/__tests__/device-settings.test.ts +3 -0
- package/__tests__/device-wallet-session-store.test.ts +80 -7
- package/__tests__/evmSignTransaction.test.ts +69 -0
- package/__tests__/get-device-state.test.ts +180 -40
- package/__tests__/open-wallet-session.test.ts +314 -55
- package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
- package/__tests__/protocol-v2.test.ts +768 -139
- package/dist/api/FirmwareUpdateV4.d.ts +1 -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/allnetwork/AllNetworkGetAddressBase.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/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
- package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
- package/dist/core/deviceEventRegistration.d.ts +2 -0
- package/dist/core/deviceEventRegistration.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +25 -6
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceWalletSessionStore.d.ts +0 -1
- package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
- package/dist/events/device.d.ts +4 -0
- package/dist/events/device.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +27 -2
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +63 -12
- package/dist/index.js +1164 -904
- 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/uiInteraction.d.ts +3 -3
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +5 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -1
- package/dist/types/api/openWalletSession.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 +2 -1
- package/dist/types/device.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
- 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/package.json +4 -4
- package/src/api/BaseMethod.ts +1 -1
- package/src/api/FirmwareUpdateV4.ts +39 -14
- package/src/api/GetPassphraseState.ts +17 -0
- package/src/api/OpenWalletSession.ts +31 -8
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +24 -2
- package/src/api/device/DeviceUnlock.ts +1 -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/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
- package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
- package/src/core/deviceEventRegistration.ts +4 -0
- package/src/core/index.ts +46 -4
- package/src/data/messages/messages-protocol-v2.json +408 -485
- package/src/device/Device.ts +266 -43
- package/src/device/DeviceCommands.ts +12 -2
- package/src/device/DeviceWalletSessionStore.ts +0 -14
- package/src/deviceProfile/buildDeviceFeatures.ts +1 -1
- package/src/events/device.ts +4 -0
- package/src/events/ui-request.ts +32 -2
- package/src/protocols/protocol-v2/features.ts +65 -16
- package/src/protocols/protocol-v2/index.ts +5 -0
- package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
- package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
- package/src/protocols/protocol-v2/unlockRetry.ts +51 -18
- package/src/protocols/protocol-v2/walletSession.ts +172 -64
- package/src/types/api/openWalletSession.ts +1 -1
- package/src/types/api/protocolV2.ts +2 -2
- package/src/types/device.ts +2 -0
- package/src/utils/deviceFeaturesUtils.ts +4 -0
|
@@ -61,14 +61,19 @@ import { createCoreApi } from '../src/inject';
|
|
|
61
61
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
62
62
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
63
63
|
import { DevicePool } from '../src/device/DevicePool';
|
|
64
|
+
import { DEVICE } from '../src/events';
|
|
64
65
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
65
66
|
import {
|
|
66
67
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
68
|
+
PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE,
|
|
67
69
|
type ProtocolV2DeviceInfo,
|
|
68
70
|
type ProtocolV2RuntimeMode,
|
|
69
71
|
getProtocolV2RuntimeMode,
|
|
72
|
+
parseProtocolV2BuildFingerprint,
|
|
70
73
|
requestProtocolV2DeviceInfo,
|
|
71
74
|
requestProtocolV2DeviceStatus,
|
|
75
|
+
requestProtocolV2ProtocolInfo,
|
|
76
|
+
supportsProtocolV2Message,
|
|
72
77
|
} from '../src/protocols/protocol-v2/features';
|
|
73
78
|
import {
|
|
74
79
|
getProtocolV2WalletSession,
|
|
@@ -95,7 +100,7 @@ import {
|
|
|
95
100
|
|
|
96
101
|
import type { DeviceCommands } from '../src/device/DeviceCommands';
|
|
97
102
|
import type { Features } from '../src/types';
|
|
98
|
-
import type { DeviceStatus } from '@onekeyfe/hd-transport';
|
|
103
|
+
import type { DeviceStatus, ProtocolInfo } from '@onekeyfe/hd-transport';
|
|
99
104
|
|
|
100
105
|
jest.mock('../src/data/config', () => ({
|
|
101
106
|
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
@@ -143,7 +148,7 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
143
148
|
const result = await method.run();
|
|
144
149
|
|
|
145
150
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
146
|
-
expect(method.unlockPolicy).toBe('
|
|
151
|
+
expect(method.unlockPolicy).toBe('none');
|
|
147
152
|
expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
|
|
148
153
|
path: 'vol1:/wallpapers',
|
|
149
154
|
});
|
|
@@ -347,6 +352,24 @@ const descriptor = {
|
|
|
347
352
|
path: 'usb-path',
|
|
348
353
|
};
|
|
349
354
|
|
|
355
|
+
const protocolV2ApplicationInfo = {
|
|
356
|
+
version: 1,
|
|
357
|
+
build_fingerprint: 'application__1.2.3__abcdef0__PROD__RELEASE',
|
|
358
|
+
supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const protocolV2BootloaderInfo = {
|
|
362
|
+
version: 1,
|
|
363
|
+
build_fingerprint: 'bootloader__0.2.0__abcdef0__PROD__RELEASE',
|
|
364
|
+
supported_messages: [],
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const protocolV2RomloaderInfo = {
|
|
368
|
+
version: 1,
|
|
369
|
+
build_fingerprint: 'romloader__1.0.0__abcdef0__PROD__RELEASE',
|
|
370
|
+
supported_messages: [],
|
|
371
|
+
};
|
|
372
|
+
|
|
350
373
|
/**
|
|
351
374
|
* Add Device protocol and getCurrent* accessors to plain-object stubs.
|
|
352
375
|
* Defaults match Device.ts and never replace an existing member such as jest.fn().
|
|
@@ -356,6 +379,8 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
356
379
|
d.updateState ??= jest.fn();
|
|
357
380
|
d.markProtocolV2Reboot ??= jest.fn();
|
|
358
381
|
d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
|
|
382
|
+
d.isBootloader ??= () => d.features?.mode === 'bootloader';
|
|
383
|
+
d.isRomloader ??= () => d.features?.mode === 'romloader';
|
|
359
384
|
d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1');
|
|
360
385
|
d.getCurrentDeviceType ??= () => getDeviceType(d.features);
|
|
361
386
|
d.getCurrentFirmwareType ??= () => getFirmwareType(d.features);
|
|
@@ -375,7 +400,8 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
375
400
|
d.updateProtocolV2Features ??= (
|
|
376
401
|
deviceInfo?: ProtocolV2DeviceInfo,
|
|
377
402
|
deviceStatus?: DeviceStatus,
|
|
378
|
-
runtimeMode?: ProtocolV2RuntimeMode
|
|
403
|
+
runtimeMode?: ProtocolV2RuntimeMode,
|
|
404
|
+
protocolInfo?: ProtocolInfo
|
|
379
405
|
) => {
|
|
380
406
|
d.features = buildProtocolV2FeaturesPayload({
|
|
381
407
|
deviceInfo,
|
|
@@ -383,44 +409,37 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
383
409
|
previous: d.features,
|
|
384
410
|
runtimeMode: runtimeMode ?? (deviceStatus ? 'normal' : undefined),
|
|
385
411
|
});
|
|
412
|
+
if (protocolInfo) {
|
|
413
|
+
d.features.raw = {
|
|
414
|
+
...d.features.raw,
|
|
415
|
+
protocolV2ProtocolInfo: protocolInfo,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
386
418
|
return d.features;
|
|
387
419
|
};
|
|
420
|
+
d.ensureProtocolV2RuntimeContext ??= (timeoutMs?: number) =>
|
|
421
|
+
requestProtocolV2ProtocolInfo({
|
|
422
|
+
commands: d.getCommands?.() ?? d.commands,
|
|
423
|
+
timeoutMs,
|
|
424
|
+
});
|
|
388
425
|
d.probeProtocolV2RuntimeState ??= async (
|
|
389
426
|
deviceInfo: ProtocolV2DeviceInfo,
|
|
390
427
|
timeoutMs?: number
|
|
391
428
|
) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
);
|
|
401
|
-
const features = d.updateProtocolV2Features(deviceInfo, response.message, 'normal');
|
|
402
|
-
return (
|
|
403
|
-
features ??
|
|
404
|
-
buildProtocolV2FeaturesPayload({
|
|
405
|
-
deviceInfo,
|
|
406
|
-
deviceStatus: response.message,
|
|
407
|
-
previous: d.features,
|
|
408
|
-
runtimeMode: 'normal',
|
|
409
|
-
})
|
|
410
|
-
);
|
|
411
|
-
} catch {
|
|
412
|
-
const runtimeMode = getProtocolV2RuntimeMode({
|
|
413
|
-
deviceInfo,
|
|
414
|
-
deviceStatusAvailable: false,
|
|
415
|
-
});
|
|
416
|
-
const features = d.updateProtocolV2Features(deviceInfo, null, runtimeMode);
|
|
417
|
-
if (features?.mode === runtimeMode) return features;
|
|
418
|
-
return buildProtocolV2FeaturesPayload({
|
|
419
|
-
deviceInfo,
|
|
420
|
-
previous: d.features,
|
|
421
|
-
runtimeMode,
|
|
422
|
-
});
|
|
429
|
+
const protocolInfo = await d.ensureProtocolV2RuntimeContext(timeoutMs);
|
|
430
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
|
|
431
|
+
if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
|
|
432
|
+
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
433
|
+
}
|
|
434
|
+
if (!supportsProtocolV2Message(protocolInfo, PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE)) {
|
|
435
|
+
if (!runtimeMode) throw new Error('Unknown Protocol V2 runtime mode');
|
|
436
|
+
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
423
437
|
}
|
|
438
|
+
const deviceStatus = await requestProtocolV2DeviceStatus({
|
|
439
|
+
commands: d.getCommands(),
|
|
440
|
+
timeoutMs,
|
|
441
|
+
});
|
|
442
|
+
return d.updateProtocolV2Features(deviceInfo, deviceStatus, 'normal', protocolInfo);
|
|
424
443
|
};
|
|
425
444
|
d.updateProtocolV2Status ??= (deviceStatus: DeviceStatus) => {
|
|
426
445
|
d.features = buildProtocolV2FeaturesPayload({
|
|
@@ -680,7 +699,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
680
699
|
expect(features.bleEnabled).toBeNull();
|
|
681
700
|
});
|
|
682
701
|
|
|
683
|
-
test('
|
|
702
|
+
test('parses bootloader runtime mode from ProtocolInfo build fingerprint', () => {
|
|
684
703
|
const deviceInfo = {
|
|
685
704
|
protocol_version: 1,
|
|
686
705
|
hw: {
|
|
@@ -693,10 +712,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
693
712
|
},
|
|
694
713
|
se1: {},
|
|
695
714
|
};
|
|
696
|
-
const runtimeMode = getProtocolV2RuntimeMode(
|
|
697
|
-
deviceInfo: deviceInfo as ProtocolV2DeviceInfo,
|
|
698
|
-
deviceStatusAvailable: false,
|
|
699
|
-
});
|
|
715
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolV2BootloaderInfo);
|
|
700
716
|
const features = normalizeProtocolV2Features(
|
|
701
717
|
descriptor as any,
|
|
702
718
|
deviceInfo as ProtocolV2DeviceInfo,
|
|
@@ -718,7 +734,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
718
734
|
expect(features.bootloaderMode).toBe(false);
|
|
719
735
|
});
|
|
720
736
|
|
|
721
|
-
test('
|
|
737
|
+
test('keeps romloader distinct from bootloader using ProtocolInfo build fingerprint', () => {
|
|
722
738
|
const deviceInfo = {
|
|
723
739
|
protocol_version: 1,
|
|
724
740
|
hw: {
|
|
@@ -733,20 +749,28 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
733
749
|
},
|
|
734
750
|
},
|
|
735
751
|
};
|
|
736
|
-
const runtimeMode = getProtocolV2RuntimeMode(
|
|
737
|
-
deviceInfo: deviceInfo as ProtocolV2DeviceInfo,
|
|
738
|
-
deviceStatusAvailable: false,
|
|
739
|
-
});
|
|
752
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolV2RomloaderInfo);
|
|
740
753
|
const features = normalizeProtocolV2Features(
|
|
741
754
|
descriptor as any,
|
|
742
755
|
deviceInfo as ProtocolV2DeviceInfo,
|
|
743
756
|
runtimeMode
|
|
744
757
|
);
|
|
745
|
-
expect(features.mode).toBe('
|
|
758
|
+
expect(features.mode).toBe('romloader');
|
|
746
759
|
expect(features.bootloaderMode).toBe(true);
|
|
747
760
|
expect(features.boardVersion).toBe('1.0.0');
|
|
748
761
|
});
|
|
749
762
|
|
|
763
|
+
test('rejects malformed Protocol V2 build fingerprints', () => {
|
|
764
|
+
expect(parseProtocolV2BuildFingerprint('application__1.2.3')).toBeNull();
|
|
765
|
+
expect(
|
|
766
|
+
getProtocolV2RuntimeMode({
|
|
767
|
+
version: 1,
|
|
768
|
+
build_fingerprint: 'unknown__1.2.3__abcdef0__PROD__RELEASE',
|
|
769
|
+
supported_messages: [],
|
|
770
|
+
})
|
|
771
|
+
).toBeUndefined();
|
|
772
|
+
});
|
|
773
|
+
|
|
750
774
|
test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
|
|
751
775
|
const features = normalizeProtocolV2Features(descriptor as any);
|
|
752
776
|
features.firmwareVersion = '1.2.3';
|
|
@@ -783,7 +807,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
783
807
|
).resolves.toEqual({
|
|
784
808
|
passphraseState: 'state-1',
|
|
785
809
|
newSession: 'session-1',
|
|
786
|
-
unlockedAttachPin:
|
|
810
|
+
unlockedAttachPin: false,
|
|
787
811
|
resumed: false,
|
|
788
812
|
});
|
|
789
813
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
@@ -844,6 +868,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
844
868
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
845
869
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
846
870
|
session_id: 'standard-session',
|
|
871
|
+
btc_test_address: 'standard-state',
|
|
847
872
|
});
|
|
848
873
|
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
849
874
|
passphraseState: 'standard-state',
|
|
@@ -908,7 +933,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
908
933
|
expect(deviceWalletSessionStore.get(deviceId, 'hidden-state')).toBe('hidden-session');
|
|
909
934
|
});
|
|
910
935
|
|
|
911
|
-
test('
|
|
936
|
+
test('propagates DeviceLocked instead of retrying a cached standard session', async () => {
|
|
912
937
|
const deviceId = 'standard-locked-device';
|
|
913
938
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
914
939
|
(device as any).features = normalizeProtocolV2Features(
|
|
@@ -950,14 +975,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
950
975
|
});
|
|
951
976
|
(device as any).commands = { typedCall };
|
|
952
977
|
|
|
953
|
-
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).
|
|
954
|
-
|
|
955
|
-
resumed: true,
|
|
978
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).rejects.toMatchObject({
|
|
979
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
956
980
|
});
|
|
957
|
-
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(
|
|
958
|
-
expect(deviceWalletSessionStore.getStandard(deviceId)
|
|
959
|
-
'cached-standard-session'
|
|
960
|
-
);
|
|
981
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
982
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toBeUndefined();
|
|
961
983
|
});
|
|
962
984
|
|
|
963
985
|
test('recovers a mismatched cached standard wallet without deleting hidden sessions', async () => {
|
|
@@ -1083,7 +1105,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1083
1105
|
});
|
|
1084
1106
|
});
|
|
1085
1107
|
|
|
1086
|
-
test('
|
|
1108
|
+
test('does not implicitly unlock inside the Protocol V2 wallet-session coordinator', async () => {
|
|
1087
1109
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1088
1110
|
(device as any).features = normalizeProtocolV2Features(
|
|
1089
1111
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
@@ -1094,13 +1116,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1094
1116
|
},
|
|
1095
1117
|
}
|
|
1096
1118
|
);
|
|
1097
|
-
const lockedError = Object.assign(new Error('Device is locked'), {
|
|
1098
|
-
errorCode: HardwareErrorCode.DeviceLocked,
|
|
1099
|
-
});
|
|
1100
1119
|
const typedCall = createWalletSessionTypedCall(
|
|
1101
1120
|
jest
|
|
1102
1121
|
.fn()
|
|
1103
|
-
.mockRejectedValueOnce(lockedError)
|
|
1104
1122
|
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1105
1123
|
.mockResolvedValueOnce({
|
|
1106
1124
|
message: {
|
|
@@ -1127,7 +1145,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1127
1145
|
resumed: false,
|
|
1128
1146
|
});
|
|
1129
1147
|
|
|
1130
|
-
expect(unlockDevice).
|
|
1148
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1131
1149
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
1132
1150
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
1133
1151
|
eventless_wallet_session: true,
|
|
@@ -1199,7 +1217,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1199
1217
|
throw new Error(`Unexpected request: ${request}`);
|
|
1200
1218
|
})
|
|
1201
1219
|
);
|
|
1202
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1220
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1203
1221
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
1204
1222
|
|
|
1205
1223
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
@@ -1209,7 +1227,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1209
1227
|
|
|
1210
1228
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
1211
1229
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
1212
|
-
|
|
1230
|
+
passphrase: 'host hidden wallet',
|
|
1231
|
+
on_device: false,
|
|
1213
1232
|
});
|
|
1214
1233
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1215
1234
|
});
|
|
@@ -1240,7 +1259,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1240
1259
|
|
|
1241
1260
|
test('deviceGetOnboardingStatus returns the current Protocol V2 onboarding status', async () => {
|
|
1242
1261
|
const typedCall = jest.fn().mockResolvedValue({
|
|
1243
|
-
type: '
|
|
1262
|
+
type: 'OnboardingStatus',
|
|
1244
1263
|
message: {
|
|
1245
1264
|
step: 4,
|
|
1246
1265
|
phase: 4,
|
|
@@ -1268,7 +1287,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1268
1287
|
pin_set: true,
|
|
1269
1288
|
wallet_initialized: false,
|
|
1270
1289
|
});
|
|
1271
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
1290
|
+
expect(typedCall).toHaveBeenCalledWith('OnboardingStatusGet', 'OnboardingStatus', {});
|
|
1272
1291
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
1273
1292
|
});
|
|
1274
1293
|
|
|
@@ -1319,6 +1338,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1319
1338
|
|
|
1320
1339
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1321
1340
|
session_id: 'session-a',
|
|
1341
|
+
btc_test_address: 'state-a',
|
|
1322
1342
|
});
|
|
1323
1343
|
expect(device.getInternalState()).toBe('session-b');
|
|
1324
1344
|
});
|
|
@@ -1386,7 +1406,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1386
1406
|
});
|
|
1387
1407
|
expect(typedCall.mock.calls).toEqual([
|
|
1388
1408
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1389
|
-
[
|
|
1409
|
+
[
|
|
1410
|
+
'DeviceSessionGet',
|
|
1411
|
+
'DeviceSession',
|
|
1412
|
+
{ session_id: 'session-a', btc_test_address: 'state-a' },
|
|
1413
|
+
],
|
|
1390
1414
|
]);
|
|
1391
1415
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1392
1416
|
expect(device.getInternalState()).toBeUndefined();
|
|
@@ -1420,7 +1444,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1420
1444
|
);
|
|
1421
1445
|
(device as any).commands = {
|
|
1422
1446
|
typedCall,
|
|
1423
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1447
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1424
1448
|
};
|
|
1425
1449
|
|
|
1426
1450
|
await expect(getProtocolV2WalletSession(device)).rejects.toMatchObject({
|
|
@@ -1428,7 +1452,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1428
1452
|
});
|
|
1429
1453
|
expect(typedCall.mock.calls).toEqual([
|
|
1430
1454
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1431
|
-
[
|
|
1455
|
+
[
|
|
1456
|
+
'DeviceSessionAskPassphrase',
|
|
1457
|
+
'Success',
|
|
1458
|
+
{ passphrase: 'host hidden wallet', on_device: false },
|
|
1459
|
+
],
|
|
1432
1460
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1433
1461
|
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1434
1462
|
]);
|
|
@@ -1469,7 +1497,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1469
1497
|
throw new Error(`Unexpected request: ${request}`);
|
|
1470
1498
|
})
|
|
1471
1499
|
);
|
|
1472
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1500
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1473
1501
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
1474
1502
|
|
|
1475
1503
|
await expect(
|
|
@@ -1522,7 +1550,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1522
1550
|
);
|
|
1523
1551
|
(device as any).commands = {
|
|
1524
1552
|
typedCall,
|
|
1525
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1553
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1526
1554
|
};
|
|
1527
1555
|
|
|
1528
1556
|
await expect(
|
|
@@ -1533,6 +1561,72 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1533
1561
|
expect(device.getInternalState()).toBeUndefined();
|
|
1534
1562
|
});
|
|
1535
1563
|
|
|
1564
|
+
test.each([false, true])(
|
|
1565
|
+
'rejects a different Attach PIN wallet when Pro2 lock failure is %s',
|
|
1566
|
+
async lockFails => {
|
|
1567
|
+
const deviceId = 'attach-mismatch-device';
|
|
1568
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1569
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1570
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1571
|
+
{
|
|
1572
|
+
status: {
|
|
1573
|
+
device_id: deviceId,
|
|
1574
|
+
unlocked: true,
|
|
1575
|
+
passphrase_enabled: true,
|
|
1576
|
+
attach_to_pin_enabled: true,
|
|
1577
|
+
unlocked_by_attach_to_pin: false,
|
|
1578
|
+
},
|
|
1579
|
+
}
|
|
1580
|
+
);
|
|
1581
|
+
device.passphraseState = 'expected-state';
|
|
1582
|
+
|
|
1583
|
+
const typedCall = jest.fn((request: string) => {
|
|
1584
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
1585
|
+
if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
|
|
1586
|
+
if (request === 'DeviceStatusGet') {
|
|
1587
|
+
return {
|
|
1588
|
+
message: {
|
|
1589
|
+
device_id: deviceId,
|
|
1590
|
+
unlocked: true,
|
|
1591
|
+
passphrase_enabled: true,
|
|
1592
|
+
attach_to_pin_enabled: true,
|
|
1593
|
+
unlocked_by_attach_to_pin: true,
|
|
1594
|
+
},
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
if (request === 'DeviceSessionGet') {
|
|
1598
|
+
return {
|
|
1599
|
+
message: {
|
|
1600
|
+
btc_test_address: 'wrong-attach-state',
|
|
1601
|
+
session_id: 'wrong-attach-session',
|
|
1602
|
+
},
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
if (request === 'LockDevice') {
|
|
1606
|
+
if (lockFails) throw new Error('Lock unsupported');
|
|
1607
|
+
return { message: { message: 'Device locked' } };
|
|
1608
|
+
}
|
|
1609
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1610
|
+
});
|
|
1611
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
1612
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1613
|
+
|
|
1614
|
+
await expect(
|
|
1615
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1616
|
+
).rejects.toMatchObject({
|
|
1617
|
+
errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1621
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1622
|
+
btc_test_address: 'expected-state',
|
|
1623
|
+
});
|
|
1624
|
+
expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
|
|
1625
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
1626
|
+
expect(device.getInternalState()).toBeUndefined();
|
|
1627
|
+
}
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1536
1630
|
test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
|
|
1537
1631
|
const device = Device.fromDescriptor({
|
|
1538
1632
|
id: 'cache-device-attach-recovery',
|
|
@@ -1589,11 +1683,21 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1589
1683
|
}),
|
|
1590
1684
|
{ cancelDeviceOnReject: false }
|
|
1591
1685
|
);
|
|
1592
|
-
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1686
|
+
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1687
|
+
DeviceSessionPinType.AttachToPin,
|
|
1688
|
+
expect.objectContaining({
|
|
1689
|
+
emitUiEvent: false,
|
|
1690
|
+
interaction: expect.objectContaining({
|
|
1691
|
+
phase: 'pin',
|
|
1692
|
+
transition: 'start',
|
|
1693
|
+
protocol: 'V2',
|
|
1694
|
+
}),
|
|
1695
|
+
})
|
|
1696
|
+
);
|
|
1593
1697
|
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1594
1698
|
});
|
|
1595
1699
|
|
|
1596
|
-
test('
|
|
1700
|
+
test('propagates DeviceLocked without retrying DeviceSessionAskPassphrase', async () => {
|
|
1597
1701
|
const device = Device.fromDescriptor({
|
|
1598
1702
|
id: 'cache-device-4',
|
|
1599
1703
|
path: 'cache-path-4',
|
|
@@ -1620,16 +1724,61 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1620
1724
|
);
|
|
1621
1725
|
(device as any).commands = {
|
|
1622
1726
|
typedCall,
|
|
1623
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1727
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1624
1728
|
};
|
|
1625
1729
|
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockResolvedValue(undefined as any);
|
|
1626
1730
|
|
|
1731
|
+
await expect(getProtocolV2WalletSession(device)).rejects.toBe(lockedError);
|
|
1732
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1733
|
+
expect(
|
|
1734
|
+
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
1735
|
+
).toHaveLength(1);
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
test('does not act on cached lock state inside the wallet-session coordinator', async () => {
|
|
1739
|
+
const calls: string[] = [];
|
|
1740
|
+
const device = Device.fromDescriptor({
|
|
1741
|
+
id: 'cache-device-pre-unlock',
|
|
1742
|
+
path: 'cache-path-pre-unlock',
|
|
1743
|
+
protocolType: 'V2',
|
|
1744
|
+
} as any);
|
|
1745
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1746
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1747
|
+
{ status: { device_id: 'stable-device-pre-unlock', unlocked: false } }
|
|
1748
|
+
);
|
|
1749
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1750
|
+
jest.fn((request: string) => {
|
|
1751
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
1752
|
+
calls.push('ask-passphrase');
|
|
1753
|
+
return Promise.resolve({ message: { message: 'Passphrase prepared' } });
|
|
1754
|
+
}
|
|
1755
|
+
if (request === 'DeviceSessionGet') {
|
|
1756
|
+
calls.push('get-session');
|
|
1757
|
+
return Promise.resolve({
|
|
1758
|
+
message: {
|
|
1759
|
+
session_id: 'session-after-pre-unlock',
|
|
1760
|
+
btc_test_address: 'state-after-pre-unlock',
|
|
1761
|
+
},
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1765
|
+
})
|
|
1766
|
+
);
|
|
1767
|
+
(device as any).commands = {
|
|
1768
|
+
typedCall,
|
|
1769
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
|
|
1770
|
+
};
|
|
1771
|
+
const unlockDevice = jest.spyOn(device, 'unlockDevice');
|
|
1772
|
+
|
|
1627
1773
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
1628
|
-
newSession: 'session-after-unlock',
|
|
1629
|
-
passphraseState: 'state-after-unlock',
|
|
1774
|
+
newSession: 'session-after-pre-unlock',
|
|
1775
|
+
passphraseState: 'state-after-pre-unlock',
|
|
1630
1776
|
});
|
|
1631
|
-
expect(unlockDevice).
|
|
1632
|
-
expect(
|
|
1777
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1778
|
+
expect(
|
|
1779
|
+
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
1780
|
+
).toHaveLength(1);
|
|
1781
|
+
expect(calls).toEqual(['ask-passphrase', 'get-session']);
|
|
1633
1782
|
});
|
|
1634
1783
|
|
|
1635
1784
|
test('does not request a Pro2 wallet session before features are initialized', async () => {
|
|
@@ -1772,6 +1921,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1772
1921
|
};
|
|
1773
1922
|
const typedCall = jest
|
|
1774
1923
|
.fn()
|
|
1924
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1775
1925
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1776
1926
|
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1777
1927
|
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
@@ -1784,7 +1934,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1784
1934
|
});
|
|
1785
1935
|
const updateInternalState = jest.fn();
|
|
1786
1936
|
const getFeatures = jest.fn().mockResolvedValue(features);
|
|
1787
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1937
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1788
1938
|
const method = new GetPassphraseState({
|
|
1789
1939
|
payload: {
|
|
1790
1940
|
method: 'getPassphraseState',
|
|
@@ -1827,19 +1977,17 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1827
1977
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
1828
1978
|
});
|
|
1829
1979
|
|
|
1830
|
-
test('
|
|
1980
|
+
test('uses the cached wallet address when the local session is missing', async () => {
|
|
1831
1981
|
const typedCall = jest
|
|
1832
1982
|
.fn()
|
|
1833
1983
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1834
|
-
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1835
|
-
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1836
1984
|
.mockResolvedValueOnce({
|
|
1837
1985
|
message: {
|
|
1838
1986
|
btc_test_address: 'expected-state',
|
|
1839
1987
|
session_id: 'recovered-session',
|
|
1840
1988
|
},
|
|
1841
1989
|
});
|
|
1842
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1990
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1843
1991
|
const device = stubDevice({
|
|
1844
1992
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
1845
1993
|
passphraseState: 'expected-state',
|
|
@@ -1860,16 +2008,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1860
2008
|
await expect(
|
|
1861
2009
|
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1862
2010
|
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
1863
|
-
expect(
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
source: 'wallet-session-coordinator',
|
|
1868
|
-
reason: 'session-recovery',
|
|
1869
|
-
expectedPassphraseState: 'expected-state',
|
|
1870
|
-
},
|
|
1871
|
-
{ cancelDeviceOnReject: false }
|
|
1872
|
-
);
|
|
2011
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2012
|
+
btc_test_address: 'expected-state',
|
|
2013
|
+
});
|
|
2014
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1873
2015
|
});
|
|
1874
2016
|
|
|
1875
2017
|
test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
|
|
@@ -1914,7 +2056,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1914
2056
|
);
|
|
1915
2057
|
(device as any).commands = {
|
|
1916
2058
|
typedCall,
|
|
1917
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2059
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1918
2060
|
};
|
|
1919
2061
|
|
|
1920
2062
|
const method = new GetPassphraseState({
|
|
@@ -1935,8 +2077,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1935
2077
|
true
|
|
1936
2078
|
);
|
|
1937
2079
|
expect(typedCall.mock.calls).toEqual([
|
|
1938
|
-
[
|
|
1939
|
-
|
|
2080
|
+
[
|
|
2081
|
+
'DeviceSessionGet',
|
|
2082
|
+
'DeviceSession',
|
|
2083
|
+
{ session_id: 'session-pro2-app', btc_test_address: 'state-pro2-app' },
|
|
2084
|
+
],
|
|
1940
2085
|
]);
|
|
1941
2086
|
});
|
|
1942
2087
|
|
|
@@ -1970,7 +2115,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1970
2115
|
features,
|
|
1971
2116
|
commands: {
|
|
1972
2117
|
typedCall,
|
|
1973
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2118
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1974
2119
|
},
|
|
1975
2120
|
updateInternalState,
|
|
1976
2121
|
getCurrentDeviceType: () => 'pro2',
|
|
@@ -1998,6 +2143,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1998
2143
|
};
|
|
1999
2144
|
const typedCall = jest
|
|
2000
2145
|
.fn()
|
|
2146
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
2001
2147
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
2002
2148
|
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
2003
2149
|
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
@@ -2023,7 +2169,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2023
2169
|
passphraseState: 'state-pro2-old',
|
|
2024
2170
|
commands: {
|
|
2025
2171
|
typedCall,
|
|
2026
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2172
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2027
2173
|
},
|
|
2028
2174
|
emit: jest.fn(),
|
|
2029
2175
|
clearInternalState,
|
|
@@ -2066,7 +2212,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2066
2212
|
};
|
|
2067
2213
|
(device as any).commands = {
|
|
2068
2214
|
typedCall,
|
|
2069
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2215
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2070
2216
|
};
|
|
2071
2217
|
|
|
2072
2218
|
await getPassphraseStateWithRefreshDeviceInfo(device);
|
|
@@ -2106,7 +2252,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2106
2252
|
);
|
|
2107
2253
|
(device as any).commands = {
|
|
2108
2254
|
typedCall,
|
|
2109
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2255
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2110
2256
|
};
|
|
2111
2257
|
|
|
2112
2258
|
await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({
|
|
@@ -2121,7 +2267,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2121
2267
|
device.passphraseState = 'state-auto';
|
|
2122
2268
|
expect(device.getInternalState()).toBe('session-auto');
|
|
2123
2269
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2124
|
-
|
|
2270
|
+
passphrase: 'host hidden wallet',
|
|
2271
|
+
on_device: false,
|
|
2125
2272
|
});
|
|
2126
2273
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2127
2274
|
});
|
|
@@ -2191,7 +2338,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2191
2338
|
(device as any).features.unlocked = true;
|
|
2192
2339
|
(device as any).commands = {
|
|
2193
2340
|
typedCall,
|
|
2194
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2341
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2195
2342
|
};
|
|
2196
2343
|
|
|
2197
2344
|
await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
|
|
@@ -2201,11 +2348,15 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2201
2348
|
);
|
|
2202
2349
|
|
|
2203
2350
|
expect(device.getInternalState()).toBeUndefined();
|
|
2204
|
-
expect(typedCall).
|
|
2205
|
-
|
|
2351
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2352
|
+
btc_test_address: 'expected-state',
|
|
2206
2353
|
});
|
|
2207
|
-
expect(typedCall).
|
|
2208
|
-
|
|
2354
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2355
|
+
passphrase: 'host hidden wallet',
|
|
2356
|
+
on_device: false,
|
|
2357
|
+
});
|
|
2358
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2359
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2209
2360
|
});
|
|
2210
2361
|
|
|
2211
2362
|
test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
|
|
@@ -2242,6 +2393,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2242
2393
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2243
2394
|
eventless_wallet_session: true,
|
|
2244
2395
|
});
|
|
2396
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
2397
|
+
type: DeviceSessionPinType.Main,
|
|
2398
|
+
});
|
|
2245
2399
|
});
|
|
2246
2400
|
|
|
2247
2401
|
test('marks fallback features as unavailable when DeviceInfo is missing', () => {
|
|
@@ -2564,6 +2718,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2564
2718
|
fw: { application: { version: '1.2.3' } },
|
|
2565
2719
|
},
|
|
2566
2720
|
})
|
|
2721
|
+
.mockResolvedValueOnce({
|
|
2722
|
+
type: 'ProtocolInfo',
|
|
2723
|
+
message: protocolV2ApplicationInfo,
|
|
2724
|
+
})
|
|
2567
2725
|
.mockResolvedValueOnce({
|
|
2568
2726
|
type: 'DeviceStatus',
|
|
2569
2727
|
message: { init_states: true, unlocked: true },
|
|
@@ -2584,7 +2742,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2584
2742
|
expect(device.features?.passphraseProtection).toBeNull();
|
|
2585
2743
|
expect(device.features?.label).toBeNull();
|
|
2586
2744
|
expect(device.features?.firmwareVersion).toBe('1.2.3');
|
|
2587
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2745
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
2588
2746
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2589
2747
|
1,
|
|
2590
2748
|
'DeviceInfoGet',
|
|
@@ -2608,10 +2766,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2608
2766
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
2609
2767
|
}
|
|
2610
2768
|
);
|
|
2611
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, '
|
|
2769
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2770
|
+
eventless_wallet_session: true,
|
|
2771
|
+
});
|
|
2772
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2612
2773
|
});
|
|
2613
2774
|
|
|
2614
|
-
test('recognizes bootloader
|
|
2775
|
+
test('recognizes bootloader from ProtocolInfo without calling DeviceStatusGet', async () => {
|
|
2615
2776
|
const device = Device.fromDescriptor({
|
|
2616
2777
|
path: 'usb-path',
|
|
2617
2778
|
protocolType: 'V2',
|
|
@@ -2626,7 +2787,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2626
2787
|
se1: {},
|
|
2627
2788
|
},
|
|
2628
2789
|
})
|
|
2629
|
-
.
|
|
2790
|
+
.mockResolvedValueOnce({
|
|
2791
|
+
type: 'ProtocolInfo',
|
|
2792
|
+
message: protocolV2BootloaderInfo,
|
|
2793
|
+
});
|
|
2630
2794
|
|
|
2631
2795
|
(device as any).commands = { typedCall };
|
|
2632
2796
|
|
|
@@ -2642,23 +2806,221 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2642
2806
|
}),
|
|
2643
2807
|
expect.anything()
|
|
2644
2808
|
);
|
|
2645
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, '
|
|
2809
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2810
|
+
eventless_wallet_session: true,
|
|
2811
|
+
});
|
|
2812
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2646
2813
|
expect(device.features).toMatchObject({
|
|
2647
2814
|
deviceId: null,
|
|
2648
2815
|
bootloaderMode: true,
|
|
2816
|
+
mode: 'bootloader',
|
|
2649
2817
|
initialized: null,
|
|
2650
2818
|
});
|
|
2819
|
+
expect(device.isBootloader()).toBe(true);
|
|
2820
|
+
expect(device.isRomloader()).toBe(false);
|
|
2651
2821
|
});
|
|
2652
2822
|
|
|
2653
|
-
test('
|
|
2823
|
+
test('recognizes romloader from ProtocolInfo without calling DeviceStatusGet', async () => {
|
|
2654
2824
|
const device = Device.fromDescriptor({
|
|
2655
2825
|
path: 'usb-path',
|
|
2656
2826
|
protocolType: 'V2',
|
|
2657
2827
|
} as any);
|
|
2658
2828
|
const typedCall = jest.fn().mockResolvedValue({
|
|
2659
|
-
type: '
|
|
2660
|
-
message:
|
|
2829
|
+
type: 'ProtocolInfo',
|
|
2830
|
+
message: protocolV2RomloaderInfo,
|
|
2831
|
+
});
|
|
2832
|
+
(device as any).commands = { typedCall };
|
|
2833
|
+
|
|
2834
|
+
await device.probeProtocolV2RuntimeState({
|
|
2835
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2836
|
+
fw: { romloader: { version: '1.0.0' } },
|
|
2837
|
+
});
|
|
2838
|
+
|
|
2839
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2840
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2841
|
+
expect(device.features).toMatchObject({
|
|
2842
|
+
mode: 'romloader',
|
|
2843
|
+
bootloaderMode: true,
|
|
2844
|
+
initialized: null,
|
|
2845
|
+
});
|
|
2846
|
+
expect(device.isBootloader()).toBe(false);
|
|
2847
|
+
expect(device.isRomloader()).toBe(true);
|
|
2848
|
+
});
|
|
2849
|
+
|
|
2850
|
+
test('rejects Protocol V2 romloader mode for non-Pro2 devices', async () => {
|
|
2851
|
+
const device = Device.fromDescriptor({
|
|
2852
|
+
path: 'pro-v2-path',
|
|
2853
|
+
protocolType: 'V2',
|
|
2854
|
+
} as any);
|
|
2855
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
2856
|
+
type: 'ProtocolInfo',
|
|
2857
|
+
message: protocolV2RomloaderInfo,
|
|
2858
|
+
});
|
|
2859
|
+
(device as any).commands = { typedCall };
|
|
2860
|
+
|
|
2861
|
+
await expect(
|
|
2862
|
+
device.probeProtocolV2RuntimeState({
|
|
2863
|
+
hw: { Device_type: DeviceType.PRO, serial_no: 'PRO-SERIAL' },
|
|
2864
|
+
fw: { romloader: { version: '1.0.0' } },
|
|
2865
|
+
})
|
|
2866
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
2867
|
+
|
|
2868
|
+
expect(device.isRomloader()).not.toBe(true);
|
|
2869
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
test('keeps application mode without calling an unadvertised DeviceStatusGet', async () => {
|
|
2873
|
+
const device = Device.fromDescriptor({
|
|
2874
|
+
path: 'usb-path',
|
|
2875
|
+
protocolType: 'V2',
|
|
2876
|
+
} as any);
|
|
2877
|
+
const protocolInfo = {
|
|
2878
|
+
...protocolV2ApplicationInfo,
|
|
2879
|
+
supported_messages: [],
|
|
2880
|
+
};
|
|
2881
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
2882
|
+
type: 'ProtocolInfo',
|
|
2883
|
+
message: protocolInfo,
|
|
2884
|
+
});
|
|
2885
|
+
(device as any).commands = { typedCall };
|
|
2886
|
+
|
|
2887
|
+
await device.probeProtocolV2RuntimeState({
|
|
2888
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
2889
|
+
fw: { application: { version: '1.2.3' } },
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2893
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2894
|
+
expect(device.features).toMatchObject({
|
|
2895
|
+
mode: 'normal',
|
|
2896
|
+
bootloaderMode: false,
|
|
2897
|
+
initialized: null,
|
|
2898
|
+
});
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
test('uses advertised DeviceStatusGet as compatibility fallback for an unknown fingerprint', async () => {
|
|
2902
|
+
const device = Device.fromDescriptor({
|
|
2903
|
+
path: 'usb-path',
|
|
2904
|
+
protocolType: 'V2',
|
|
2905
|
+
} as any);
|
|
2906
|
+
const protocolInfo = {
|
|
2907
|
+
...protocolV2ApplicationInfo,
|
|
2908
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2909
|
+
};
|
|
2910
|
+
const typedCall = jest
|
|
2911
|
+
.fn()
|
|
2912
|
+
.mockResolvedValueOnce({ type: 'ProtocolInfo', message: protocolInfo })
|
|
2913
|
+
.mockResolvedValueOnce({
|
|
2914
|
+
type: 'DeviceStatus',
|
|
2915
|
+
message: { init_states: true, unlocked: true },
|
|
2916
|
+
});
|
|
2917
|
+
(device as any).commands = { typedCall };
|
|
2918
|
+
|
|
2919
|
+
await device.probeProtocolV2RuntimeState({
|
|
2920
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
2921
|
+
fw: { application: { version: '1.2.3' } },
|
|
2922
|
+
});
|
|
2923
|
+
|
|
2924
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
2925
|
+
expect(device.features).toMatchObject({
|
|
2926
|
+
mode: 'normal',
|
|
2927
|
+
initialized: true,
|
|
2928
|
+
unlocked: true,
|
|
2929
|
+
});
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
test('maps an explicit unsupported DeviceStatusGet response to bootloader for an unknown fingerprint', async () => {
|
|
2933
|
+
const device = Device.fromDescriptor({
|
|
2934
|
+
path: 'usb-path',
|
|
2935
|
+
protocolType: 'V2',
|
|
2936
|
+
} as any);
|
|
2937
|
+
const protocolInfo = {
|
|
2938
|
+
...protocolV2ApplicationInfo,
|
|
2939
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2940
|
+
};
|
|
2941
|
+
const typedCall = jest
|
|
2942
|
+
.fn()
|
|
2943
|
+
.mockResolvedValueOnce({ type: 'ProtocolInfo', message: protocolInfo })
|
|
2944
|
+
.mockRejectedValueOnce(new Error('Failure_UnexpectedMessage,Unknown message'));
|
|
2945
|
+
(device as any).commands = { typedCall };
|
|
2946
|
+
|
|
2947
|
+
await device.probeProtocolV2RuntimeState({
|
|
2948
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2949
|
+
fw: { bootloader: { version: '0.2.0' } },
|
|
2950
|
+
});
|
|
2951
|
+
|
|
2952
|
+
expect(device.features).toMatchObject({
|
|
2953
|
+
mode: 'bootloader',
|
|
2954
|
+
bootloaderMode: true,
|
|
2955
|
+
initialized: null,
|
|
2661
2956
|
});
|
|
2957
|
+
});
|
|
2958
|
+
|
|
2959
|
+
test('propagates DeviceStatusGet link failures instead of mapping them to bootloader', async () => {
|
|
2960
|
+
const device = Device.fromDescriptor({
|
|
2961
|
+
path: 'usb-path',
|
|
2962
|
+
protocolType: 'V2',
|
|
2963
|
+
} as any);
|
|
2964
|
+
const protocolInfo = {
|
|
2965
|
+
...protocolV2ApplicationInfo,
|
|
2966
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2967
|
+
};
|
|
2968
|
+
const linkError = new Error('Protocol V2 CRC mismatch');
|
|
2969
|
+
const typedCall = jest
|
|
2970
|
+
.fn()
|
|
2971
|
+
.mockResolvedValueOnce({ type: 'ProtocolInfo', message: protocolInfo })
|
|
2972
|
+
.mockRejectedValueOnce(linkError);
|
|
2973
|
+
(device as any).commands = { typedCall };
|
|
2974
|
+
|
|
2975
|
+
await expect(
|
|
2976
|
+
device.probeProtocolV2RuntimeState({
|
|
2977
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2978
|
+
fw: { application: { version: '1.2.3' } },
|
|
2979
|
+
})
|
|
2980
|
+
).rejects.toBe(linkError);
|
|
2981
|
+
|
|
2982
|
+
expect(device.features).toBeUndefined();
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
test('fails safely when runtime fingerprint and capabilities are both unknown', async () => {
|
|
2986
|
+
const device = Device.fromDescriptor({
|
|
2987
|
+
path: 'usb-path',
|
|
2988
|
+
protocolType: 'V2',
|
|
2989
|
+
} as any);
|
|
2990
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
2991
|
+
type: 'ProtocolInfo',
|
|
2992
|
+
message: {
|
|
2993
|
+
version: 1,
|
|
2994
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2995
|
+
supported_messages: [],
|
|
2996
|
+
},
|
|
2997
|
+
});
|
|
2998
|
+
(device as any).commands = { typedCall };
|
|
2999
|
+
|
|
3000
|
+
await expect(
|
|
3001
|
+
device.probeProtocolV2RuntimeState({
|
|
3002
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
3003
|
+
fw: { application: { version: '1.2.3' } },
|
|
3004
|
+
})
|
|
3005
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
3006
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3007
|
+
});
|
|
3008
|
+
|
|
3009
|
+
test('does not reinterpret a state update error as runtime detection failure', async () => {
|
|
3010
|
+
const device = Device.fromDescriptor({
|
|
3011
|
+
path: 'usb-path',
|
|
3012
|
+
protocolType: 'V2',
|
|
3013
|
+
} as any);
|
|
3014
|
+
const typedCall = jest
|
|
3015
|
+
.fn()
|
|
3016
|
+
.mockResolvedValueOnce({
|
|
3017
|
+
type: 'ProtocolInfo',
|
|
3018
|
+
message: protocolV2ApplicationInfo,
|
|
3019
|
+
})
|
|
3020
|
+
.mockResolvedValueOnce({
|
|
3021
|
+
type: 'DeviceStatus',
|
|
3022
|
+
message: { init_states: true, unlocked: true },
|
|
3023
|
+
});
|
|
2662
3024
|
const updateProtocolV2Features = jest
|
|
2663
3025
|
.spyOn(device, 'updateProtocolV2Features')
|
|
2664
3026
|
.mockImplementation(() => {
|
|
@@ -2673,12 +3035,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2673
3035
|
})
|
|
2674
3036
|
).rejects.toThrow('state update failed');
|
|
2675
3037
|
|
|
2676
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3038
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
2677
3039
|
expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
|
|
2678
3040
|
expect(updateProtocolV2Features).toHaveBeenCalledWith(
|
|
2679
3041
|
expect.anything(),
|
|
2680
3042
|
expect.objectContaining({ init_states: true }),
|
|
2681
|
-
'normal'
|
|
3043
|
+
'normal',
|
|
3044
|
+
protocolV2ApplicationInfo
|
|
2682
3045
|
);
|
|
2683
3046
|
});
|
|
2684
3047
|
|
|
@@ -2696,6 +3059,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2696
3059
|
fw: { application: { version: '1.2.3' } },
|
|
2697
3060
|
},
|
|
2698
3061
|
})
|
|
3062
|
+
.mockResolvedValueOnce({
|
|
3063
|
+
type: 'ProtocolInfo',
|
|
3064
|
+
message: protocolV2ApplicationInfo,
|
|
3065
|
+
})
|
|
2699
3066
|
.mockResolvedValueOnce({
|
|
2700
3067
|
type: 'DeviceStatus',
|
|
2701
3068
|
message: { init_states: true, unlocked: true },
|
|
@@ -2728,9 +3095,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2728
3095
|
firmwareVersion: '1.2.4',
|
|
2729
3096
|
passphraseProtection: null,
|
|
2730
3097
|
});
|
|
2731
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3098
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
2732
3099
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2733
|
-
|
|
3100
|
+
4,
|
|
2734
3101
|
'DeviceInfoGet',
|
|
2735
3102
|
'DeviceInfo',
|
|
2736
3103
|
{
|
|
@@ -2755,14 +3122,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2755
3122
|
|
|
2756
3123
|
test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
|
|
2757
3124
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2758
|
-
const typedCall = jest
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
3125
|
+
const typedCall = jest
|
|
3126
|
+
.fn()
|
|
3127
|
+
.mockResolvedValueOnce({
|
|
3128
|
+
type: 'DeviceInfo',
|
|
3129
|
+
message: {
|
|
3130
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
3131
|
+
fw: { application: { version: '1.2.4' } },
|
|
3132
|
+
},
|
|
3133
|
+
})
|
|
3134
|
+
.mockResolvedValueOnce({
|
|
3135
|
+
type: 'ProtocolInfo',
|
|
3136
|
+
message: protocolV2ApplicationInfo,
|
|
3137
|
+
})
|
|
3138
|
+
.mockResolvedValueOnce({
|
|
3139
|
+
type: 'DeviceStatus',
|
|
3140
|
+
message: { init_states: true, passphrase_enabled: true },
|
|
3141
|
+
});
|
|
2766
3142
|
(device as any).commands = { typedCall };
|
|
2767
3143
|
|
|
2768
3144
|
await device.initialize();
|
|
@@ -2833,9 +3209,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2833
3209
|
status: { init_states: true },
|
|
2834
3210
|
}
|
|
2835
3211
|
);
|
|
3212
|
+
const pinOnDevice = jest.fn();
|
|
3213
|
+
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
2836
3214
|
|
|
2837
3215
|
const features = await device.unlockDevice();
|
|
2838
3216
|
|
|
3217
|
+
expect(pinOnDevice).toHaveBeenCalledWith(
|
|
3218
|
+
device,
|
|
3219
|
+
DeviceSessionPinType.Main,
|
|
3220
|
+
expect.objectContaining({
|
|
3221
|
+
source: 'unlock-coordinator',
|
|
3222
|
+
reason: 'device-unlock',
|
|
3223
|
+
deviceOnly: true,
|
|
3224
|
+
})
|
|
3225
|
+
);
|
|
3226
|
+
expect(pinOnDevice.mock.invocationCallOrder[0]).toBeLessThan(
|
|
3227
|
+
typedCall.mock.invocationCallOrder[0]
|
|
3228
|
+
);
|
|
2839
3229
|
expect(typedCall.mock.calls).toEqual([
|
|
2840
3230
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
2841
3231
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -2881,9 +3271,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2881
3271
|
throw new Error(`Unexpected request: ${requestType}`);
|
|
2882
3272
|
});
|
|
2883
3273
|
(device as any).commands = { typedCall };
|
|
3274
|
+
const pinOnDevice = jest.fn();
|
|
3275
|
+
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
2884
3276
|
|
|
2885
|
-
await device.unlockDevice();
|
|
3277
|
+
await device.unlockDevice(DeviceSessionPinType.Main, { emitUiEvent: false });
|
|
2886
3278
|
|
|
3279
|
+
expect(pinOnDevice).not.toHaveBeenCalled();
|
|
2887
3280
|
expect(typedCall.mock.calls).toEqual([
|
|
2888
3281
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
2889
3282
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -3362,6 +3755,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3362
3755
|
message: { init_states: true, unlocked: true },
|
|
3363
3756
|
});
|
|
3364
3757
|
}
|
|
3758
|
+
if (name === 'ProtocolInfoRequest') {
|
|
3759
|
+
return Promise.resolve({
|
|
3760
|
+
type: 'ProtocolInfo',
|
|
3761
|
+
message: protocolV2ApplicationInfo,
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3365
3764
|
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
3366
3765
|
});
|
|
3367
3766
|
const commands = { typedCall };
|
|
@@ -3406,7 +3805,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3406
3805
|
},
|
|
3407
3806
|
{ timeoutMs: 5000 }
|
|
3408
3807
|
);
|
|
3409
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3808
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
3809
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3810
|
+
'DeviceInfoGet',
|
|
3811
|
+
'ProtocolInfoRequest',
|
|
3812
|
+
'DeviceStatusGet',
|
|
3813
|
+
]);
|
|
3410
3814
|
expect(typedCall).not.toHaveBeenCalledWith('Initialize', 'Features', {});
|
|
3411
3815
|
expect(versions).toEqual({
|
|
3412
3816
|
bootloaderVersion: '0.0.0',
|
|
@@ -3486,7 +3890,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3486
3890
|
message: protocolV2BootloaderDeviceInfo,
|
|
3487
3891
|
});
|
|
3488
3892
|
}
|
|
3489
|
-
|
|
3893
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
3894
|
+
return Promise.resolve({
|
|
3895
|
+
type: 'ProtocolInfo',
|
|
3896
|
+
message: protocolV2BootloaderInfo,
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
return Promise.reject(new Error(`unexpected call ${requestType}`));
|
|
3490
3900
|
});
|
|
3491
3901
|
const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
|
|
3492
3902
|
(method as any).device.isBootloader = () => true;
|
|
@@ -3513,10 +3923,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3513
3923
|
method: 'firmwareUpdateV4',
|
|
3514
3924
|
},
|
|
3515
3925
|
});
|
|
3926
|
+
const isRomloader = jest.fn(() => true);
|
|
3516
3927
|
(method as any).device = stubDevice({
|
|
3517
3928
|
originalDescriptor: { id: 'usb-id', path: 'romloader-path', protocolType: 'V2' },
|
|
3518
|
-
features: {
|
|
3929
|
+
features: {
|
|
3930
|
+
deviceType: 'pro2',
|
|
3931
|
+
mode: 'romloader',
|
|
3932
|
+
bootloaderMode: false,
|
|
3933
|
+
capabilities: [],
|
|
3934
|
+
},
|
|
3519
3935
|
isBootloader: () => false,
|
|
3936
|
+
isRomloader,
|
|
3520
3937
|
});
|
|
3521
3938
|
(method as any).protocolV2Reboot = jest
|
|
3522
3939
|
.fn()
|
|
@@ -3525,10 +3942,52 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3525
3942
|
|
|
3526
3943
|
await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
|
|
3527
3944
|
|
|
3945
|
+
expect(isRomloader).toHaveBeenCalledTimes(1);
|
|
3528
3946
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
3529
3947
|
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
3530
3948
|
});
|
|
3531
3949
|
|
|
3950
|
+
test('starts Protocol V2 firmware transfer directly from romloader mode', async () => {
|
|
3951
|
+
const method = new FirmwareUpdateV4({
|
|
3952
|
+
id: 1,
|
|
3953
|
+
payload: {
|
|
3954
|
+
method: 'firmwareUpdateV4',
|
|
3955
|
+
platform: 'web',
|
|
3956
|
+
coprocessorBinary: new Uint8Array([1]).buffer,
|
|
3957
|
+
},
|
|
3958
|
+
});
|
|
3959
|
+
method.init();
|
|
3960
|
+
(method as any).device = stubDevice({
|
|
3961
|
+
originalDescriptor: { id: 'usb-id', path: 'romloader-path', protocolType: 'V2' },
|
|
3962
|
+
features: {
|
|
3963
|
+
deviceType: 'pro2',
|
|
3964
|
+
firmwareVersion: '0.0.0',
|
|
3965
|
+
mode: 'romloader',
|
|
3966
|
+
bootloaderMode: true,
|
|
3967
|
+
capabilities: [],
|
|
3968
|
+
},
|
|
3969
|
+
});
|
|
3970
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
3971
|
+
(method as any).protocolV2Reboot = jest.fn();
|
|
3972
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
3973
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
3974
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
3975
|
+
bootloaderVersion: '1.0.0',
|
|
3976
|
+
bleVersion: '0.0.0',
|
|
3977
|
+
firmwareVersion: '1.0.0',
|
|
3978
|
+
});
|
|
3979
|
+
method.postTipMessage = jest.fn();
|
|
3980
|
+
|
|
3981
|
+
await method.run();
|
|
3982
|
+
|
|
3983
|
+
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
3984
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
3985
|
+
bootloaderBinary: null,
|
|
3986
|
+
fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
|
|
3987
|
+
});
|
|
3988
|
+
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
3989
|
+
});
|
|
3990
|
+
|
|
3532
3991
|
test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
|
|
3533
3992
|
const method = new FirmwareUpdateV4({
|
|
3534
3993
|
id: 1,
|
|
@@ -3548,7 +4007,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3548
4007
|
message: protocolV2BootloaderDeviceInfo,
|
|
3549
4008
|
});
|
|
3550
4009
|
}
|
|
3551
|
-
|
|
4010
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
4011
|
+
return Promise.resolve({
|
|
4012
|
+
type: 'ProtocolInfo',
|
|
4013
|
+
message: protocolV2BootloaderInfo,
|
|
4014
|
+
});
|
|
4015
|
+
}
|
|
4016
|
+
return Promise.reject(new Error(`unexpected call ${requestType}`));
|
|
3552
4017
|
});
|
|
3553
4018
|
const reconnectProtocolV2Device = jest
|
|
3554
4019
|
.fn()
|
|
@@ -3565,8 +4030,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3565
4030
|
await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
|
|
3566
4031
|
|
|
3567
4032
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
|
|
3568
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3569
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
4033
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
4034
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
4035
|
+
'DeviceInfoGet',
|
|
4036
|
+
'ProtocolInfoRequest',
|
|
4037
|
+
]);
|
|
3570
4038
|
});
|
|
3571
4039
|
|
|
3572
4040
|
test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
|
|
@@ -4027,6 +4495,19 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4027
4495
|
)
|
|
4028
4496
|
).toBe(true);
|
|
4029
4497
|
|
|
4498
|
+
(method.postProgressMessage as jest.Mock).mockClear();
|
|
4499
|
+
expect(
|
|
4500
|
+
(method as any).assertProtocolV2TargetStatus(
|
|
4501
|
+
[
|
|
4502
|
+
{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
|
|
4503
|
+
{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
|
|
4504
|
+
],
|
|
4505
|
+
new Set([4, 10])
|
|
4506
|
+
)
|
|
4507
|
+
).toBe(false);
|
|
4508
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
|
|
4509
|
+
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4510
|
+
|
|
4030
4511
|
expect(
|
|
4031
4512
|
(method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds)
|
|
4032
4513
|
).toBe(false);
|
|
@@ -4632,7 +5113,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4632
5113
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4633
5114
|
|
|
4634
5115
|
const order: string[] = [];
|
|
4635
|
-
const resourceBinary =
|
|
5116
|
+
const resourceBinary = buildOkppHeader().buffer as ArrayBuffer;
|
|
4636
5117
|
const getSysResourceBinarySpy = jest
|
|
4637
5118
|
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
4638
5119
|
.mockImplementation(() => {
|
|
@@ -4700,6 +5181,31 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4700
5181
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
4701
5182
|
});
|
|
4702
5183
|
|
|
5184
|
+
test('rejects a remote RESC bundle without an OKPP header when metadata is absent', async () => {
|
|
5185
|
+
const method = new FirmwareUpdateV4({
|
|
5186
|
+
id: 1,
|
|
5187
|
+
payload: {
|
|
5188
|
+
method: 'firmwareUpdateV4',
|
|
5189
|
+
},
|
|
5190
|
+
});
|
|
5191
|
+
const getSysResourceBinarySpy = jest
|
|
5192
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5193
|
+
.mockResolvedValue({ binary: new Uint8Array([1, 2, 3]).buffer });
|
|
5194
|
+
|
|
5195
|
+
await expect(
|
|
5196
|
+
(method as any).prefetchProtocolV2ResourceBundles([
|
|
5197
|
+
{
|
|
5198
|
+
name: 'images',
|
|
5199
|
+
binary: new ArrayBuffer(0),
|
|
5200
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5201
|
+
url: 'https://example.com/images.okpkg',
|
|
5202
|
+
},
|
|
5203
|
+
])
|
|
5204
|
+
).rejects.toThrow('Invalid Protocol V2 RESC bundle header: images');
|
|
5205
|
+
|
|
5206
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5207
|
+
});
|
|
5208
|
+
|
|
4703
5209
|
test('rejects an empty remote RESC bundle before entering bootloader', async () => {
|
|
4704
5210
|
const method = new FirmwareUpdateV4({
|
|
4705
5211
|
id: 1,
|
|
@@ -5768,7 +6274,10 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5768
6274
|
}
|
|
5769
6275
|
if (requestType === 'DeviceSessionGet') {
|
|
5770
6276
|
calls.push('resume-hidden-session');
|
|
5771
|
-
expect(request).toEqual({
|
|
6277
|
+
expect(request).toEqual({
|
|
6278
|
+
session_id: 'hidden-session',
|
|
6279
|
+
btc_test_address: 'hidden-state',
|
|
6280
|
+
});
|
|
5772
6281
|
return Promise.resolve({
|
|
5773
6282
|
message: {
|
|
5774
6283
|
session_id: 'hidden-session',
|
|
@@ -5782,6 +6291,10 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5782
6291
|
features: { unlocked: true, passphraseProtection: true },
|
|
5783
6292
|
passphraseState: 'hidden-state',
|
|
5784
6293
|
commands: { typedCall },
|
|
6294
|
+
ensureProtocolV2RuntimeContext: () =>
|
|
6295
|
+
typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6296
|
+
eventless_wallet_session: true,
|
|
6297
|
+
}).then(response => response.message),
|
|
5785
6298
|
isProtocolV2: () => true,
|
|
5786
6299
|
unlockDevice: jest.fn(() => {
|
|
5787
6300
|
calls.push('unlock');
|
|
@@ -5830,6 +6343,10 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5830
6343
|
features: { unlocked: true, passphraseProtection: true },
|
|
5831
6344
|
passphraseState: 'hidden-state',
|
|
5832
6345
|
commands: { typedCall },
|
|
6346
|
+
ensureProtocolV2RuntimeContext: () =>
|
|
6347
|
+
typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6348
|
+
eventless_wallet_session: true,
|
|
6349
|
+
}).then(response => response.message),
|
|
5833
6350
|
isProtocolV2: () => true,
|
|
5834
6351
|
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
5835
6352
|
getCurrentPassphraseProtection: () => true,
|
|
@@ -5865,7 +6382,10 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5865
6382
|
}
|
|
5866
6383
|
if (requestType === 'DeviceSessionGet') {
|
|
5867
6384
|
calls.push('resume-hidden-session');
|
|
5868
|
-
expect(request).toEqual({
|
|
6385
|
+
expect(request).toEqual({
|
|
6386
|
+
session_id: 'hidden-session',
|
|
6387
|
+
btc_test_address: 'hidden-state',
|
|
6388
|
+
});
|
|
5869
6389
|
return Promise.resolve({
|
|
5870
6390
|
message: {
|
|
5871
6391
|
session_id: 'hidden-session',
|
|
@@ -5884,6 +6404,10 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5884
6404
|
features,
|
|
5885
6405
|
passphraseState: 'hidden-state',
|
|
5886
6406
|
commands: { typedCall },
|
|
6407
|
+
ensureProtocolV2RuntimeContext: () =>
|
|
6408
|
+
typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6409
|
+
eventless_wallet_session: true,
|
|
6410
|
+
}).then(response => response.message),
|
|
5887
6411
|
isProtocolV2: () => true,
|
|
5888
6412
|
unlockDevice: jest.fn(() => {
|
|
5889
6413
|
calls.push('unlock-main');
|
|
@@ -5901,7 +6425,12 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5901
6425
|
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
5902
6426
|
message: 'ok',
|
|
5903
6427
|
});
|
|
5904
|
-
expect(device.unlockDevice).toHaveBeenCalledWith(
|
|
6428
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(undefined, {
|
|
6429
|
+
source: 'unlock-coordinator',
|
|
6430
|
+
reason: 'device-locked',
|
|
6431
|
+
deviceOnly: true,
|
|
6432
|
+
method: 'evmSignMessage',
|
|
6433
|
+
});
|
|
5905
6434
|
expect(calls).toEqual([
|
|
5906
6435
|
'unlock-main',
|
|
5907
6436
|
'negotiate-session',
|
|
@@ -5933,7 +6462,7 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5933
6462
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5934
6463
|
});
|
|
5935
6464
|
|
|
5936
|
-
test('
|
|
6465
|
+
test('refreshes status and unlocks before a protected Protocol V2 method', async () => {
|
|
5937
6466
|
const calls: string[] = [];
|
|
5938
6467
|
const method = {
|
|
5939
6468
|
name: 'deviceSettingsPageShow',
|
|
@@ -5944,11 +6473,23 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5944
6473
|
return Promise.resolve({ message: 'ok' });
|
|
5945
6474
|
}),
|
|
5946
6475
|
};
|
|
6476
|
+
const features = { unlocked: true };
|
|
5947
6477
|
const device = {
|
|
5948
|
-
features
|
|
6478
|
+
features,
|
|
6479
|
+
commands: {
|
|
6480
|
+
typedCall: jest.fn(() => {
|
|
6481
|
+
calls.push('refresh-status');
|
|
6482
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
6483
|
+
}),
|
|
6484
|
+
},
|
|
5949
6485
|
isProtocolV2: () => true,
|
|
6486
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6487
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6488
|
+
return features;
|
|
6489
|
+
}),
|
|
5950
6490
|
unlockDevice: jest.fn(() => {
|
|
5951
6491
|
calls.push('unlock');
|
|
6492
|
+
features.unlocked = true;
|
|
5952
6493
|
return Promise.resolve();
|
|
5953
6494
|
}),
|
|
5954
6495
|
};
|
|
@@ -5961,12 +6502,66 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5961
6502
|
await expect(
|
|
5962
6503
|
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
5963
6504
|
).resolves.toEqual({ message: 'ok' });
|
|
5964
|
-
expect(calls).toEqual(['unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6505
|
+
expect(calls).toEqual(['refresh-status', 'unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6506
|
+
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
5965
6507
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5966
6508
|
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
5967
6509
|
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
5968
6510
|
});
|
|
5969
6511
|
|
|
6512
|
+
test('uses refreshed unlocked status instead of a stale locked cache', async () => {
|
|
6513
|
+
const features = { unlocked: false };
|
|
6514
|
+
const method = {
|
|
6515
|
+
name: 'deviceSettings',
|
|
6516
|
+
unlockPolicy: 'unlock-before-run',
|
|
6517
|
+
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6518
|
+
};
|
|
6519
|
+
const device = {
|
|
6520
|
+
features,
|
|
6521
|
+
commands: {
|
|
6522
|
+
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
|
|
6523
|
+
},
|
|
6524
|
+
isProtocolV2: () => true,
|
|
6525
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6526
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6527
|
+
return features;
|
|
6528
|
+
}),
|
|
6529
|
+
unlockDevice: jest.fn(),
|
|
6530
|
+
};
|
|
6531
|
+
|
|
6532
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6533
|
+
message: 'ok',
|
|
6534
|
+
});
|
|
6535
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6536
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6537
|
+
});
|
|
6538
|
+
|
|
6539
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
6540
|
+
'skips DeviceStatus and unlock in Protocol V2 %s mode',
|
|
6541
|
+
async mode => {
|
|
6542
|
+
const method = {
|
|
6543
|
+
name: 'firmwareUpdateV4',
|
|
6544
|
+
unlockPolicy: 'unlock-before-run',
|
|
6545
|
+
run: jest.fn().mockResolvedValue({ message: 'updating' }),
|
|
6546
|
+
};
|
|
6547
|
+
const device = {
|
|
6548
|
+
features: { unlocked: false },
|
|
6549
|
+
commands: { typedCall: jest.fn() },
|
|
6550
|
+
isProtocolV2: () => true,
|
|
6551
|
+
isBootloader: () => mode === 'bootloader',
|
|
6552
|
+
isRomloader: () => mode === 'romloader',
|
|
6553
|
+
unlockDevice: jest.fn(),
|
|
6554
|
+
};
|
|
6555
|
+
|
|
6556
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6557
|
+
message: 'updating',
|
|
6558
|
+
});
|
|
6559
|
+
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
6560
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6561
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6562
|
+
}
|
|
6563
|
+
);
|
|
6564
|
+
|
|
5970
6565
|
test('infers a signing interaction before running a Protocol V2 business method', async () => {
|
|
5971
6566
|
const method = {
|
|
5972
6567
|
name: 'evmSignMessage',
|
|
@@ -6045,6 +6640,38 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6045
6640
|
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6046
6641
|
});
|
|
6047
6642
|
|
|
6643
|
+
test('keeps the PIN prompt when an eventless method actually unlocks the device', async () => {
|
|
6644
|
+
const lockedError = deviceLockedError();
|
|
6645
|
+
const method = {
|
|
6646
|
+
name: 'backgroundRead',
|
|
6647
|
+
unlockPolicy: 'retry-on-locked',
|
|
6648
|
+
protocolV2UiMode: 'none',
|
|
6649
|
+
run: jest.fn().mockRejectedValueOnce(lockedError).mockResolvedValueOnce({ message: 'ok' }),
|
|
6650
|
+
};
|
|
6651
|
+
const device = {
|
|
6652
|
+
isProtocolV2: () => true,
|
|
6653
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6654
|
+
};
|
|
6655
|
+
const uiCoordinator = {
|
|
6656
|
+
enterMethodInteraction: jest.fn(),
|
|
6657
|
+
enterUnlockInteraction: jest.fn(),
|
|
6658
|
+
resumeMethodInteraction: jest.fn(),
|
|
6659
|
+
};
|
|
6660
|
+
|
|
6661
|
+
await expect(
|
|
6662
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
6663
|
+
).resolves.toEqual({ message: 'ok' });
|
|
6664
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(undefined, {
|
|
6665
|
+
source: 'unlock-coordinator',
|
|
6666
|
+
reason: 'device-locked',
|
|
6667
|
+
deviceOnly: true,
|
|
6668
|
+
method: 'backgroundRead',
|
|
6669
|
+
});
|
|
6670
|
+
expect(uiCoordinator.enterMethodInteraction).not.toHaveBeenCalled();
|
|
6671
|
+
expect(uiCoordinator.enterUnlockInteraction).not.toHaveBeenCalled();
|
|
6672
|
+
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6673
|
+
});
|
|
6674
|
+
|
|
6048
6675
|
test('does not unlock or replay a lock-free state-changing method after a locked response', async () => {
|
|
6049
6676
|
const error = deviceLockedError();
|
|
6050
6677
|
const method = {
|
|
@@ -6249,7 +6876,9 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
6249
6876
|
|
|
6250
6877
|
await method.run();
|
|
6251
6878
|
|
|
6252
|
-
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6879
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6880
|
+
eventless_wallet_session: true,
|
|
6881
|
+
});
|
|
6253
6882
|
});
|
|
6254
6883
|
|
|
6255
6884
|
test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
|