@onekeyfe/hd-core 1.2.0-alpha.34 → 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__/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 +78 -30
- package/__tests__/open-wallet-session.test.ts +186 -51
- package/__tests__/protocol-v2.test.ts +490 -112
- 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/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/device/Device.d.ts +3 -2
- 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/index.d.ts +8 -6
- package/dist/index.js +606 -641
- 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 -0
- 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/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +26 -8
- package/src/api/GetPassphraseState.ts +11 -0
- package/src/api/OpenWalletSession.ts +15 -7
- 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/data/messages/messages-protocol-v2.json +393 -485
- package/src/device/Device.ts +63 -37
- package/src/device/DeviceCommands.ts +12 -2
- package/src/device/DeviceWalletSessionStore.ts +0 -14
- package/src/deviceProfile/buildDeviceFeatures.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 +35 -35
- package/src/types/api/openWalletSession.ts +1 -1
- package/src/types/api/protocolV2.ts +2 -2
- package/src/types/device.ts +2 -0
|
@@ -64,11 +64,15 @@ import { DevicePool } from '../src/device/DevicePool';
|
|
|
64
64
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
65
65
|
import {
|
|
66
66
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
67
|
+
PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE,
|
|
67
68
|
type ProtocolV2DeviceInfo,
|
|
68
69
|
type ProtocolV2RuntimeMode,
|
|
69
70
|
getProtocolV2RuntimeMode,
|
|
71
|
+
parseProtocolV2BuildFingerprint,
|
|
70
72
|
requestProtocolV2DeviceInfo,
|
|
73
|
+
requestProtocolV2ProtocolInfo,
|
|
71
74
|
requestProtocolV2DeviceStatus,
|
|
75
|
+
supportsProtocolV2Message,
|
|
72
76
|
} from '../src/protocols/protocol-v2/features';
|
|
73
77
|
import {
|
|
74
78
|
getProtocolV2WalletSession,
|
|
@@ -95,7 +99,7 @@ import {
|
|
|
95
99
|
|
|
96
100
|
import type { DeviceCommands } from '../src/device/DeviceCommands';
|
|
97
101
|
import type { Features } from '../src/types';
|
|
98
|
-
import type { DeviceStatus } from '@onekeyfe/hd-transport';
|
|
102
|
+
import type { DeviceStatus, ProtocolInfo } from '@onekeyfe/hd-transport';
|
|
99
103
|
|
|
100
104
|
jest.mock('../src/data/config', () => ({
|
|
101
105
|
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
@@ -143,7 +147,7 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
143
147
|
const result = await method.run();
|
|
144
148
|
|
|
145
149
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
146
|
-
expect(method.unlockPolicy).toBe('
|
|
150
|
+
expect(method.unlockPolicy).toBe('none');
|
|
147
151
|
expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
|
|
148
152
|
path: 'vol1:/wallpapers',
|
|
149
153
|
});
|
|
@@ -347,6 +351,24 @@ const descriptor = {
|
|
|
347
351
|
path: 'usb-path',
|
|
348
352
|
};
|
|
349
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
|
+
|
|
350
372
|
/**
|
|
351
373
|
* Add Device protocol and getCurrent* accessors to plain-object stubs.
|
|
352
374
|
* Defaults match Device.ts and never replace an existing member such as jest.fn().
|
|
@@ -356,6 +378,8 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
356
378
|
d.updateState ??= jest.fn();
|
|
357
379
|
d.markProtocolV2Reboot ??= jest.fn();
|
|
358
380
|
d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
|
|
381
|
+
d.isBootloader ??= () => d.features?.mode === 'bootloader';
|
|
382
|
+
d.isRomloader ??= () => d.features?.mode === 'romloader';
|
|
359
383
|
d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1');
|
|
360
384
|
d.getCurrentDeviceType ??= () => getDeviceType(d.features);
|
|
361
385
|
d.getCurrentFirmwareType ??= () => getFirmwareType(d.features);
|
|
@@ -375,7 +399,8 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
375
399
|
d.updateProtocolV2Features ??= (
|
|
376
400
|
deviceInfo?: ProtocolV2DeviceInfo,
|
|
377
401
|
deviceStatus?: DeviceStatus,
|
|
378
|
-
runtimeMode?: ProtocolV2RuntimeMode
|
|
402
|
+
runtimeMode?: ProtocolV2RuntimeMode,
|
|
403
|
+
protocolInfo?: ProtocolInfo
|
|
379
404
|
) => {
|
|
380
405
|
d.features = buildProtocolV2FeaturesPayload({
|
|
381
406
|
deviceInfo,
|
|
@@ -383,44 +408,35 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
383
408
|
previous: d.features,
|
|
384
409
|
runtimeMode: runtimeMode ?? (deviceStatus ? 'normal' : undefined),
|
|
385
410
|
});
|
|
411
|
+
if (protocolInfo) {
|
|
412
|
+
d.features.raw = {
|
|
413
|
+
...d.features.raw,
|
|
414
|
+
protocolV2ProtocolInfo: protocolInfo,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
386
417
|
return d.features;
|
|
387
418
|
};
|
|
388
419
|
d.probeProtocolV2RuntimeState ??= async (
|
|
389
420
|
deviceInfo: ProtocolV2DeviceInfo,
|
|
390
421
|
timeoutMs?: number
|
|
391
422
|
) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
});
|
|
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);
|
|
423
434
|
}
|
|
435
|
+
const deviceStatus = await requestProtocolV2DeviceStatus({
|
|
436
|
+
commands: d.getCommands(),
|
|
437
|
+
timeoutMs,
|
|
438
|
+
});
|
|
439
|
+
return d.updateProtocolV2Features(deviceInfo, deviceStatus, 'normal', protocolInfo);
|
|
424
440
|
};
|
|
425
441
|
d.updateProtocolV2Status ??= (deviceStatus: DeviceStatus) => {
|
|
426
442
|
d.features = buildProtocolV2FeaturesPayload({
|
|
@@ -680,7 +696,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
680
696
|
expect(features.bleEnabled).toBeNull();
|
|
681
697
|
});
|
|
682
698
|
|
|
683
|
-
test('
|
|
699
|
+
test('parses bootloader runtime mode from ProtocolInfo build fingerprint', () => {
|
|
684
700
|
const deviceInfo = {
|
|
685
701
|
protocol_version: 1,
|
|
686
702
|
hw: {
|
|
@@ -693,10 +709,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
693
709
|
},
|
|
694
710
|
se1: {},
|
|
695
711
|
};
|
|
696
|
-
const runtimeMode = getProtocolV2RuntimeMode(
|
|
697
|
-
deviceInfo: deviceInfo as ProtocolV2DeviceInfo,
|
|
698
|
-
deviceStatusAvailable: false,
|
|
699
|
-
});
|
|
712
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolV2BootloaderInfo);
|
|
700
713
|
const features = normalizeProtocolV2Features(
|
|
701
714
|
descriptor as any,
|
|
702
715
|
deviceInfo as ProtocolV2DeviceInfo,
|
|
@@ -718,7 +731,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
718
731
|
expect(features.bootloaderMode).toBe(false);
|
|
719
732
|
});
|
|
720
733
|
|
|
721
|
-
test('
|
|
734
|
+
test('keeps romloader distinct from bootloader using ProtocolInfo build fingerprint', () => {
|
|
722
735
|
const deviceInfo = {
|
|
723
736
|
protocol_version: 1,
|
|
724
737
|
hw: {
|
|
@@ -733,20 +746,28 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
733
746
|
},
|
|
734
747
|
},
|
|
735
748
|
};
|
|
736
|
-
const runtimeMode = getProtocolV2RuntimeMode(
|
|
737
|
-
deviceInfo: deviceInfo as ProtocolV2DeviceInfo,
|
|
738
|
-
deviceStatusAvailable: false,
|
|
739
|
-
});
|
|
749
|
+
const runtimeMode = getProtocolV2RuntimeMode(protocolV2RomloaderInfo);
|
|
740
750
|
const features = normalizeProtocolV2Features(
|
|
741
751
|
descriptor as any,
|
|
742
752
|
deviceInfo as ProtocolV2DeviceInfo,
|
|
743
753
|
runtimeMode
|
|
744
754
|
);
|
|
745
|
-
expect(features.mode).toBe('
|
|
755
|
+
expect(features.mode).toBe('romloader');
|
|
746
756
|
expect(features.bootloaderMode).toBe(true);
|
|
747
757
|
expect(features.boardVersion).toBe('1.0.0');
|
|
748
758
|
});
|
|
749
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
|
+
|
|
750
771
|
test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
|
|
751
772
|
const features = normalizeProtocolV2Features(descriptor as any);
|
|
752
773
|
features.firmwareVersion = '1.2.3';
|
|
@@ -908,7 +929,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
908
929
|
expect(deviceWalletSessionStore.get(deviceId, 'hidden-state')).toBe('hidden-session');
|
|
909
930
|
});
|
|
910
931
|
|
|
911
|
-
test('
|
|
932
|
+
test('propagates DeviceLocked instead of retrying a cached standard session', async () => {
|
|
912
933
|
const deviceId = 'standard-locked-device';
|
|
913
934
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
914
935
|
(device as any).features = normalizeProtocolV2Features(
|
|
@@ -950,14 +971,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
950
971
|
});
|
|
951
972
|
(device as any).commands = { typedCall };
|
|
952
973
|
|
|
953
|
-
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).
|
|
954
|
-
|
|
955
|
-
resumed: true,
|
|
974
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).rejects.toMatchObject({
|
|
975
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
956
976
|
});
|
|
957
|
-
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(
|
|
958
|
-
expect(deviceWalletSessionStore.getStandard(deviceId)
|
|
959
|
-
'cached-standard-session'
|
|
960
|
-
);
|
|
977
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
978
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toBeUndefined();
|
|
961
979
|
});
|
|
962
980
|
|
|
963
981
|
test('recovers a mismatched cached standard wallet without deleting hidden sessions', async () => {
|
|
@@ -1083,7 +1101,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1083
1101
|
});
|
|
1084
1102
|
});
|
|
1085
1103
|
|
|
1086
|
-
test('
|
|
1104
|
+
test('does not implicitly unlock inside the Protocol V2 wallet-session coordinator', async () => {
|
|
1087
1105
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1088
1106
|
(device as any).features = normalizeProtocolV2Features(
|
|
1089
1107
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
@@ -1094,13 +1112,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1094
1112
|
},
|
|
1095
1113
|
}
|
|
1096
1114
|
);
|
|
1097
|
-
const lockedError = Object.assign(new Error('Device is locked'), {
|
|
1098
|
-
errorCode: HardwareErrorCode.DeviceLocked,
|
|
1099
|
-
});
|
|
1100
1115
|
const typedCall = createWalletSessionTypedCall(
|
|
1101
1116
|
jest
|
|
1102
1117
|
.fn()
|
|
1103
|
-
.mockRejectedValueOnce(lockedError)
|
|
1104
1118
|
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1105
1119
|
.mockResolvedValueOnce({
|
|
1106
1120
|
message: {
|
|
@@ -1127,7 +1141,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1127
1141
|
resumed: false,
|
|
1128
1142
|
});
|
|
1129
1143
|
|
|
1130
|
-
expect(unlockDevice).
|
|
1144
|
+
expect(unlockDevice).not.toHaveBeenCalled();
|
|
1131
1145
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
1132
1146
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
1133
1147
|
eventless_wallet_session: true,
|
|
@@ -1199,7 +1213,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1199
1213
|
throw new Error(`Unexpected request: ${request}`);
|
|
1200
1214
|
})
|
|
1201
1215
|
);
|
|
1202
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1216
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1203
1217
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
1204
1218
|
|
|
1205
1219
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
@@ -1209,7 +1223,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1209
1223
|
|
|
1210
1224
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
1211
1225
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
1212
|
-
|
|
1226
|
+
passphrase: 'host hidden wallet',
|
|
1227
|
+
on_device: false,
|
|
1213
1228
|
});
|
|
1214
1229
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1215
1230
|
});
|
|
@@ -1240,7 +1255,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1240
1255
|
|
|
1241
1256
|
test('deviceGetOnboardingStatus returns the current Protocol V2 onboarding status', async () => {
|
|
1242
1257
|
const typedCall = jest.fn().mockResolvedValue({
|
|
1243
|
-
type: '
|
|
1258
|
+
type: 'OnboardingStatus',
|
|
1244
1259
|
message: {
|
|
1245
1260
|
step: 4,
|
|
1246
1261
|
phase: 4,
|
|
@@ -1268,7 +1283,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1268
1283
|
pin_set: true,
|
|
1269
1284
|
wallet_initialized: false,
|
|
1270
1285
|
});
|
|
1271
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
1286
|
+
expect(typedCall).toHaveBeenCalledWith('OnboardingStatusGet', 'OnboardingStatus', {});
|
|
1272
1287
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
1273
1288
|
});
|
|
1274
1289
|
|
|
@@ -1420,7 +1435,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1420
1435
|
);
|
|
1421
1436
|
(device as any).commands = {
|
|
1422
1437
|
typedCall,
|
|
1423
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1438
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1424
1439
|
};
|
|
1425
1440
|
|
|
1426
1441
|
await expect(getProtocolV2WalletSession(device)).rejects.toMatchObject({
|
|
@@ -1428,7 +1443,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1428
1443
|
});
|
|
1429
1444
|
expect(typedCall.mock.calls).toEqual([
|
|
1430
1445
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1431
|
-
[
|
|
1446
|
+
[
|
|
1447
|
+
'DeviceSessionAskPassphrase',
|
|
1448
|
+
'Success',
|
|
1449
|
+
{ passphrase: 'host hidden wallet', on_device: false },
|
|
1450
|
+
],
|
|
1432
1451
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1433
1452
|
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1434
1453
|
]);
|
|
@@ -1469,7 +1488,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1469
1488
|
throw new Error(`Unexpected request: ${request}`);
|
|
1470
1489
|
})
|
|
1471
1490
|
);
|
|
1472
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1491
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1473
1492
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
1474
1493
|
|
|
1475
1494
|
await expect(
|
|
@@ -1522,7 +1541,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1522
1541
|
);
|
|
1523
1542
|
(device as any).commands = {
|
|
1524
1543
|
typedCall,
|
|
1525
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1544
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1526
1545
|
};
|
|
1527
1546
|
|
|
1528
1547
|
await expect(
|
|
@@ -1593,7 +1612,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1593
1612
|
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1594
1613
|
});
|
|
1595
1614
|
|
|
1596
|
-
test('
|
|
1615
|
+
test('propagates DeviceLocked without retrying DeviceSessionAskPassphrase', async () => {
|
|
1597
1616
|
const device = Device.fromDescriptor({
|
|
1598
1617
|
id: 'cache-device-4',
|
|
1599
1618
|
path: 'cache-path-4',
|
|
@@ -1620,16 +1639,61 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1620
1639
|
);
|
|
1621
1640
|
(device as any).commands = {
|
|
1622
1641
|
typedCall,
|
|
1623
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1642
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1624
1643
|
};
|
|
1625
1644
|
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockResolvedValue(undefined as any);
|
|
1626
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
|
+
|
|
1627
1688
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
1628
|
-
newSession: 'session-after-unlock',
|
|
1629
|
-
passphraseState: 'state-after-unlock',
|
|
1689
|
+
newSession: 'session-after-pre-unlock',
|
|
1690
|
+
passphraseState: 'state-after-pre-unlock',
|
|
1630
1691
|
});
|
|
1631
|
-
expect(unlockDevice).
|
|
1632
|
-
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']);
|
|
1633
1697
|
});
|
|
1634
1698
|
|
|
1635
1699
|
test('does not request a Pro2 wallet session before features are initialized', async () => {
|
|
@@ -1772,6 +1836,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1772
1836
|
};
|
|
1773
1837
|
const typedCall = jest
|
|
1774
1838
|
.fn()
|
|
1839
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1775
1840
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1776
1841
|
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1777
1842
|
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
@@ -1784,7 +1849,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1784
1849
|
});
|
|
1785
1850
|
const updateInternalState = jest.fn();
|
|
1786
1851
|
const getFeatures = jest.fn().mockResolvedValue(features);
|
|
1787
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1852
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1788
1853
|
const method = new GetPassphraseState({
|
|
1789
1854
|
payload: {
|
|
1790
1855
|
method: 'getPassphraseState',
|
|
@@ -1839,7 +1904,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1839
1904
|
session_id: 'recovered-session',
|
|
1840
1905
|
},
|
|
1841
1906
|
});
|
|
1842
|
-
const promptPassphrase = jest.fn().mockResolvedValue({
|
|
1907
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
|
|
1843
1908
|
const device = stubDevice({
|
|
1844
1909
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
1845
1910
|
passphraseState: 'expected-state',
|
|
@@ -1914,7 +1979,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1914
1979
|
);
|
|
1915
1980
|
(device as any).commands = {
|
|
1916
1981
|
typedCall,
|
|
1917
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
1982
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1918
1983
|
};
|
|
1919
1984
|
|
|
1920
1985
|
const method = new GetPassphraseState({
|
|
@@ -1970,7 +2035,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1970
2035
|
features,
|
|
1971
2036
|
commands: {
|
|
1972
2037
|
typedCall,
|
|
1973
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2038
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
1974
2039
|
},
|
|
1975
2040
|
updateInternalState,
|
|
1976
2041
|
getCurrentDeviceType: () => 'pro2',
|
|
@@ -1998,6 +2063,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1998
2063
|
};
|
|
1999
2064
|
const typedCall = jest
|
|
2000
2065
|
.fn()
|
|
2066
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
2001
2067
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
2002
2068
|
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
2003
2069
|
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
@@ -2023,7 +2089,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2023
2089
|
passphraseState: 'state-pro2-old',
|
|
2024
2090
|
commands: {
|
|
2025
2091
|
typedCall,
|
|
2026
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2092
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2027
2093
|
},
|
|
2028
2094
|
emit: jest.fn(),
|
|
2029
2095
|
clearInternalState,
|
|
@@ -2066,7 +2132,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2066
2132
|
};
|
|
2067
2133
|
(device as any).commands = {
|
|
2068
2134
|
typedCall,
|
|
2069
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2135
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2070
2136
|
};
|
|
2071
2137
|
|
|
2072
2138
|
await getPassphraseStateWithRefreshDeviceInfo(device);
|
|
@@ -2106,7 +2172,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2106
2172
|
);
|
|
2107
2173
|
(device as any).commands = {
|
|
2108
2174
|
typedCall,
|
|
2109
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2175
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2110
2176
|
};
|
|
2111
2177
|
|
|
2112
2178
|
await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({
|
|
@@ -2121,7 +2187,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2121
2187
|
device.passphraseState = 'state-auto';
|
|
2122
2188
|
expect(device.getInternalState()).toBe('session-auto');
|
|
2123
2189
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2124
|
-
|
|
2190
|
+
passphrase: 'host hidden wallet',
|
|
2191
|
+
on_device: false,
|
|
2125
2192
|
});
|
|
2126
2193
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2127
2194
|
});
|
|
@@ -2191,7 +2258,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2191
2258
|
(device as any).features.unlocked = true;
|
|
2192
2259
|
(device as any).commands = {
|
|
2193
2260
|
typedCall,
|
|
2194
|
-
promptPassphrase: jest.fn().mockResolvedValue({
|
|
2261
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
|
|
2195
2262
|
};
|
|
2196
2263
|
|
|
2197
2264
|
await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
|
|
@@ -2202,7 +2269,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2202
2269
|
|
|
2203
2270
|
expect(device.getInternalState()).toBeUndefined();
|
|
2204
2271
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2205
|
-
|
|
2272
|
+
passphrase: 'host hidden wallet',
|
|
2273
|
+
on_device: false,
|
|
2206
2274
|
});
|
|
2207
2275
|
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2208
2276
|
expect(typedCall).toHaveBeenNthCalledWith(4, 'DeviceSessionGet', 'DeviceSession', {});
|
|
@@ -2564,6 +2632,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2564
2632
|
fw: { application: { version: '1.2.3' } },
|
|
2565
2633
|
},
|
|
2566
2634
|
})
|
|
2635
|
+
.mockResolvedValueOnce({
|
|
2636
|
+
type: 'ProtocolInfo',
|
|
2637
|
+
message: protocolV2ApplicationInfo,
|
|
2638
|
+
})
|
|
2567
2639
|
.mockResolvedValueOnce({
|
|
2568
2640
|
type: 'DeviceStatus',
|
|
2569
2641
|
message: { init_states: true, unlocked: true },
|
|
@@ -2584,7 +2656,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2584
2656
|
expect(device.features?.passphraseProtection).toBeNull();
|
|
2585
2657
|
expect(device.features?.label).toBeNull();
|
|
2586
2658
|
expect(device.features?.firmwareVersion).toBe('1.2.3');
|
|
2587
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2659
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
2588
2660
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2589
2661
|
1,
|
|
2590
2662
|
'DeviceInfoGet',
|
|
@@ -2608,10 +2680,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2608
2680
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
2609
2681
|
}
|
|
2610
2682
|
);
|
|
2611
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, '
|
|
2683
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
2684
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2612
2685
|
});
|
|
2613
2686
|
|
|
2614
|
-
test('recognizes bootloader
|
|
2687
|
+
test('recognizes bootloader from ProtocolInfo without calling DeviceStatusGet', async () => {
|
|
2615
2688
|
const device = Device.fromDescriptor({
|
|
2616
2689
|
path: 'usb-path',
|
|
2617
2690
|
protocolType: 'V2',
|
|
@@ -2626,7 +2699,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2626
2699
|
se1: {},
|
|
2627
2700
|
},
|
|
2628
2701
|
})
|
|
2629
|
-
.
|
|
2702
|
+
.mockResolvedValueOnce({
|
|
2703
|
+
type: 'ProtocolInfo',
|
|
2704
|
+
message: protocolV2BootloaderInfo,
|
|
2705
|
+
});
|
|
2630
2706
|
|
|
2631
2707
|
(device as any).commands = { typedCall };
|
|
2632
2708
|
|
|
@@ -2642,23 +2718,166 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2642
2718
|
}),
|
|
2643
2719
|
expect.anything()
|
|
2644
2720
|
);
|
|
2645
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, '
|
|
2721
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
2722
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2646
2723
|
expect(device.features).toMatchObject({
|
|
2647
2724
|
deviceId: null,
|
|
2648
2725
|
bootloaderMode: true,
|
|
2726
|
+
mode: 'bootloader',
|
|
2649
2727
|
initialized: null,
|
|
2650
2728
|
});
|
|
2729
|
+
expect(device.isBootloader()).toBe(true);
|
|
2730
|
+
expect(device.isRomloader()).toBe(false);
|
|
2651
2731
|
});
|
|
2652
2732
|
|
|
2653
|
-
test('
|
|
2733
|
+
test('recognizes romloader from ProtocolInfo without calling DeviceStatusGet', async () => {
|
|
2654
2734
|
const device = Device.fromDescriptor({
|
|
2655
2735
|
path: 'usb-path',
|
|
2656
2736
|
protocolType: 'V2',
|
|
2657
2737
|
} as any);
|
|
2658
2738
|
const typedCall = jest.fn().mockResolvedValue({
|
|
2659
|
-
type: '
|
|
2660
|
-
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,
|
|
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
|
+
},
|
|
2661
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
|
+
});
|
|
2662
2881
|
const updateProtocolV2Features = jest
|
|
2663
2882
|
.spyOn(device, 'updateProtocolV2Features')
|
|
2664
2883
|
.mockImplementation(() => {
|
|
@@ -2673,12 +2892,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2673
2892
|
})
|
|
2674
2893
|
).rejects.toThrow('state update failed');
|
|
2675
2894
|
|
|
2676
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2895
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
2677
2896
|
expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
|
|
2678
2897
|
expect(updateProtocolV2Features).toHaveBeenCalledWith(
|
|
2679
2898
|
expect.anything(),
|
|
2680
2899
|
expect.objectContaining({ init_states: true }),
|
|
2681
|
-
'normal'
|
|
2900
|
+
'normal',
|
|
2901
|
+
protocolV2ApplicationInfo
|
|
2682
2902
|
);
|
|
2683
2903
|
});
|
|
2684
2904
|
|
|
@@ -2696,6 +2916,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2696
2916
|
fw: { application: { version: '1.2.3' } },
|
|
2697
2917
|
},
|
|
2698
2918
|
})
|
|
2919
|
+
.mockResolvedValueOnce({
|
|
2920
|
+
type: 'ProtocolInfo',
|
|
2921
|
+
message: protocolV2ApplicationInfo,
|
|
2922
|
+
})
|
|
2699
2923
|
.mockResolvedValueOnce({
|
|
2700
2924
|
type: 'DeviceStatus',
|
|
2701
2925
|
message: { init_states: true, unlocked: true },
|
|
@@ -2707,6 +2931,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2707
2931
|
fw: { application: { version: '1.2.4' } },
|
|
2708
2932
|
},
|
|
2709
2933
|
})
|
|
2934
|
+
.mockResolvedValueOnce({
|
|
2935
|
+
type: 'ProtocolInfo',
|
|
2936
|
+
message: protocolV2ApplicationInfo,
|
|
2937
|
+
})
|
|
2710
2938
|
.mockResolvedValueOnce({
|
|
2711
2939
|
type: 'DeviceStatus',
|
|
2712
2940
|
message: { init_states: true, unlocked: true },
|
|
@@ -2728,9 +2956,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2728
2956
|
firmwareVersion: '1.2.4',
|
|
2729
2957
|
passphraseProtection: null,
|
|
2730
2958
|
});
|
|
2731
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2959
|
+
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
2732
2960
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2733
|
-
|
|
2961
|
+
4,
|
|
2734
2962
|
'DeviceInfoGet',
|
|
2735
2963
|
'DeviceInfo',
|
|
2736
2964
|
{
|
|
@@ -2755,14 +2983,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2755
2983
|
|
|
2756
2984
|
test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
|
|
2757
2985
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2758
|
-
const typedCall = jest
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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
|
+
});
|
|
2766
3003
|
(device as any).commands = { typedCall };
|
|
2767
3004
|
|
|
2768
3005
|
await device.initialize();
|
|
@@ -3362,6 +3599,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3362
3599
|
message: { init_states: true, unlocked: true },
|
|
3363
3600
|
});
|
|
3364
3601
|
}
|
|
3602
|
+
if (name === 'ProtocolInfoRequest') {
|
|
3603
|
+
return Promise.resolve({
|
|
3604
|
+
type: 'ProtocolInfo',
|
|
3605
|
+
message: protocolV2ApplicationInfo,
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3365
3608
|
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
3366
3609
|
});
|
|
3367
3610
|
const commands = { typedCall };
|
|
@@ -3406,7 +3649,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3406
3649
|
},
|
|
3407
3650
|
{ timeoutMs: 5000 }
|
|
3408
3651
|
);
|
|
3409
|
-
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
|
+
]);
|
|
3410
3658
|
expect(typedCall).not.toHaveBeenCalledWith('Initialize', 'Features', {});
|
|
3411
3659
|
expect(versions).toEqual({
|
|
3412
3660
|
bootloaderVersion: '0.0.0',
|
|
@@ -3486,7 +3734,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3486
3734
|
message: protocolV2BootloaderDeviceInfo,
|
|
3487
3735
|
});
|
|
3488
3736
|
}
|
|
3489
|
-
|
|
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}`));
|
|
3490
3744
|
});
|
|
3491
3745
|
const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
|
|
3492
3746
|
(method as any).device.isBootloader = () => true;
|
|
@@ -3513,10 +3767,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3513
3767
|
method: 'firmwareUpdateV4',
|
|
3514
3768
|
},
|
|
3515
3769
|
});
|
|
3770
|
+
const isRomloader = jest.fn(() => true);
|
|
3516
3771
|
(method as any).device = stubDevice({
|
|
3517
3772
|
originalDescriptor: { id: 'usb-id', path: 'romloader-path', protocolType: 'V2' },
|
|
3518
|
-
features: {
|
|
3773
|
+
features: {
|
|
3774
|
+
deviceType: 'pro2',
|
|
3775
|
+
mode: 'romloader',
|
|
3776
|
+
bootloaderMode: false,
|
|
3777
|
+
capabilities: [],
|
|
3778
|
+
},
|
|
3519
3779
|
isBootloader: () => false,
|
|
3780
|
+
isRomloader,
|
|
3520
3781
|
});
|
|
3521
3782
|
(method as any).protocolV2Reboot = jest
|
|
3522
3783
|
.fn()
|
|
@@ -3525,10 +3786,52 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3525
3786
|
|
|
3526
3787
|
await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
|
|
3527
3788
|
|
|
3789
|
+
expect(isRomloader).toHaveBeenCalledTimes(1);
|
|
3528
3790
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
3529
3791
|
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
3530
3792
|
});
|
|
3531
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
|
+
|
|
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
|
+
});
|
|
3832
|
+
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
3833
|
+
});
|
|
3834
|
+
|
|
3532
3835
|
test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
|
|
3533
3836
|
const method = new FirmwareUpdateV4({
|
|
3534
3837
|
id: 1,
|
|
@@ -3548,7 +3851,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3548
3851
|
message: protocolV2BootloaderDeviceInfo,
|
|
3549
3852
|
});
|
|
3550
3853
|
}
|
|
3551
|
-
|
|
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}`));
|
|
3552
3861
|
});
|
|
3553
3862
|
const reconnectProtocolV2Device = jest
|
|
3554
3863
|
.fn()
|
|
@@ -3565,8 +3874,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3565
3874
|
await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
|
|
3566
3875
|
|
|
3567
3876
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
|
|
3568
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3569
|
-
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
|
+
]);
|
|
3570
3882
|
});
|
|
3571
3883
|
|
|
3572
3884
|
test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
|
|
@@ -5933,7 +6245,7 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5933
6245
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5934
6246
|
});
|
|
5935
6247
|
|
|
5936
|
-
test('
|
|
6248
|
+
test('refreshes status and unlocks before a protected Protocol V2 method', async () => {
|
|
5937
6249
|
const calls: string[] = [];
|
|
5938
6250
|
const method = {
|
|
5939
6251
|
name: 'deviceSettingsPageShow',
|
|
@@ -5944,11 +6256,23 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5944
6256
|
return Promise.resolve({ message: 'ok' });
|
|
5945
6257
|
}),
|
|
5946
6258
|
};
|
|
6259
|
+
const features = { unlocked: true };
|
|
5947
6260
|
const device = {
|
|
5948
|
-
features
|
|
6261
|
+
features,
|
|
6262
|
+
commands: {
|
|
6263
|
+
typedCall: jest.fn(() => {
|
|
6264
|
+
calls.push('refresh-status');
|
|
6265
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
6266
|
+
}),
|
|
6267
|
+
},
|
|
5949
6268
|
isProtocolV2: () => true,
|
|
6269
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6270
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6271
|
+
return features;
|
|
6272
|
+
}),
|
|
5950
6273
|
unlockDevice: jest.fn(() => {
|
|
5951
6274
|
calls.push('unlock');
|
|
6275
|
+
features.unlocked = true;
|
|
5952
6276
|
return Promise.resolve();
|
|
5953
6277
|
}),
|
|
5954
6278
|
};
|
|
@@ -5961,12 +6285,66 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5961
6285
|
await expect(
|
|
5962
6286
|
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
5963
6287
|
).resolves.toEqual({ message: 'ok' });
|
|
5964
|
-
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', {});
|
|
5965
6290
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5966
6291
|
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
5967
6292
|
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
5968
6293
|
});
|
|
5969
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
|
+
|
|
5970
6348
|
test('infers a signing interaction before running a Protocol V2 business method', async () => {
|
|
5971
6349
|
const method = {
|
|
5972
6350
|
name: 'evmSignMessage',
|