@onekeyfe/hd-core 1.2.0-alpha.16 → 1.2.0-alpha.18
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__/DeviceEventRegistration.test.ts +3 -0
- package/__tests__/deviceFeaturesUtils.test.ts +27 -0
- package/__tests__/evmLedgerLegacySafety.test.ts +3 -0
- package/__tests__/homescreen.test.ts +24 -0
- package/__tests__/kaspaSignTransaction.test.ts +591 -0
- package/__tests__/protocol-v2.test.ts +1006 -204
- package/__tests__/protocolV2FileWrite.test.ts +28 -3
- package/__tests__/protocolV2UiInteraction.test.ts +266 -0
- package/dist/api/BaseMethod.d.ts +4 -0
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +3 -3
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceInfo.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/device/DeviceCancel.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceLock.d.ts.map +1 -1
- package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts +5 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/api/kaspa/helpers/TransferSerialize.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceSessionOpen.d.ts +28 -0
- package/dist/api/protocol-v2/DeviceSessionOpen.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/core/deviceEventRegistration.d.ts +1 -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 +3 -3
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +7 -5
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +2 -2
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts +6 -2
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceProfile.d.ts +3 -2
- package/dist/deviceProfile/buildDeviceProfile.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 +18 -2
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +85 -15
- package/dist/index.js +1515 -554
- package/dist/protocols/protocol-v2/features.d.ts +6 -18
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +1 -1
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/uiInteraction.d.ts +39 -0
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/unlockRetry.d.ts +5 -3
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +2 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/getDeviceInfo.d.ts +2 -1
- package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +2 -2
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/kaspaSignTransaction.d.ts +27 -2
- package/dist/types/api/kaspaSignTransaction.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +4 -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.map +1 -1
- package/dist/utils/homescreen.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 +10 -2
- package/src/api/FirmwareUpdateV4.ts +27 -73
- package/src/api/GetDeviceInfo.ts +9 -2
- package/src/api/GetPassphraseState.ts +5 -2
- package/src/api/UploadPortfolio.ts +2 -0
- package/src/api/device/DeviceCancel.ts +1 -0
- package/src/api/device/DeviceChangePin.ts +23 -0
- package/src/api/device/DeviceLock.ts +1 -0
- package/src/api/device/DeviceUnlock.ts +9 -0
- package/src/api/device/DeviceWipe.ts +18 -0
- package/src/api/helpers/protocolV2FileWrite.ts +35 -11
- package/src/api/index.ts +1 -1
- package/src/api/kaspa/KaspaSignTransaction.ts +341 -22
- package/src/api/kaspa/helpers/TransferSerialize.ts +14 -0
- package/src/api/protocol-v2/DeviceInfoGet.ts +0 -1
- package/src/api/protocol-v2/DeviceSessionOpen.ts +100 -0
- package/src/api/protocol-v2/DeviceSettingsPageShow.ts +9 -1
- package/src/api/protocol-v2/DeviceSettingsSet.ts +11 -1
- package/src/api/protocol-v2/DeviceStatusGet.ts +1 -0
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +2 -1
- package/src/core/deviceEventRegistration.ts +2 -0
- package/src/core/index.ts +36 -3
- package/src/data/messages/messages-protocol-v2.json +122 -23
- package/src/data/messages/messages.json +286 -1
- package/src/device/Device.ts +33 -41
- package/src/device/DeviceCommands.ts +22 -4
- package/src/device/DevicePool.ts +14 -6
- package/src/deviceProfile/buildDeviceFeatures.ts +21 -9
- package/src/deviceProfile/buildDeviceProfile.ts +27 -11
- package/src/events/device.ts +4 -0
- package/src/events/ui-request.ts +24 -2
- package/src/inject.ts +2 -2
- package/src/protocols/protocol-v2/features.ts +27 -20
- package/src/protocols/protocol-v2/index.ts +0 -1
- package/src/protocols/protocol-v2/uiInteraction.ts +209 -0
- package/src/protocols/protocol-v2/unlockRetry.ts +43 -3
- package/src/protocols/protocol-v2/walletSession.ts +122 -31
- package/src/types/api/getDeviceInfo.ts +6 -1
- package/src/types/api/index.ts +2 -2
- package/src/types/api/kaspaSignTransaction.ts +40 -2
- package/src/types/api/protocolV2.ts +5 -3
- package/src/types/device.ts +6 -1
- package/src/utils/deviceFeaturesUtils.ts +13 -0
- package/src/utils/homescreen.ts +10 -0
- package/dist/api/protocol-v2/DeviceSessionGet.d.ts +0 -6
- package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +0 -1
- package/src/api/protocol-v2/DeviceSessionGet.ts +0 -15
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import { DeviceRebootType } from '@onekeyfe/hd-transport';
|
|
2
|
+
import { DeviceRebootType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
4
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
5
5
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -15,11 +15,16 @@ import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmw
|
|
|
15
15
|
import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus';
|
|
16
16
|
import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
|
|
17
17
|
import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
|
|
18
|
+
import DeviceCancel from '../src/api/device/DeviceCancel';
|
|
19
|
+
import DeviceChangePin from '../src/api/device/DeviceChangePin';
|
|
20
|
+
import DeviceLock from '../src/api/device/DeviceLock';
|
|
21
|
+
import DeviceUnlock from '../src/api/device/DeviceUnlock';
|
|
22
|
+
import DeviceWipe from '../src/api/device/DeviceWipe';
|
|
18
23
|
import DeviceSettingsGet from '../src/api/protocol-v2/DeviceSettingsGet';
|
|
19
24
|
import DeviceSettingsPageShow from '../src/api/protocol-v2/DeviceSettingsPageShow';
|
|
20
25
|
import DeviceSettingsSet from '../src/api/protocol-v2/DeviceSettingsSet';
|
|
21
26
|
import DeviceUploadWallpaper from '../src/api/protocol-v2/DeviceUploadWallpaper';
|
|
22
|
-
import
|
|
27
|
+
import DeviceSessionOpen from '../src/api/protocol-v2/DeviceSessionOpen';
|
|
23
28
|
import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet';
|
|
24
29
|
import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat';
|
|
25
30
|
import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
|
|
@@ -53,16 +58,20 @@ import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit'
|
|
|
53
58
|
import { DataManager } from '../src/data-manager';
|
|
54
59
|
import { createCoreApi } from '../src/inject';
|
|
55
60
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
61
|
+
import { DevicePool } from '../src/device/DevicePool';
|
|
62
|
+
import { DEVICE } from '../src/events/device';
|
|
56
63
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
57
64
|
import {
|
|
58
65
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
59
66
|
requestProtocolV2DeviceInfo,
|
|
67
|
+
requestProtocolV2DeviceStatus,
|
|
60
68
|
} from '../src/protocols/protocol-v2/features';
|
|
61
69
|
import {
|
|
62
70
|
getProtocolV2WalletSession,
|
|
63
71
|
refreshProtocolV2DeviceStatus,
|
|
64
72
|
} from '../src/protocols/protocol-v2/walletSession';
|
|
65
73
|
import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry';
|
|
74
|
+
import { BaseMethod } from '../src/api/BaseMethod';
|
|
66
75
|
import {
|
|
67
76
|
buildProfileFromProtocolV2,
|
|
68
77
|
buildProtocolV1FeaturesPayload,
|
|
@@ -84,6 +93,7 @@ import {
|
|
|
84
93
|
|
|
85
94
|
import type { DeviceCommands } from '../src/device/DeviceCommands';
|
|
86
95
|
import type { Features } from '../src/types';
|
|
96
|
+
import type { DeviceStatus } from '@onekeyfe/hd-transport';
|
|
87
97
|
import type { ProtocolV2DeviceInfo } from '../src/protocols/protocol-v2/features';
|
|
88
98
|
|
|
89
99
|
jest.mock('../src/data/config', () => ({
|
|
@@ -118,11 +128,11 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
118
128
|
expect(method.requireProtocolV2).toBe(true);
|
|
119
129
|
expect(method.unlockPolicy).toBe('retry-on-locked');
|
|
120
130
|
expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
|
|
121
|
-
path: '
|
|
131
|
+
path: 'vol1:/wallpapers',
|
|
122
132
|
});
|
|
123
133
|
const fileWriteCall = typedCall.mock.calls.find(call => call[0] === 'FilesystemFileWrite');
|
|
124
134
|
expect(fileWriteCall?.[2]).toMatchObject({
|
|
125
|
-
file: { path: expect.stringMatching(/^
|
|
135
|
+
file: { path: expect.stringMatching(/^vol1:\/wallpapers\/wallpaper-[a-f0-9]+\.bin$/) },
|
|
126
136
|
overwrite: true,
|
|
127
137
|
append: false,
|
|
128
138
|
});
|
|
@@ -206,6 +216,9 @@ describe('UploadPortfolio', () => {
|
|
|
206
216
|
const result = await method.run();
|
|
207
217
|
|
|
208
218
|
expect(method.unlockPolicy).toBe('retry-on-locked');
|
|
219
|
+
expect(method.protocolV2UiMode).toBe('none');
|
|
220
|
+
expect(method.protocolV2UiInteraction).toBeUndefined();
|
|
221
|
+
expect(method.payload.emitProgress).toBe(false);
|
|
209
222
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
210
223
|
1,
|
|
211
224
|
'FilesystemFileWrite',
|
|
@@ -305,8 +318,23 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
305
318
|
if (range) return range;
|
|
306
319
|
return getMethodVersionRange(d.features, fn);
|
|
307
320
|
};
|
|
308
|
-
d.updateProtocolV2Features ??= (
|
|
309
|
-
|
|
321
|
+
d.updateProtocolV2Features ??= (
|
|
322
|
+
deviceInfo?: ProtocolV2DeviceInfo,
|
|
323
|
+
deviceStatus?: DeviceStatus
|
|
324
|
+
) => {
|
|
325
|
+
d.features = buildProtocolV2FeaturesPayload({
|
|
326
|
+
deviceInfo,
|
|
327
|
+
deviceStatus: deviceStatus ?? deviceInfo?.status,
|
|
328
|
+
previous: d.features,
|
|
329
|
+
});
|
|
330
|
+
return d.features;
|
|
331
|
+
};
|
|
332
|
+
d.updateProtocolV2Status ??= (deviceStatus: DeviceStatus) => {
|
|
333
|
+
d.features = buildProtocolV2FeaturesPayload({
|
|
334
|
+
deviceInfo: d.features?.raw?.protocolV2DeviceInfo,
|
|
335
|
+
deviceStatus,
|
|
336
|
+
previous: d.features,
|
|
337
|
+
});
|
|
310
338
|
return d.features;
|
|
311
339
|
};
|
|
312
340
|
return device;
|
|
@@ -314,7 +342,10 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
314
342
|
|
|
315
343
|
// 直接复用生产映射函数,避免测试内副本与实现漂移(之前的手抄副本已缺失 se boot 字段)
|
|
316
344
|
function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: ProtocolV2DeviceInfo) {
|
|
317
|
-
return buildProtocolV2FeaturesPayload(
|
|
345
|
+
return buildProtocolV2FeaturesPayload({
|
|
346
|
+
deviceInfo,
|
|
347
|
+
deviceStatus: deviceInfo?.status,
|
|
348
|
+
});
|
|
318
349
|
}
|
|
319
350
|
|
|
320
351
|
const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
|
|
@@ -337,10 +368,64 @@ async function requestProtocolV2Features({
|
|
|
337
368
|
descriptor?: unknown;
|
|
338
369
|
}) {
|
|
339
370
|
const deviceInfo = await requestProtocolV2DeviceInfo({ commands });
|
|
340
|
-
|
|
371
|
+
const deviceStatus = await requestProtocolV2DeviceStatus({ commands });
|
|
372
|
+
return buildProtocolV2FeaturesPayload({ deviceInfo, deviceStatus });
|
|
341
373
|
}
|
|
342
374
|
|
|
343
375
|
describe('Protocol V2 feature adapter', () => {
|
|
376
|
+
test('treats passphrase status as unknown until the Protocol V2 device is unlocked', () => {
|
|
377
|
+
const features = buildProtocolV2FeaturesPayload({
|
|
378
|
+
deviceInfo: {
|
|
379
|
+
hw: { serial_no: 'P2-LOCKED' },
|
|
380
|
+
},
|
|
381
|
+
deviceStatus: {
|
|
382
|
+
unlocked: false,
|
|
383
|
+
passphrase_enabled: true,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(features.unlocked).toBe(false);
|
|
388
|
+
expect(features.passphraseProtection).toBeNull();
|
|
389
|
+
expect(features.passphrase_protection).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('builds dynamic features from DeviceStatus when DeviceInfo has no nested status', () => {
|
|
393
|
+
const deviceInfo: ProtocolV2DeviceInfo = {
|
|
394
|
+
protocol_version: 1,
|
|
395
|
+
hw: { serial_no: 'P2-STATIC' },
|
|
396
|
+
fw: { application: { version: '1.0.0' } },
|
|
397
|
+
coprocessor: { bt_adv_name: 'Pro2 STATIC' },
|
|
398
|
+
};
|
|
399
|
+
const deviceStatus: DeviceStatus = {
|
|
400
|
+
device_id: 'P2-DYNAMIC',
|
|
401
|
+
unlocked: true,
|
|
402
|
+
init_states: true,
|
|
403
|
+
backup_required: false,
|
|
404
|
+
passphrase_enabled: true,
|
|
405
|
+
attach_to_pin_enabled: true,
|
|
406
|
+
unlocked_by_attach_to_pin: true,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const features = buildProtocolV2FeaturesPayload({
|
|
410
|
+
deviceInfo,
|
|
411
|
+
deviceStatus,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(features).toMatchObject({
|
|
415
|
+
deviceId: 'P2-DYNAMIC',
|
|
416
|
+
serialNo: 'P2-STATIC',
|
|
417
|
+
bleName: 'Pro2 STATIC',
|
|
418
|
+
initialized: true,
|
|
419
|
+
unlocked: true,
|
|
420
|
+
passphraseProtection: true,
|
|
421
|
+
attachToPinEnabled: true,
|
|
422
|
+
unlockedAttachPin: true,
|
|
423
|
+
});
|
|
424
|
+
expect(features.raw?.protocolV2DeviceInfo).toEqual(deviceInfo);
|
|
425
|
+
expect(features.raw?.protocolV2DeviceStatus).toEqual(deviceStatus);
|
|
426
|
+
expect(features.raw?.protocolV2DeviceInfo?.status).toBeUndefined();
|
|
427
|
+
});
|
|
428
|
+
|
|
344
429
|
test('keeps legacy snake_case feature fields for existing SDK consumers', () => {
|
|
345
430
|
const protocolV1 = buildProtocolV1FeaturesPayload({
|
|
346
431
|
device_id: 'v1-device',
|
|
@@ -350,14 +435,16 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
350
435
|
unlocked: true,
|
|
351
436
|
} as any);
|
|
352
437
|
const protocolV2 = buildProtocolV2FeaturesPayload({
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
438
|
+
deviceInfo: {
|
|
439
|
+
hw: { serial_no: 'P2-001' },
|
|
440
|
+
coprocessor: { bt_adv_name: 'Pro 2 BLE' },
|
|
441
|
+
},
|
|
442
|
+
deviceStatus: {
|
|
356
443
|
device_id: 'v2-device',
|
|
357
444
|
unlocked: true,
|
|
358
445
|
passphrase_enabled: true,
|
|
359
446
|
},
|
|
360
|
-
}
|
|
447
|
+
});
|
|
361
448
|
|
|
362
449
|
expect(protocolV1).toMatchObject({
|
|
363
450
|
device_id: 'v1-device',
|
|
@@ -511,7 +598,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
511
598
|
expect(profile.versions.board).toBe('1.0.0');
|
|
512
599
|
});
|
|
513
600
|
|
|
514
|
-
test('
|
|
601
|
+
test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
|
|
515
602
|
const features = normalizeProtocolV2Features(descriptor as any);
|
|
516
603
|
features.firmwareVersion = '1.2.3';
|
|
517
604
|
features.passphraseProtection = true;
|
|
@@ -527,34 +614,164 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
527
614
|
(device as any).features = features;
|
|
528
615
|
(device as any).commands = commands;
|
|
529
616
|
|
|
530
|
-
await getPassphraseState(device, {
|
|
531
|
-
expectPassphraseState: 'state-1',
|
|
532
|
-
});
|
|
533
|
-
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
534
|
-
|
|
535
617
|
await expect(
|
|
536
618
|
getPassphraseState(device, {
|
|
537
|
-
|
|
538
|
-
})
|
|
539
|
-
).rejects.toEqual(
|
|
540
|
-
expect.objectContaining({
|
|
541
|
-
errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
|
|
619
|
+
onlyMainPin: true,
|
|
542
620
|
})
|
|
621
|
+
).resolves.toEqual({
|
|
622
|
+
passphraseState: undefined,
|
|
623
|
+
newSession: undefined,
|
|
624
|
+
unlockedAttachPin: false,
|
|
625
|
+
});
|
|
626
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
627
|
+
expect(device.getInternalState()).toBeUndefined();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test('uses the default main wallet context when Protocol V2 passphrase protection is disabled', async () => {
|
|
631
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
632
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
633
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
634
|
+
{ status: { passphrase_enabled: false, unlocked: true } }
|
|
635
|
+
);
|
|
636
|
+
const typedCall = jest.fn();
|
|
637
|
+
(device as any).commands = { typedCall };
|
|
638
|
+
|
|
639
|
+
await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
|
|
640
|
+
passphraseState: undefined,
|
|
641
|
+
newSession: undefined,
|
|
642
|
+
unlockedAttachPin: false,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('unlocks before deciding that Protocol V2 uses the default main wallet context', async () => {
|
|
649
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
650
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
651
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
652
|
+
{
|
|
653
|
+
status: {
|
|
654
|
+
unlocked: false,
|
|
655
|
+
passphrase_enabled: true,
|
|
656
|
+
},
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
const typedCall = jest.fn();
|
|
660
|
+
const promptPassphrase = jest.fn();
|
|
661
|
+
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockImplementation(() => {
|
|
662
|
+
(device as any).features = {
|
|
663
|
+
...(device as any).features,
|
|
664
|
+
unlocked: true,
|
|
665
|
+
passphraseProtection: false,
|
|
666
|
+
};
|
|
667
|
+
return (device as any).features;
|
|
668
|
+
});
|
|
669
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
670
|
+
|
|
671
|
+
await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
|
|
672
|
+
passphraseState: undefined,
|
|
673
|
+
newSession: undefined,
|
|
674
|
+
unlockedAttachPin: false,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
expect(unlockDevice).toHaveBeenCalledTimes(1);
|
|
678
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
679
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('onlyMainPin takes precedence over a cached hidden-wallet state', async () => {
|
|
683
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
684
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
685
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
686
|
+
{ status: { device_id: 'main-wallet-device', passphrase_enabled: true, unlocked: true } }
|
|
543
687
|
);
|
|
544
|
-
|
|
688
|
+
device.passphraseState = 'hidden-state';
|
|
689
|
+
preloadSessionCache('main-wallet-device', 'hidden-state', 'hidden-session');
|
|
690
|
+
const typedCall = jest.fn();
|
|
691
|
+
(device as any).commands = { typedCall };
|
|
545
692
|
|
|
546
693
|
await getPassphraseState(device, {
|
|
547
694
|
onlyMainPin: true,
|
|
548
695
|
});
|
|
549
|
-
expect(typedCall).
|
|
696
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
697
|
+
expect(device.getInternalState()).toBe('hidden-session');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test.each([
|
|
701
|
+
{
|
|
702
|
+
name: 'Host Passphrase',
|
|
703
|
+
response: { passphrase: 'hidden\u00e9 secret' },
|
|
704
|
+
select: { host_passphrase: { passphrase: 'hidden\u0065\u0301 secret' } },
|
|
705
|
+
event: undefined,
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: '设备 Passphrase',
|
|
709
|
+
response: { passphraseOnDevice: true },
|
|
710
|
+
select: { passphrase_on_device: {} },
|
|
711
|
+
event: DEVICE.PASSPHRASE_ON_DEVICE,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: 'Attach PIN',
|
|
715
|
+
response: { attachPinOnDevice: true },
|
|
716
|
+
select: { attach_pin_on_device: {} },
|
|
717
|
+
event: DEVICE.ATTACH_PIN_ON_DEVICE,
|
|
718
|
+
},
|
|
719
|
+
])('maps $name selection to DeviceSessionOpen', async ({ response, select, event }) => {
|
|
720
|
+
const device = Device.fromDescriptor({
|
|
721
|
+
id: 'select-device',
|
|
722
|
+
path: 'select-path',
|
|
723
|
+
protocolType: 'V2',
|
|
724
|
+
} as any);
|
|
725
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
726
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
727
|
+
{
|
|
728
|
+
status: {
|
|
729
|
+
device_id: 'select-device-id',
|
|
730
|
+
unlocked: true,
|
|
731
|
+
passphrase_enabled: true,
|
|
732
|
+
attach_to_pin_enabled: true,
|
|
733
|
+
},
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
737
|
+
type: 'DeviceSession',
|
|
738
|
+
message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
|
|
739
|
+
});
|
|
740
|
+
const promptPassphrase = jest.fn().mockResolvedValue(response);
|
|
741
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
742
|
+
const emit = jest.spyOn(device, 'emit');
|
|
743
|
+
|
|
744
|
+
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
745
|
+
newSession: 'hidden-session',
|
|
746
|
+
passphraseState: 'hidden-state',
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
750
|
+
expect.objectContaining({
|
|
751
|
+
source: 'wallet-session-coordinator',
|
|
752
|
+
reason: 'open-wallet',
|
|
753
|
+
existsAttachPinUser: true,
|
|
754
|
+
}),
|
|
755
|
+
{ cancelDeviceOnReject: false }
|
|
756
|
+
);
|
|
757
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
758
|
+
select,
|
|
759
|
+
});
|
|
760
|
+
if (event) {
|
|
761
|
+
expect(emit).toHaveBeenCalledWith(
|
|
762
|
+
event,
|
|
763
|
+
device,
|
|
764
|
+
expect.objectContaining({ source: 'wallet-session-coordinator' })
|
|
765
|
+
);
|
|
766
|
+
}
|
|
550
767
|
});
|
|
551
768
|
|
|
552
|
-
test('deviceStatusGet returns raw DeviceStatus
|
|
769
|
+
test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
|
|
553
770
|
const typedCall = jest.fn().mockResolvedValue({
|
|
554
771
|
type: 'DeviceStatus',
|
|
555
772
|
message: { device_id: 'device-1', unlocked: true },
|
|
556
773
|
});
|
|
557
|
-
const
|
|
774
|
+
const updateProtocolV2Status = jest.fn();
|
|
558
775
|
const method = new DeviceStatusGet({
|
|
559
776
|
payload: { method: 'deviceStatusGet', connectId: 'connect-id' },
|
|
560
777
|
});
|
|
@@ -562,18 +779,27 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
562
779
|
method.device = stubDevice({
|
|
563
780
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
564
781
|
commands: { typedCall },
|
|
565
|
-
|
|
782
|
+
updateProtocolV2Status,
|
|
566
783
|
}) as any;
|
|
567
784
|
|
|
568
785
|
await expect(method.run()).resolves.toEqual({ device_id: 'device-1', unlocked: true });
|
|
569
786
|
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
570
|
-
expect(
|
|
787
|
+
expect(updateProtocolV2Status).toHaveBeenCalledWith({
|
|
788
|
+
device_id: 'device-1',
|
|
789
|
+
unlocked: true,
|
|
790
|
+
});
|
|
571
791
|
});
|
|
572
792
|
|
|
573
|
-
test('deviceGetOnboardingStatus returns the
|
|
793
|
+
test('deviceGetOnboardingStatus returns the current Protocol V2 onboarding status', async () => {
|
|
574
794
|
const typedCall = jest.fn().mockResolvedValue({
|
|
575
795
|
type: 'DevOnboardingStatus',
|
|
576
|
-
message: {
|
|
796
|
+
message: {
|
|
797
|
+
step: 4,
|
|
798
|
+
phase: 4,
|
|
799
|
+
setup: { kind: 1, method: 0 },
|
|
800
|
+
pin_set: true,
|
|
801
|
+
wallet_initialized: false,
|
|
802
|
+
},
|
|
577
803
|
});
|
|
578
804
|
const method = new DeviceGetOnboardingStatus({
|
|
579
805
|
payload: {
|
|
@@ -588,24 +814,27 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
588
814
|
}) as any;
|
|
589
815
|
|
|
590
816
|
await expect(method.run()).resolves.toEqual({
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
817
|
+
step: 4,
|
|
818
|
+
phase: 4,
|
|
819
|
+
setup: { kind: 1, method: 0 },
|
|
820
|
+
pin_set: true,
|
|
821
|
+
wallet_initialized: false,
|
|
594
822
|
});
|
|
595
823
|
expect(typedCall).toHaveBeenCalledWith('DevGetOnboardingStatus', 'DevOnboardingStatus', {});
|
|
596
824
|
expect(method.requireProtocolV2).toBe(true);
|
|
597
825
|
});
|
|
598
826
|
|
|
599
|
-
test('
|
|
827
|
+
test('deviceSessionOpen sends the explicit wallet selection and does not mutate wallet cache', async () => {
|
|
600
828
|
const typedCall = jest.fn().mockResolvedValue({
|
|
601
829
|
type: 'DeviceSession',
|
|
602
830
|
message: { session_id: 'new-session', btc_test_address: 'state-a' },
|
|
603
831
|
});
|
|
604
832
|
const updateInternalState = jest.fn();
|
|
605
|
-
const method = new
|
|
833
|
+
const method = new DeviceSessionOpen({
|
|
606
834
|
payload: {
|
|
607
|
-
method: '
|
|
835
|
+
method: 'deviceSessionOpen',
|
|
608
836
|
connectId: 'connect-id',
|
|
837
|
+
select: { host_passphrase: { passphrase: 'hidden secret' } },
|
|
609
838
|
},
|
|
610
839
|
});
|
|
611
840
|
method.init();
|
|
@@ -619,17 +848,42 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
619
848
|
session_id: 'new-session',
|
|
620
849
|
btc_test_address: 'state-a',
|
|
621
850
|
});
|
|
622
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
851
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
852
|
+
select: { host_passphrase: { passphrase: 'hidden secret' } },
|
|
853
|
+
});
|
|
623
854
|
expect(updateInternalState).not.toHaveBeenCalled();
|
|
624
855
|
});
|
|
625
856
|
|
|
857
|
+
test.each([
|
|
858
|
+
{ payload: {}, reason: 'missing mode' },
|
|
859
|
+
{ payload: { resume: { session_id: '' } }, reason: 'empty resume session' },
|
|
860
|
+
{ payload: { select: { wallet_type: 0 } }, reason: 'removed standard wallet selection' },
|
|
861
|
+
{
|
|
862
|
+
payload: { select: { host_passphrase: { passphrase: 'secret' }, passphrase_on_device: {} } },
|
|
863
|
+
reason: 'multiple hidden wallet access modes',
|
|
864
|
+
},
|
|
865
|
+
])('rejects invalid deviceSessionOpen payload: $reason', ({ payload }) => {
|
|
866
|
+
const method = new DeviceSessionOpen({
|
|
867
|
+
payload: {
|
|
868
|
+
method: 'deviceSessionOpen',
|
|
869
|
+
connectId: 'connect-id',
|
|
870
|
+
...payload,
|
|
871
|
+
} as any,
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
expect(() => method.init()).toThrow();
|
|
875
|
+
});
|
|
876
|
+
|
|
626
877
|
test('routes raw status and session methods through CoreApi', async () => {
|
|
627
878
|
const call = jest.fn().mockResolvedValue({ success: true, payload: {} });
|
|
628
879
|
const api = createCoreApi(call as any);
|
|
629
880
|
|
|
630
881
|
await api.deviceStatusGet('connect-id', { retryCount: 1 });
|
|
631
882
|
await api.deviceGetOnboardingStatus('connect-id', { retryCount: 1 });
|
|
632
|
-
await api.
|
|
883
|
+
await api.deviceSessionOpen('connect-id', {
|
|
884
|
+
retryCount: 1,
|
|
885
|
+
select: { passphrase_on_device: {} },
|
|
886
|
+
});
|
|
633
887
|
|
|
634
888
|
expect(call).toHaveBeenNthCalledWith(1, {
|
|
635
889
|
method: 'deviceStatusGet',
|
|
@@ -642,9 +896,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
642
896
|
retryCount: 1,
|
|
643
897
|
});
|
|
644
898
|
expect(call).toHaveBeenNthCalledWith(3, {
|
|
645
|
-
method: '
|
|
899
|
+
method: 'deviceSessionOpen',
|
|
646
900
|
connectId: 'connect-id',
|
|
647
901
|
retryCount: 1,
|
|
902
|
+
select: { passphrase_on_device: {} },
|
|
648
903
|
});
|
|
649
904
|
});
|
|
650
905
|
|
|
@@ -677,8 +932,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
677
932
|
newSession: 'session-b',
|
|
678
933
|
});
|
|
679
934
|
|
|
680
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
681
|
-
session_id: 'session-a',
|
|
935
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
936
|
+
resume: { session_id: 'session-a' },
|
|
682
937
|
});
|
|
683
938
|
expect(device.getInternalState()).toBe('session-b');
|
|
684
939
|
});
|
|
@@ -700,9 +955,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
700
955
|
});
|
|
701
956
|
(device as any).commands = { typedCall };
|
|
702
957
|
|
|
703
|
-
await getProtocolV2WalletSession(device);
|
|
958
|
+
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
704
959
|
|
|
705
|
-
expect(typedCall).
|
|
960
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
706
961
|
expect(device.getInternalState()).toBeUndefined();
|
|
707
962
|
});
|
|
708
963
|
|
|
@@ -731,15 +986,24 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
731
986
|
type: 'DeviceSession',
|
|
732
987
|
message: { session_id: 'session-b', btc_test_address: 'state-a' },
|
|
733
988
|
});
|
|
734
|
-
|
|
989
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'state-a-secret' });
|
|
990
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
735
991
|
|
|
736
992
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
737
993
|
passphraseState: 'state-a',
|
|
738
994
|
newSession: 'session-b',
|
|
739
995
|
});
|
|
740
996
|
expect(typedCall.mock.calls).toEqual([
|
|
741
|
-
['
|
|
742
|
-
[
|
|
997
|
+
['DeviceSessionOpen', 'DeviceSession', { resume: { session_id: 'session-a' } }],
|
|
998
|
+
[
|
|
999
|
+
'DeviceSessionOpen',
|
|
1000
|
+
'DeviceSession',
|
|
1001
|
+
{
|
|
1002
|
+
select: {
|
|
1003
|
+
host_passphrase: { passphrase: 'state-a-secret' },
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
743
1007
|
]);
|
|
744
1008
|
expect(device.getInternalState()).toBe('session-b');
|
|
745
1009
|
});
|
|
@@ -763,10 +1027,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
763
1027
|
const typedCall = jest
|
|
764
1028
|
.fn()
|
|
765
1029
|
.mockRejectedValue(new Error('Failure_ProcessError,Failure_InvalidSession'));
|
|
766
|
-
(device as any).commands = {
|
|
1030
|
+
(device as any).commands = {
|
|
1031
|
+
typedCall,
|
|
1032
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'new-secret' }),
|
|
1033
|
+
};
|
|
767
1034
|
|
|
768
1035
|
await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Failure_InvalidSession');
|
|
769
|
-
expect(typedCall.mock.calls).toEqual([
|
|
1036
|
+
expect(typedCall.mock.calls).toEqual([
|
|
1037
|
+
[
|
|
1038
|
+
'DeviceSessionOpen',
|
|
1039
|
+
'DeviceSession',
|
|
1040
|
+
{
|
|
1041
|
+
select: {
|
|
1042
|
+
host_passphrase: { passphrase: 'new-secret' },
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
]);
|
|
770
1047
|
});
|
|
771
1048
|
|
|
772
1049
|
test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
|
|
@@ -804,7 +1081,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
804
1081
|
expect(device.getInternalState()).toBeUndefined();
|
|
805
1082
|
});
|
|
806
1083
|
|
|
807
|
-
test('unlocks and retries
|
|
1084
|
+
test('unlocks and retries DeviceSessionOpen when hidden-wallet selection is locked', async () => {
|
|
808
1085
|
const device = Device.fromDescriptor({
|
|
809
1086
|
id: 'cache-device-4',
|
|
810
1087
|
path: 'cache-path-4',
|
|
@@ -812,7 +1089,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
812
1089
|
} as any);
|
|
813
1090
|
(device as any).features = normalizeProtocolV2Features(
|
|
814
1091
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
815
|
-
{ status: { device_id: 'stable-device-4', unlocked:
|
|
1092
|
+
{ status: { device_id: 'stable-device-4', unlocked: true, passphrase_enabled: true } }
|
|
816
1093
|
);
|
|
817
1094
|
const lockedError = Object.assign(new Error('Device is locked'), {
|
|
818
1095
|
errorCode: HardwareErrorCode.DeviceLocked,
|
|
@@ -823,7 +1100,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
823
1100
|
.mockResolvedValueOnce({
|
|
824
1101
|
message: { session_id: 'session-after-unlock', btc_test_address: '' },
|
|
825
1102
|
});
|
|
826
|
-
(device as any).commands = {
|
|
1103
|
+
(device as any).commands = {
|
|
1104
|
+
typedCall,
|
|
1105
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'hidden secret' }),
|
|
1106
|
+
};
|
|
827
1107
|
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockResolvedValue(undefined as any);
|
|
828
1108
|
|
|
829
1109
|
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
@@ -887,6 +1167,32 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
887
1167
|
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
888
1168
|
});
|
|
889
1169
|
|
|
1170
|
+
test('drops stale normal status when firmware reconnect supplies bootloader DeviceInfo', () => {
|
|
1171
|
+
const device = Device.fromDescriptor({
|
|
1172
|
+
id: 'firmware-reconnect-device',
|
|
1173
|
+
path: 'firmware-reconnect-path',
|
|
1174
|
+
protocolType: 'V2',
|
|
1175
|
+
} as any);
|
|
1176
|
+
(device as any).features = buildProtocolV2FeaturesPayload({
|
|
1177
|
+
deviceInfo: {
|
|
1178
|
+
protocol_version: 1,
|
|
1179
|
+
hw: { serial_no: 'PR9999999999' },
|
|
1180
|
+
fw: { application: { version: '1.0.0' } },
|
|
1181
|
+
},
|
|
1182
|
+
deviceStatus: {
|
|
1183
|
+
device_id: 'firmware-reconnect-id',
|
|
1184
|
+
init_states: true,
|
|
1185
|
+
unlocked: false,
|
|
1186
|
+
} as any,
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
(device as any).updateProtocolV2Features(protocolV2BootloaderDeviceInfo, null);
|
|
1190
|
+
|
|
1191
|
+
expect(device.features?.mode).toBe('bootloader');
|
|
1192
|
+
expect(device.features?.bootloaderMode).toBe(true);
|
|
1193
|
+
expect(device.features?.raw?.protocolV2DeviceStatus).toBeUndefined();
|
|
1194
|
+
});
|
|
1195
|
+
|
|
890
1196
|
test('returns passphrase state string for existing Pro devices', async () => {
|
|
891
1197
|
const features = {
|
|
892
1198
|
deviceId: 'pro-device-id',
|
|
@@ -914,7 +1220,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
914
1220
|
method.device = stubDevice({
|
|
915
1221
|
originalDescriptor: { ...descriptor, protocolType: 'V1' },
|
|
916
1222
|
features,
|
|
917
|
-
commands: {
|
|
1223
|
+
commands: {
|
|
1224
|
+
typedCall,
|
|
1225
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-secret' }),
|
|
1226
|
+
},
|
|
918
1227
|
updateInternalState,
|
|
919
1228
|
getCurrentDeviceId: () => 'pro-device-id',
|
|
920
1229
|
getCurrentPassphraseProtection: () => true,
|
|
@@ -957,7 +1266,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
957
1266
|
method.device = stubDevice({
|
|
958
1267
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
959
1268
|
features,
|
|
960
|
-
commands: {
|
|
1269
|
+
commands: {
|
|
1270
|
+
typedCall,
|
|
1271
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-new-secret' }),
|
|
1272
|
+
},
|
|
961
1273
|
updateInternalState,
|
|
962
1274
|
getFeatures,
|
|
963
1275
|
getCurrentDeviceType: () => 'pro2',
|
|
@@ -996,7 +1308,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
996
1308
|
method.device = stubDevice({
|
|
997
1309
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
998
1310
|
features,
|
|
999
|
-
commands: {
|
|
1311
|
+
commands: {
|
|
1312
|
+
typedCall,
|
|
1313
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-new-secret' }),
|
|
1314
|
+
},
|
|
1000
1315
|
clearInternalState,
|
|
1001
1316
|
updateInternalState,
|
|
1002
1317
|
getCurrentDeviceId: () => 'pro2-device-id',
|
|
@@ -1032,7 +1347,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1032
1347
|
}),
|
|
1033
1348
|
unlocked: true,
|
|
1034
1349
|
};
|
|
1035
|
-
(device as any).commands = {
|
|
1350
|
+
(device as any).commands = {
|
|
1351
|
+
typedCall,
|
|
1352
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-profile-secret' }),
|
|
1353
|
+
};
|
|
1036
1354
|
|
|
1037
1355
|
await getPassphraseStateWithRefreshDeviceInfo(device);
|
|
1038
1356
|
|
|
@@ -1063,7 +1381,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1063
1381
|
(device as any).features.firmwareVersion = '4.15.0';
|
|
1064
1382
|
(device as any).features.passphraseProtection = true;
|
|
1065
1383
|
(device as any).features.unlocked = true;
|
|
1066
|
-
(device as any).commands = {
|
|
1384
|
+
(device as any).commands = {
|
|
1385
|
+
typedCall,
|
|
1386
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-auto-secret' }),
|
|
1387
|
+
};
|
|
1067
1388
|
|
|
1068
1389
|
await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({
|
|
1069
1390
|
passphraseState: 'state-auto',
|
|
@@ -1076,7 +1397,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1076
1397
|
expect(device.getInternalState()).toBeUndefined();
|
|
1077
1398
|
device.passphraseState = 'state-auto';
|
|
1078
1399
|
expect(device.getInternalState()).toBe('session-auto');
|
|
1079
|
-
expect(typedCall).toHaveBeenLastCalledWith('
|
|
1400
|
+
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
1401
|
+
select: {
|
|
1402
|
+
host_passphrase: { passphrase: 'state-auto-secret' },
|
|
1403
|
+
},
|
|
1404
|
+
});
|
|
1080
1405
|
});
|
|
1081
1406
|
|
|
1082
1407
|
test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
|
|
@@ -1095,6 +1420,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1095
1420
|
} as any,
|
|
1096
1421
|
{
|
|
1097
1422
|
status: {
|
|
1423
|
+
unlocked: true,
|
|
1098
1424
|
passphrase_enabled: false,
|
|
1099
1425
|
},
|
|
1100
1426
|
}
|
|
@@ -1107,13 +1433,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1107
1433
|
getPassphraseStateWithRefreshDeviceInfo(device, { onlyMainPin: true })
|
|
1108
1434
|
).resolves.toMatchObject({
|
|
1109
1435
|
passphraseState: undefined,
|
|
1110
|
-
newSession:
|
|
1436
|
+
newSession: undefined,
|
|
1111
1437
|
});
|
|
1112
1438
|
|
|
1113
1439
|
expect(device.features?.passphraseProtection).toBe(false);
|
|
1114
1440
|
expect(device.features?.sessionId).toBeNull();
|
|
1115
1441
|
expect(device.getInternalState()).toBeUndefined();
|
|
1116
|
-
expect(typedCall).
|
|
1442
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
1117
1443
|
});
|
|
1118
1444
|
|
|
1119
1445
|
test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
|
|
@@ -1132,7 +1458,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1132
1458
|
} as any);
|
|
1133
1459
|
(device as any).features.firmwareVersion = '4.15.0';
|
|
1134
1460
|
(device as any).features.passphraseProtection = true;
|
|
1135
|
-
(device as any).
|
|
1461
|
+
(device as any).features.unlocked = true;
|
|
1462
|
+
(device as any).commands = {
|
|
1463
|
+
typedCall,
|
|
1464
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'expected-secret' }),
|
|
1465
|
+
};
|
|
1136
1466
|
|
|
1137
1467
|
await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
|
|
1138
1468
|
expect.objectContaining({
|
|
@@ -1141,7 +1471,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1141
1471
|
);
|
|
1142
1472
|
|
|
1143
1473
|
expect(device.getInternalState()).toBeUndefined();
|
|
1144
|
-
expect(typedCall).toHaveBeenNthCalledWith(1, '
|
|
1474
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionOpen', 'DeviceSession', {
|
|
1475
|
+
select: {
|
|
1476
|
+
host_passphrase: { passphrase: 'expected-secret' },
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1145
1479
|
});
|
|
1146
1480
|
|
|
1147
1481
|
test('marks fallback features as unavailable when DeviceInfo is missing', () => {
|
|
@@ -1235,17 +1569,22 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1235
1569
|
|
|
1236
1570
|
test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => {
|
|
1237
1571
|
const commands = {
|
|
1238
|
-
typedCall: jest
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1572
|
+
typedCall: jest
|
|
1573
|
+
.fn()
|
|
1574
|
+
.mockResolvedValueOnce({
|
|
1575
|
+
type: 'DeviceInfo',
|
|
1576
|
+
message: {
|
|
1577
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
1578
|
+
},
|
|
1579
|
+
})
|
|
1580
|
+
.mockResolvedValueOnce({
|
|
1581
|
+
type: 'DeviceStatus',
|
|
1582
|
+
message: {
|
|
1243
1583
|
device_id: 'PRO2-STATUS-ID',
|
|
1244
1584
|
init_states: true,
|
|
1245
1585
|
passphrase_enabled: true,
|
|
1246
1586
|
},
|
|
1247
|
-
},
|
|
1248
|
-
}),
|
|
1587
|
+
}),
|
|
1249
1588
|
};
|
|
1250
1589
|
|
|
1251
1590
|
const features = await requestProtocolV2Features({
|
|
@@ -1255,8 +1594,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1255
1594
|
|
|
1256
1595
|
expect(features.deviceId).toBe('PRO2-STATUS-ID');
|
|
1257
1596
|
expect(features.initialized).toBe(true);
|
|
1258
|
-
expect(features.passphraseProtection).
|
|
1259
|
-
expect(commands.typedCall).toHaveBeenCalledTimes(
|
|
1597
|
+
expect(features.passphraseProtection).toBeNull();
|
|
1598
|
+
expect(commands.typedCall).toHaveBeenCalledTimes(2);
|
|
1260
1599
|
expect(commands.typedCall).toHaveBeenNthCalledWith(
|
|
1261
1600
|
1,
|
|
1262
1601
|
'DeviceInfoGet',
|
|
@@ -1266,7 +1605,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1266
1605
|
hw: true,
|
|
1267
1606
|
fw: true,
|
|
1268
1607
|
coprocessor: true,
|
|
1269
|
-
status: true,
|
|
1270
1608
|
},
|
|
1271
1609
|
types: {
|
|
1272
1610
|
version: true,
|
|
@@ -1275,6 +1613,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1275
1613
|
},
|
|
1276
1614
|
{ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
|
|
1277
1615
|
);
|
|
1616
|
+
expect(commands.typedCall).toHaveBeenNthCalledWith(2, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
1278
1617
|
});
|
|
1279
1618
|
|
|
1280
1619
|
test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => {
|
|
@@ -1291,29 +1630,34 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1291
1630
|
});
|
|
1292
1631
|
|
|
1293
1632
|
test('refreshes Protocol V2 basic device info with the lightweight request', async () => {
|
|
1294
|
-
const typedCall = jest
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1633
|
+
const typedCall = jest
|
|
1634
|
+
.fn()
|
|
1635
|
+
.mockResolvedValueOnce({
|
|
1636
|
+
type: 'DeviceInfo',
|
|
1637
|
+
message: {
|
|
1638
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
1639
|
+
fw: {
|
|
1640
|
+
application: {
|
|
1641
|
+
version: '5.6.7',
|
|
1642
|
+
},
|
|
1301
1643
|
},
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1644
|
+
coprocessor: {
|
|
1645
|
+
application: {
|
|
1646
|
+
version: '8.9.10',
|
|
1647
|
+
},
|
|
1648
|
+
bt_adv_name: 'Raw Pro2 BLE',
|
|
1306
1649
|
},
|
|
1307
|
-
bt_adv_name: 'Raw Pro2 BLE',
|
|
1308
1650
|
},
|
|
1309
|
-
|
|
1651
|
+
})
|
|
1652
|
+
.mockResolvedValueOnce({
|
|
1653
|
+
type: 'DeviceStatus',
|
|
1654
|
+
message: {
|
|
1310
1655
|
device_id: 'PRO2-BASIC-ID',
|
|
1311
1656
|
unlocked_by_attach_to_pin: true,
|
|
1312
1657
|
init_states: true,
|
|
1313
1658
|
passphrase_enabled: true,
|
|
1314
1659
|
},
|
|
1315
|
-
}
|
|
1316
|
-
});
|
|
1660
|
+
});
|
|
1317
1661
|
const method = new GetDeviceInfo({
|
|
1318
1662
|
id: 1,
|
|
1319
1663
|
payload: {
|
|
@@ -1347,7 +1691,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1347
1691
|
hw: true,
|
|
1348
1692
|
fw: true,
|
|
1349
1693
|
coprocessor: true,
|
|
1350
|
-
status: true,
|
|
1351
1694
|
},
|
|
1352
1695
|
types: {
|
|
1353
1696
|
version: true,
|
|
@@ -1429,13 +1772,16 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1429
1772
|
});
|
|
1430
1773
|
|
|
1431
1774
|
test('reads full Protocol V2 device info only for verify scope', async () => {
|
|
1432
|
-
const typedCall = jest
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1775
|
+
const typedCall = jest
|
|
1776
|
+
.fn()
|
|
1777
|
+
.mockResolvedValueOnce({
|
|
1778
|
+
type: 'DeviceInfo',
|
|
1779
|
+
message: { hw: { serial_no: 'PR2SERIAL' } },
|
|
1780
|
+
})
|
|
1781
|
+
.mockResolvedValueOnce({
|
|
1782
|
+
type: 'DeviceStatus',
|
|
1783
|
+
message: { init_states: true },
|
|
1784
|
+
});
|
|
1439
1785
|
const method = new GetDeviceInfo({
|
|
1440
1786
|
id: 1,
|
|
1441
1787
|
payload: {
|
|
@@ -1465,7 +1811,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1465
1811
|
se2: true,
|
|
1466
1812
|
se3: true,
|
|
1467
1813
|
se4: true,
|
|
1468
|
-
status: true,
|
|
1469
1814
|
},
|
|
1470
1815
|
types: {
|
|
1471
1816
|
version: true,
|
|
@@ -1479,21 +1824,26 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1479
1824
|
});
|
|
1480
1825
|
|
|
1481
1826
|
test('reads Protocol V2 SE version fields for versions scope', async () => {
|
|
1482
|
-
const typedCall = jest
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1827
|
+
const typedCall = jest
|
|
1828
|
+
.fn()
|
|
1829
|
+
.mockResolvedValueOnce({
|
|
1830
|
+
type: 'DeviceInfo',
|
|
1831
|
+
message: {
|
|
1832
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
1833
|
+
se1: {
|
|
1834
|
+
application: { version: '1.0.1' },
|
|
1835
|
+
bootloader: { version: '1.0.0' },
|
|
1836
|
+
},
|
|
1837
|
+
se2: {
|
|
1838
|
+
application: { version: '2.0.1' },
|
|
1839
|
+
bootloader: { version: '2.0.0' },
|
|
1840
|
+
},
|
|
1493
1841
|
},
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1842
|
+
})
|
|
1843
|
+
.mockResolvedValueOnce({
|
|
1844
|
+
type: 'DeviceStatus',
|
|
1845
|
+
message: { init_states: true },
|
|
1846
|
+
});
|
|
1497
1847
|
const method = new GetDeviceInfo({
|
|
1498
1848
|
id: 1,
|
|
1499
1849
|
payload: {
|
|
@@ -1522,7 +1872,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1522
1872
|
se2: true,
|
|
1523
1873
|
se3: true,
|
|
1524
1874
|
se4: true,
|
|
1525
|
-
status: true,
|
|
1526
1875
|
},
|
|
1527
1876
|
types: {
|
|
1528
1877
|
version: true,
|
|
@@ -1698,7 +2047,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1698
2047
|
expect(message).not.toHaveProperty('label');
|
|
1699
2048
|
});
|
|
1700
2049
|
|
|
1701
|
-
test('
|
|
2050
|
+
test('reuses cached Protocol V2 features on later runs without polling device status', async () => {
|
|
1702
2051
|
const device = Device.fromDescriptor({
|
|
1703
2052
|
path: 'usb-path',
|
|
1704
2053
|
protocolType: 'V2',
|
|
@@ -1710,21 +2059,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1710
2059
|
message: {
|
|
1711
2060
|
hw: { serial_no: 'PR2SERIAL' },
|
|
1712
2061
|
fw: { application: { version: '1.2.3' } },
|
|
1713
|
-
status: {
|
|
1714
|
-
device_id: 'PRO2-REFRESH-ID',
|
|
1715
|
-
passphrase_enabled: true,
|
|
1716
|
-
},
|
|
1717
2062
|
},
|
|
1718
2063
|
})
|
|
1719
|
-
// 第二次 run 的轻量 status 刷新:设备端 label / init_states 等可能已变化
|
|
1720
2064
|
.mockResolvedValueOnce({
|
|
1721
|
-
type: '
|
|
2065
|
+
type: 'DeviceStatus',
|
|
1722
2066
|
message: {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
passphrase_enabled: false,
|
|
1727
|
-
},
|
|
2067
|
+
device_id: 'PRO2-REFRESH-ID',
|
|
2068
|
+
unlocked: true,
|
|
2069
|
+
passphrase_enabled: true,
|
|
1728
2070
|
},
|
|
1729
2071
|
});
|
|
1730
2072
|
|
|
@@ -1736,46 +2078,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1736
2078
|
expect(device.features).toMatchObject({
|
|
1737
2079
|
deviceId: 'PRO2-REFRESH-ID',
|
|
1738
2080
|
firmwareVersion: '1.2.3',
|
|
1739
|
-
passphraseProtection:
|
|
1740
|
-
label: null,
|
|
1741
|
-
});
|
|
1742
|
-
expect((device as any).profile).toBeUndefined();
|
|
1743
|
-
|
|
1744
|
-
expect(device.features?.
|
|
1745
|
-
expect(device.features?.
|
|
1746
|
-
|
|
1747
|
-
expect(device.features?.firmwareVersion).toBe('1.2.3');
|
|
1748
|
-
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
1749
|
-
expect(typedCall).toHaveBeenNthCalledWith(
|
|
1750
|
-
1,
|
|
1751
|
-
'DeviceInfoGet',
|
|
1752
|
-
'DeviceInfo',
|
|
1753
|
-
{
|
|
1754
|
-
targets: {
|
|
1755
|
-
hw: true,
|
|
1756
|
-
fw: true,
|
|
1757
|
-
coprocessor: true,
|
|
1758
|
-
status: true,
|
|
1759
|
-
},
|
|
1760
|
-
types: {
|
|
1761
|
-
version: true,
|
|
1762
|
-
specific: true,
|
|
1763
|
-
},
|
|
1764
|
-
},
|
|
1765
|
-
{
|
|
1766
|
-
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
1767
|
-
}
|
|
1768
|
-
);
|
|
1769
|
-
// 第二次为 status-only 轻量请求(hw/coprocessor 仅用于身份字段,避免顶层覆盖清空)
|
|
2081
|
+
passphraseProtection: true,
|
|
2082
|
+
label: null,
|
|
2083
|
+
});
|
|
2084
|
+
expect((device as any).profile).toBeUndefined();
|
|
2085
|
+
expect(device.features?.passphraseProtection).toBe(true);
|
|
2086
|
+
expect(device.features?.label).toBeNull();
|
|
2087
|
+
expect(device.features?.firmwareVersion).toBe('1.2.3');
|
|
2088
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
1770
2089
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
1771
|
-
|
|
2090
|
+
1,
|
|
1772
2091
|
'DeviceInfoGet',
|
|
1773
2092
|
'DeviceInfo',
|
|
1774
2093
|
{
|
|
1775
2094
|
targets: {
|
|
1776
2095
|
hw: true,
|
|
2096
|
+
fw: true,
|
|
1777
2097
|
coprocessor: true,
|
|
1778
|
-
status: true,
|
|
1779
2098
|
},
|
|
1780
2099
|
types: {
|
|
1781
2100
|
version: true,
|
|
@@ -1786,6 +2105,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1786
2105
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
1787
2106
|
}
|
|
1788
2107
|
);
|
|
2108
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
1789
2109
|
});
|
|
1790
2110
|
|
|
1791
2111
|
test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
|
|
@@ -1800,16 +2120,22 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1800
2120
|
message: {
|
|
1801
2121
|
hw: { serial_no: 'PR2SERIAL' },
|
|
1802
2122
|
fw: { application: { version: '1.2.3' } },
|
|
1803
|
-
status: { init_states: true },
|
|
1804
2123
|
},
|
|
1805
2124
|
})
|
|
2125
|
+
.mockResolvedValueOnce({
|
|
2126
|
+
type: 'DeviceStatus',
|
|
2127
|
+
message: { init_states: true },
|
|
2128
|
+
})
|
|
1806
2129
|
.mockResolvedValueOnce({
|
|
1807
2130
|
type: 'DeviceInfo',
|
|
1808
2131
|
message: {
|
|
1809
2132
|
hw: { serial_no: 'PR2SERIAL' },
|
|
1810
2133
|
fw: { application: { version: '1.2.4' } },
|
|
1811
|
-
status: { init_states: true, passphrase_enabled: true },
|
|
1812
2134
|
},
|
|
2135
|
+
})
|
|
2136
|
+
.mockResolvedValueOnce({
|
|
2137
|
+
type: 'DeviceStatus',
|
|
2138
|
+
message: { init_states: true, unlocked: true, passphrase_enabled: true },
|
|
1813
2139
|
});
|
|
1814
2140
|
|
|
1815
2141
|
(device as any).commands = { typedCall };
|
|
@@ -1828,9 +2154,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1828
2154
|
firmwareVersion: '1.2.4',
|
|
1829
2155
|
passphraseProtection: true,
|
|
1830
2156
|
});
|
|
1831
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2157
|
+
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
1832
2158
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
1833
|
-
|
|
2159
|
+
3,
|
|
1834
2160
|
'DeviceInfoGet',
|
|
1835
2161
|
'DeviceInfo',
|
|
1836
2162
|
{
|
|
@@ -1838,7 +2164,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1838
2164
|
hw: true,
|
|
1839
2165
|
fw: true,
|
|
1840
2166
|
coprocessor: true,
|
|
1841
|
-
status: true,
|
|
1842
2167
|
},
|
|
1843
2168
|
types: {
|
|
1844
2169
|
version: true,
|
|
@@ -1934,7 +2259,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1934
2259
|
const features = await device.unlockDevice();
|
|
1935
2260
|
|
|
1936
2261
|
expect(typedCall.mock.calls).toEqual([
|
|
1937
|
-
['DeviceSessionAskPin', 'Success'],
|
|
2262
|
+
['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
|
|
1938
2263
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1939
2264
|
]);
|
|
1940
2265
|
expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
|
|
@@ -1946,7 +2271,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1946
2271
|
unlockedAttachPin: true,
|
|
1947
2272
|
passphraseProtection: true,
|
|
1948
2273
|
});
|
|
1949
|
-
expect(features.raw?.
|
|
2274
|
+
expect(features.raw?.protocolV2DeviceStatus).toMatchObject({
|
|
1950
2275
|
unlocked: true,
|
|
1951
2276
|
unlocked_by_attach_to_pin: true,
|
|
1952
2277
|
passphrase_enabled: true,
|
|
@@ -1987,7 +2312,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1987
2312
|
await device.unlockDevice();
|
|
1988
2313
|
|
|
1989
2314
|
expect(typedCall.mock.calls).toEqual([
|
|
1990
|
-
['DeviceSessionAskPin', 'Success'],
|
|
2315
|
+
['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
|
|
1991
2316
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1992
2317
|
]);
|
|
1993
2318
|
expect((device as any).profile).toBeUndefined();
|
|
@@ -2016,9 +2341,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2016
2341
|
})
|
|
2017
2342
|
);
|
|
2018
2343
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2019
|
-
expect(typedCall.mock.calls).toEqual([
|
|
2344
|
+
expect(typedCall.mock.calls).toEqual([
|
|
2345
|
+
['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
|
|
2346
|
+
]);
|
|
2020
2347
|
expect(typedCall).not.toHaveBeenCalledWith(
|
|
2021
|
-
'
|
|
2348
|
+
'DeviceSessionOpen',
|
|
2022
2349
|
'DeviceSession',
|
|
2023
2350
|
expect.anything()
|
|
2024
2351
|
);
|
|
@@ -2506,7 +2833,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2506
2833
|
se2: true,
|
|
2507
2834
|
se3: true,
|
|
2508
2835
|
se4: true,
|
|
2509
|
-
status: true,
|
|
2510
2836
|
},
|
|
2511
2837
|
types: {
|
|
2512
2838
|
version: true,
|
|
@@ -2634,6 +2960,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2634
2960
|
});
|
|
2635
2961
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
2636
2962
|
(method as any).device.getCommands = () => ({ typedCall });
|
|
2963
|
+
(method as any).protocolV2ExpectedDeviceId = '8693920D7CA90CEC4A88353D';
|
|
2637
2964
|
|
|
2638
2965
|
await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
|
|
2639
2966
|
|
|
@@ -2641,6 +2968,47 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2641
2968
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
2642
2969
|
});
|
|
2643
2970
|
|
|
2971
|
+
test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
|
|
2972
|
+
const method = new FirmwareUpdateV4({
|
|
2973
|
+
id: 1,
|
|
2974
|
+
payload: {
|
|
2975
|
+
method: 'firmwareUpdateV4',
|
|
2976
|
+
},
|
|
2977
|
+
});
|
|
2978
|
+
const commands = { disposed: true, mainId: '' };
|
|
2979
|
+
const initialize = jest.fn().mockResolvedValue(undefined);
|
|
2980
|
+
const device = stubDevice({
|
|
2981
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
2982
|
+
features: { bootloaderMode: false, capabilities: [] },
|
|
2983
|
+
deviceConnector: {
|
|
2984
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
2985
|
+
descriptors: [{ id: 'usb-id', path: 'usb-path', protocolType: 'V2' }],
|
|
2986
|
+
}),
|
|
2987
|
+
},
|
|
2988
|
+
updateFromCache: jest.fn(),
|
|
2989
|
+
acquire: jest.fn().mockResolvedValue(undefined),
|
|
2990
|
+
initialize,
|
|
2991
|
+
commands,
|
|
2992
|
+
getCommands: () => commands,
|
|
2993
|
+
mainId: 'usb-path',
|
|
2994
|
+
});
|
|
2995
|
+
const cachedDevice = { getConnectId: () => 'usb-path' };
|
|
2996
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
2997
|
+
devices: {},
|
|
2998
|
+
deviceList: [cachedDevice],
|
|
2999
|
+
} as any);
|
|
3000
|
+
(method as any).device = device;
|
|
3001
|
+
|
|
3002
|
+
try {
|
|
3003
|
+
await (method as any).reconnectProtocolV2Device();
|
|
3004
|
+
} finally {
|
|
3005
|
+
getDevices.mockRestore();
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
expect(device.acquire).toHaveBeenCalledWith('V2', { throwOnRunPromiseError: true });
|
|
3009
|
+
expect(initialize).not.toHaveBeenCalled();
|
|
3010
|
+
});
|
|
3011
|
+
|
|
2644
3012
|
test('reboots Protocol V2 firmware flow back to normal after install status is finished', async () => {
|
|
2645
3013
|
const method = new FirmwareUpdateV4({
|
|
2646
3014
|
id: 1,
|
|
@@ -2754,7 +3122,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2754
3122
|
expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
|
|
2755
3123
|
});
|
|
2756
3124
|
|
|
2757
|
-
test('
|
|
3125
|
+
test('retries final device readiness through temporary Protocol V2 probe failures', async () => {
|
|
2758
3126
|
const method = new FirmwareUpdateV4({
|
|
2759
3127
|
id: 1,
|
|
2760
3128
|
payload: {
|
|
@@ -2762,7 +3130,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2762
3130
|
},
|
|
2763
3131
|
});
|
|
2764
3132
|
const typedCall = jest.fn().mockImplementation((name: string) => {
|
|
2765
|
-
if (name === '
|
|
3133
|
+
if (name === 'DeviceStatusGet') {
|
|
2766
3134
|
const callCount = typedCall.mock.calls.filter(call => call[0] === name).length;
|
|
2767
3135
|
if (callCount === 1) {
|
|
2768
3136
|
return Promise.reject(
|
|
@@ -2772,24 +3140,28 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2772
3140
|
);
|
|
2773
3141
|
}
|
|
2774
3142
|
return Promise.resolve({
|
|
2775
|
-
type: '
|
|
2776
|
-
message: {
|
|
2777
|
-
records: [{ target_id: 6, status: 2 }],
|
|
2778
|
-
},
|
|
3143
|
+
type: 'DeviceStatus',
|
|
3144
|
+
message: { device_id: 'PRO2-DEVICE-ID', init_states: true },
|
|
2779
3145
|
});
|
|
2780
3146
|
}
|
|
2781
3147
|
if (name === 'DeviceInfoGet') {
|
|
2782
|
-
return Promise.
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3148
|
+
return Promise.resolve({
|
|
3149
|
+
type: 'DeviceInfo',
|
|
3150
|
+
message: { hw: { serial_no: 'PR9999999999' } },
|
|
3151
|
+
});
|
|
2786
3152
|
}
|
|
2787
3153
|
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
2788
3154
|
});
|
|
2789
3155
|
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
3156
|
+
const updateProtocolV2Features = jest.fn(() => ({
|
|
3157
|
+
deviceId: 'PRO2-DEVICE-ID',
|
|
3158
|
+
bootloaderMode: false,
|
|
3159
|
+
mode: 'normal',
|
|
3160
|
+
}));
|
|
2790
3161
|
|
|
2791
3162
|
(method as any).device = stubDevice({
|
|
2792
3163
|
getCommands: () => ({ typedCall }),
|
|
3164
|
+
updateProtocolV2Features,
|
|
2793
3165
|
});
|
|
2794
3166
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
2795
3167
|
method.postProgressMessage = jest.fn();
|
|
@@ -2798,16 +3170,67 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2798
3170
|
{ target_id: 6, path: 'vol1:ble-firmware.bin' },
|
|
2799
3171
|
]);
|
|
2800
3172
|
|
|
2801
|
-
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(
|
|
3173
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
|
|
2802
3174
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
2803
|
-
'
|
|
3175
|
+
'DeviceStatusGet',
|
|
3176
|
+
'DeviceStatusGet',
|
|
3177
|
+
'DeviceInfoGet',
|
|
3178
|
+
]);
|
|
3179
|
+
});
|
|
3180
|
+
|
|
3181
|
+
test('retries final device readiness after the USB handle is no longer acquired', async () => {
|
|
3182
|
+
const method = new FirmwareUpdateV4({
|
|
3183
|
+
id: 1,
|
|
3184
|
+
payload: {
|
|
3185
|
+
method: 'firmwareUpdateV4',
|
|
3186
|
+
},
|
|
3187
|
+
});
|
|
3188
|
+
const typedCall = jest.fn().mockImplementation((name: string) => {
|
|
3189
|
+
if (name === 'DeviceStatusGet') {
|
|
3190
|
+
const callCount = typedCall.mock.calls.filter(call => call[0] === name).length;
|
|
3191
|
+
if (callCount === 1) {
|
|
3192
|
+
return Promise.reject(new Error('Device not acquired: PR9999999999'));
|
|
3193
|
+
}
|
|
3194
|
+
return Promise.resolve({
|
|
3195
|
+
type: 'DeviceStatus',
|
|
3196
|
+
message: { device_id: 'PRO2-DEVICE-ID', init_states: true },
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
if (name === 'DeviceInfoGet') {
|
|
3200
|
+
return Promise.resolve({
|
|
3201
|
+
type: 'DeviceInfo',
|
|
3202
|
+
message: { hw: { serial_no: 'PR9999999999' } },
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
3206
|
+
});
|
|
3207
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
3208
|
+
const updateProtocolV2Features = jest.fn(() => ({
|
|
3209
|
+
deviceId: 'PRO2-DEVICE-ID',
|
|
3210
|
+
bootloaderMode: false,
|
|
3211
|
+
mode: 'normal',
|
|
3212
|
+
}));
|
|
3213
|
+
|
|
3214
|
+
(method as any).device = stubDevice({
|
|
3215
|
+
getCommands: () => ({ typedCall }),
|
|
3216
|
+
updateProtocolV2Features,
|
|
3217
|
+
});
|
|
3218
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
3219
|
+
method.postProgressMessage = jest.fn();
|
|
3220
|
+
|
|
3221
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
3222
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
3223
|
+
]);
|
|
3224
|
+
|
|
3225
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
|
|
3226
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3227
|
+
'DeviceStatusGet',
|
|
3228
|
+
'DeviceStatusGet',
|
|
2804
3229
|
'DeviceInfoGet',
|
|
2805
|
-
'Ping',
|
|
2806
|
-
'DeviceFirmwareUpdateStatusGet',
|
|
2807
3230
|
]);
|
|
2808
3231
|
});
|
|
2809
3232
|
|
|
2810
|
-
test('treats
|
|
3233
|
+
test('treats DeviceStatusGet and DeviceInfoGet readiness as firmware update complete', async () => {
|
|
2811
3234
|
const method = new FirmwareUpdateV4({
|
|
2812
3235
|
id: 1,
|
|
2813
3236
|
payload: {
|
|
@@ -2815,8 +3238,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2815
3238
|
},
|
|
2816
3239
|
});
|
|
2817
3240
|
const typedCall = jest.fn().mockImplementation((name: string) => {
|
|
2818
|
-
if (name === '
|
|
2819
|
-
return Promise.
|
|
3241
|
+
if (name === 'DeviceStatusGet') {
|
|
3242
|
+
return Promise.resolve({
|
|
3243
|
+
type: 'DeviceStatus',
|
|
3244
|
+
message: {
|
|
3245
|
+
device_id: 'PRO2-DEVICE-ID',
|
|
3246
|
+
init_states: true,
|
|
3247
|
+
},
|
|
3248
|
+
});
|
|
2820
3249
|
}
|
|
2821
3250
|
if (name === 'DeviceInfoGet') {
|
|
2822
3251
|
return Promise.resolve({
|
|
@@ -2827,19 +3256,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2827
3256
|
bootloader: { version: '9.9.9' },
|
|
2828
3257
|
application: { version: '9.9.9' },
|
|
2829
3258
|
},
|
|
2830
|
-
status: {
|
|
2831
|
-
device_id: 'PRO2-DEVICE-ID',
|
|
2832
|
-
init_states: true,
|
|
2833
|
-
},
|
|
2834
3259
|
},
|
|
2835
3260
|
});
|
|
2836
3261
|
}
|
|
2837
3262
|
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
2838
3263
|
});
|
|
2839
3264
|
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
2840
|
-
const updateProtocolV2Features = jest.fn((
|
|
2841
|
-
|
|
2842
|
-
|
|
3265
|
+
const updateProtocolV2Features = jest.fn(() => ({
|
|
3266
|
+
deviceId: 'PRO2-DEVICE-ID',
|
|
3267
|
+
bootloaderMode: false,
|
|
3268
|
+
mode: 'normal',
|
|
3269
|
+
}));
|
|
2843
3270
|
|
|
2844
3271
|
(method as any).device = stubDevice({
|
|
2845
3272
|
getCommands: () => ({ typedCall }),
|
|
@@ -2849,18 +3276,35 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2849
3276
|
method.postProgressMessage = jest.fn();
|
|
2850
3277
|
|
|
2851
3278
|
await expect(
|
|
2852
|
-
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3279
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
3280
|
+
[
|
|
3281
|
+
{ target_id: 3, path: 'vol1:bootloader.bin' },
|
|
3282
|
+
{ target_id: 4, path: 'vol1:application_p1.bin' },
|
|
3283
|
+
],
|
|
3284
|
+
{
|
|
3285
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
3286
|
+
message: {
|
|
3287
|
+
records: [
|
|
3288
|
+
{ target_id: 3, status: 2 },
|
|
3289
|
+
{ target_id: 4, status: 2 },
|
|
3290
|
+
],
|
|
3291
|
+
},
|
|
3292
|
+
}
|
|
3293
|
+
)
|
|
2856
3294
|
).resolves.toBeUndefined();
|
|
2857
3295
|
|
|
2858
3296
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
2859
3297
|
expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
|
|
2860
|
-
expect(
|
|
3298
|
+
expect(updateProtocolV2Features).toHaveBeenCalledWith(
|
|
3299
|
+
expect.objectContaining({ hw: { serial_no: 'PR9999999999' } }),
|
|
3300
|
+
expect.objectContaining({ device_id: 'PRO2-DEVICE-ID' })
|
|
3301
|
+
);
|
|
3302
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceStatusGet', 'DeviceInfoGet']);
|
|
3303
|
+
expect(typedCall).not.toHaveBeenCalledWith(
|
|
2861
3304
|
'DeviceFirmwareUpdateStatusGet',
|
|
2862
|
-
|
|
2863
|
-
|
|
3305
|
+
expect.anything(),
|
|
3306
|
+
expect.anything()
|
|
3307
|
+
);
|
|
2864
3308
|
});
|
|
2865
3309
|
|
|
2866
3310
|
test('uses SDK decoded enum names for Protocol V2 install polling', () => {
|
|
@@ -3995,6 +4439,64 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
3995
4439
|
assertProtocolV2ReconnectIdentity('expected-device', 'expected-device')
|
|
3996
4440
|
).not.toThrow();
|
|
3997
4441
|
});
|
|
4442
|
+
|
|
4443
|
+
test('allows bootloader reconnect without wallet device id after USB identity is selected', () => {
|
|
4444
|
+
expect(() =>
|
|
4445
|
+
assertProtocolV2ReconnectIdentity('expected-device', undefined, {
|
|
4446
|
+
allowMissingActual: true,
|
|
4447
|
+
})
|
|
4448
|
+
).not.toThrow();
|
|
4449
|
+
expect(() =>
|
|
4450
|
+
assertProtocolV2ReconnectIdentity('expected-device', 'other-device', {
|
|
4451
|
+
allowMissingActual: true,
|
|
4452
|
+
})
|
|
4453
|
+
).toThrow('identity mismatch');
|
|
4454
|
+
});
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
describe('Protocol V2 explicit USB device selection', () => {
|
|
4458
|
+
beforeEach(() => {
|
|
4459
|
+
DevicePool.resetState();
|
|
4460
|
+
jest.restoreAllMocks();
|
|
4461
|
+
});
|
|
4462
|
+
|
|
4463
|
+
test('only initializes the descriptor matching an explicit connectId', async () => {
|
|
4464
|
+
const zeroDescriptor = {
|
|
4465
|
+
path: '000000000000000000000000',
|
|
4466
|
+
id: '000000000000000000000000',
|
|
4467
|
+
} as any;
|
|
4468
|
+
const pro2Descriptor = {
|
|
4469
|
+
path: 'PR9999999999',
|
|
4470
|
+
id: 'PR9999999999',
|
|
4471
|
+
} as any;
|
|
4472
|
+
const pro2Device = {
|
|
4473
|
+
getConnectId: () => 'PR9999999999',
|
|
4474
|
+
originalDescriptor: pro2Descriptor,
|
|
4475
|
+
updateDescriptor: jest.fn(),
|
|
4476
|
+
} as any;
|
|
4477
|
+
const zeroDevice = {
|
|
4478
|
+
getConnectId: () => '000000000000000000000000',
|
|
4479
|
+
originalDescriptor: zeroDescriptor,
|
|
4480
|
+
updateDescriptor: jest.fn(),
|
|
4481
|
+
} as any;
|
|
4482
|
+
|
|
4483
|
+
const createDevice = jest
|
|
4484
|
+
.spyOn(DevicePool as any, '_createDevice')
|
|
4485
|
+
.mockImplementation((descriptor: { path: string }) =>
|
|
4486
|
+
Promise.resolve(descriptor.path === pro2Descriptor.path ? pro2Device : zeroDevice)
|
|
4487
|
+
);
|
|
4488
|
+
const checkDevicePool = jest
|
|
4489
|
+
.spyOn(DevicePool as any, '_checkDevicePool')
|
|
4490
|
+
.mockResolvedValue(undefined);
|
|
4491
|
+
|
|
4492
|
+
const result = await DevicePool.getDevices([zeroDescriptor, pro2Descriptor], 'PR9999999999');
|
|
4493
|
+
|
|
4494
|
+
expect(createDevice).toHaveBeenCalledTimes(1);
|
|
4495
|
+
expect(createDevice).toHaveBeenCalledWith(pro2Descriptor, undefined);
|
|
4496
|
+
expect(checkDevicePool).toHaveBeenCalledWith(undefined, 'PR9999999999');
|
|
4497
|
+
expect(result.deviceList).toEqual([pro2Device]);
|
|
4498
|
+
expect(result.devices).toEqual({ PR9999999999: pro2Device });
|
|
4499
|
+
});
|
|
3998
4500
|
});
|
|
3999
4501
|
|
|
4000
4502
|
describe('Protocol V2 reboot methods', () => {
|
|
@@ -4024,10 +4526,43 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
4024
4526
|
errorCode: HardwareErrorCode.DeviceLocked,
|
|
4025
4527
|
});
|
|
4026
4528
|
|
|
4529
|
+
test('uses locked retry by default for SDK business methods', () => {
|
|
4530
|
+
class TestBusinessMethod extends BaseMethod {
|
|
4531
|
+
init() {}
|
|
4532
|
+
|
|
4533
|
+
run() {
|
|
4534
|
+
return Promise.resolve({});
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
const method = new TestBusinessMethod({
|
|
4539
|
+
id: 1,
|
|
4540
|
+
payload: { method: 'testBusinessMethod' },
|
|
4541
|
+
} as any);
|
|
4542
|
+
|
|
4543
|
+
expect(method.unlockPolicy).toBe('retry-on-locked');
|
|
4544
|
+
});
|
|
4545
|
+
|
|
4546
|
+
test.each([
|
|
4547
|
+
['deviceLock', DeviceLock],
|
|
4548
|
+
['deviceUnlock', DeviceUnlock],
|
|
4549
|
+
['deviceCancel', DeviceCancel],
|
|
4550
|
+
])('does not auto-retry the %s control method', (_name, MethodClass) => {
|
|
4551
|
+
const method = new MethodClass({
|
|
4552
|
+
id: 1,
|
|
4553
|
+
payload: { method: _name },
|
|
4554
|
+
} as any);
|
|
4555
|
+
method.init();
|
|
4556
|
+
|
|
4557
|
+
expect(method.unlockPolicy).toBe('none');
|
|
4558
|
+
});
|
|
4559
|
+
|
|
4027
4560
|
test('unlocks and retries an opted-in Protocol V2 method once', async () => {
|
|
4028
4561
|
const calls: string[] = [];
|
|
4029
4562
|
const method = {
|
|
4563
|
+
name: 'testBusinessMethod',
|
|
4030
4564
|
unlockPolicy: 'retry-on-locked',
|
|
4565
|
+
protocolV2UiInteraction: { reason: 'settings-page' },
|
|
4031
4566
|
run: jest
|
|
4032
4567
|
.fn()
|
|
4033
4568
|
.mockImplementationOnce(() => {
|
|
@@ -4046,11 +4581,87 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
4046
4581
|
return Promise.resolve();
|
|
4047
4582
|
}),
|
|
4048
4583
|
};
|
|
4584
|
+
const uiCoordinator = {
|
|
4585
|
+
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
4586
|
+
enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
|
|
4587
|
+
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
4588
|
+
};
|
|
4589
|
+
|
|
4590
|
+
await expect(
|
|
4591
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
4592
|
+
).resolves.toEqual({ message: 'ok' });
|
|
4593
|
+
expect(calls).toEqual([
|
|
4594
|
+
'method-prompt',
|
|
4595
|
+
'run-1',
|
|
4596
|
+
'unlock-prompt',
|
|
4597
|
+
'unlock',
|
|
4598
|
+
'method-prompt',
|
|
4599
|
+
'run-2',
|
|
4600
|
+
]);
|
|
4601
|
+
});
|
|
4602
|
+
|
|
4603
|
+
test('unlocks before showing the method interaction when cached status is locked', async () => {
|
|
4604
|
+
const calls: string[] = [];
|
|
4605
|
+
const method = {
|
|
4606
|
+
name: 'deviceSettingsPageShow',
|
|
4607
|
+
unlockPolicy: 'retry-on-locked',
|
|
4608
|
+
protocolV2UiInteraction: { reason: 'settings-page' },
|
|
4609
|
+
run: jest.fn(() => {
|
|
4610
|
+
calls.push('run');
|
|
4611
|
+
return Promise.resolve({ message: 'ok' });
|
|
4612
|
+
}),
|
|
4613
|
+
};
|
|
4614
|
+
const device = {
|
|
4615
|
+
features: { unlocked: false },
|
|
4616
|
+
isProtocolV2: () => true,
|
|
4617
|
+
unlockDevice: jest.fn(() => {
|
|
4618
|
+
calls.push('unlock');
|
|
4619
|
+
return Promise.resolve();
|
|
4620
|
+
}),
|
|
4621
|
+
};
|
|
4622
|
+
const uiCoordinator = {
|
|
4623
|
+
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
4624
|
+
enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
|
|
4625
|
+
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
4626
|
+
};
|
|
4627
|
+
|
|
4628
|
+
await expect(
|
|
4629
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
4630
|
+
).resolves.toEqual({ message: 'ok' });
|
|
4631
|
+
expect(calls).toEqual(['unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
4632
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
4633
|
+
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
4634
|
+
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
4635
|
+
});
|
|
4636
|
+
|
|
4637
|
+
test('infers a signing interaction before running a Protocol V2 business method', async () => {
|
|
4638
|
+
const method = {
|
|
4639
|
+
name: 'evmSignMessage',
|
|
4640
|
+
params: { message: 'not-exposed-in-event' },
|
|
4641
|
+
payload: {},
|
|
4642
|
+
unlockPolicy: 'retry-on-locked',
|
|
4643
|
+
run: jest.fn().mockResolvedValue({ signature: 'test' }),
|
|
4644
|
+
};
|
|
4645
|
+
const device = {
|
|
4646
|
+
isProtocolV2: () => true,
|
|
4647
|
+
unlockDevice: jest.fn(),
|
|
4648
|
+
};
|
|
4649
|
+
const uiCoordinator = {
|
|
4650
|
+
enterMethodInteraction: jest.fn(),
|
|
4651
|
+
enterUnlockInteraction: jest.fn(),
|
|
4652
|
+
resumeMethodInteraction: jest.fn(),
|
|
4653
|
+
};
|
|
4654
|
+
|
|
4655
|
+
await runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any);
|
|
4049
4656
|
|
|
4050
|
-
|
|
4051
|
-
|
|
4657
|
+
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledWith({
|
|
4658
|
+
request: 'button',
|
|
4659
|
+
source: 'method-lifecycle',
|
|
4660
|
+
reason: 'signing-confirmation',
|
|
4661
|
+
completion: 'operation-completed',
|
|
4662
|
+
deviceOnly: true,
|
|
4663
|
+
operation: 'evmSignMessage',
|
|
4052
4664
|
});
|
|
4053
|
-
expect(calls).toEqual(['run-1', 'unlock', 'run-2']);
|
|
4054
4665
|
});
|
|
4055
4666
|
|
|
4056
4667
|
test('does not unlock a Protocol V1 device or a method without the policy', async () => {
|
|
@@ -4074,6 +4685,35 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
4074
4685
|
}
|
|
4075
4686
|
});
|
|
4076
4687
|
|
|
4688
|
+
test('keeps auto unlock but suppresses all synthesized UI for eventless methods', async () => {
|
|
4689
|
+
const method = {
|
|
4690
|
+
name: 'uploadPortfolio',
|
|
4691
|
+
unlockPolicy: 'retry-on-locked',
|
|
4692
|
+
protocolV2UiMode: 'none',
|
|
4693
|
+
run: jest
|
|
4694
|
+
.fn()
|
|
4695
|
+
.mockRejectedValueOnce(deviceLockedError())
|
|
4696
|
+
.mockResolvedValueOnce({ message: 'ok' }),
|
|
4697
|
+
};
|
|
4698
|
+
const device = {
|
|
4699
|
+
isProtocolV2: () => true,
|
|
4700
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
4701
|
+
};
|
|
4702
|
+
const uiCoordinator = {
|
|
4703
|
+
enterMethodInteraction: jest.fn(),
|
|
4704
|
+
enterUnlockInteraction: jest.fn(),
|
|
4705
|
+
resumeMethodInteraction: jest.fn(),
|
|
4706
|
+
};
|
|
4707
|
+
|
|
4708
|
+
await expect(
|
|
4709
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
4710
|
+
).resolves.toEqual({ message: 'ok' });
|
|
4711
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
4712
|
+
expect(uiCoordinator.enterMethodInteraction).not.toHaveBeenCalled();
|
|
4713
|
+
expect(uiCoordinator.enterUnlockInteraction).not.toHaveBeenCalled();
|
|
4714
|
+
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
4715
|
+
});
|
|
4716
|
+
|
|
4077
4717
|
test('does not retry when unlock fails or when the retry is still locked', async () => {
|
|
4078
4718
|
const initialError = deviceLockedError();
|
|
4079
4719
|
const unlockError = new Error('PIN cancelled');
|
|
@@ -4085,11 +4725,22 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
4085
4725
|
isProtocolV2: () => true,
|
|
4086
4726
|
unlockDevice: jest.fn().mockRejectedValue(unlockError),
|
|
4087
4727
|
};
|
|
4728
|
+
const unlockFailCoordinator = {
|
|
4729
|
+
enterMethodInteraction: jest.fn(),
|
|
4730
|
+
enterUnlockInteraction: jest.fn(),
|
|
4731
|
+
resumeMethodInteraction: jest.fn(),
|
|
4732
|
+
};
|
|
4088
4733
|
|
|
4089
4734
|
await expect(
|
|
4090
|
-
runMethodWithUnlockRetry(
|
|
4735
|
+
runMethodWithUnlockRetry(
|
|
4736
|
+
unlockFailMethod as any,
|
|
4737
|
+
unlockFailDevice as any,
|
|
4738
|
+
unlockFailCoordinator as any
|
|
4739
|
+
)
|
|
4091
4740
|
).rejects.toBe(unlockError);
|
|
4092
4741
|
expect(unlockFailMethod.run).toHaveBeenCalledTimes(1);
|
|
4742
|
+
expect(unlockFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
4743
|
+
expect(unlockFailCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
4093
4744
|
|
|
4094
4745
|
const retryError = deviceLockedError();
|
|
4095
4746
|
const retryFailMethod = {
|
|
@@ -4100,16 +4751,121 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
4100
4751
|
isProtocolV2: () => true,
|
|
4101
4752
|
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
4102
4753
|
};
|
|
4754
|
+
const retryFailCoordinator = {
|
|
4755
|
+
enterMethodInteraction: jest.fn(),
|
|
4756
|
+
enterUnlockInteraction: jest.fn(),
|
|
4757
|
+
resumeMethodInteraction: jest.fn(),
|
|
4758
|
+
};
|
|
4103
4759
|
|
|
4104
4760
|
await expect(
|
|
4105
|
-
runMethodWithUnlockRetry(
|
|
4761
|
+
runMethodWithUnlockRetry(
|
|
4762
|
+
retryFailMethod as any,
|
|
4763
|
+
retryFailDevice as any,
|
|
4764
|
+
retryFailCoordinator as any
|
|
4765
|
+
)
|
|
4106
4766
|
).rejects.toBe(retryError);
|
|
4107
4767
|
expect(retryFailMethod.run).toHaveBeenCalledTimes(2);
|
|
4108
4768
|
expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1);
|
|
4769
|
+
expect(retryFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
4770
|
+
expect(retryFailCoordinator.resumeMethodInteraction).toHaveBeenCalledTimes(1);
|
|
4109
4771
|
});
|
|
4110
4772
|
});
|
|
4111
4773
|
|
|
4112
4774
|
describe('Protocol V2 current low-level methods', () => {
|
|
4775
|
+
test('routes device wipe to the Pro2 reset page until the operation completes', async () => {
|
|
4776
|
+
const v1TypedCall = jest.fn().mockResolvedValue({ message: { message: 'wiped' } });
|
|
4777
|
+
const v1Method = new DeviceWipe({ id: 1, payload: { method: 'deviceWipe' } });
|
|
4778
|
+
v1Method.init();
|
|
4779
|
+
(v1Method as any).device = stubDevice({
|
|
4780
|
+
isProtocolV2: () => false,
|
|
4781
|
+
commands: { typedCall: v1TypedCall },
|
|
4782
|
+
});
|
|
4783
|
+
|
|
4784
|
+
await v1Method.run();
|
|
4785
|
+
expect(v1TypedCall).toHaveBeenCalledWith('WipeDevice', 'Success');
|
|
4786
|
+
|
|
4787
|
+
const v2TypedCall = jest.fn().mockResolvedValue({ message: { message: 'accepted' } });
|
|
4788
|
+
const v2Method = new DeviceWipe({ id: 2, payload: { method: 'deviceWipe' } });
|
|
4789
|
+
v2Method.init();
|
|
4790
|
+
(v2Method as any).device = stubDevice({
|
|
4791
|
+
isProtocolV2: () => true,
|
|
4792
|
+
commands: { typedCall: v2TypedCall },
|
|
4793
|
+
});
|
|
4794
|
+
|
|
4795
|
+
await expect(v2Method.run()).resolves.toEqual({ message: 'accepted' });
|
|
4796
|
+
expect(v2TypedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
|
|
4797
|
+
page: DeviceSettingsPage.DeviceReset,
|
|
4798
|
+
});
|
|
4799
|
+
expect(v2Method.protocolV2UiInteraction).toEqual({
|
|
4800
|
+
request: 'button',
|
|
4801
|
+
source: 'method-lifecycle',
|
|
4802
|
+
reason: 'device-management',
|
|
4803
|
+
completion: 'operation-completed',
|
|
4804
|
+
deviceOnly: true,
|
|
4805
|
+
page: DeviceSettingsPage.DeviceReset,
|
|
4806
|
+
operation: 'wipe-device',
|
|
4807
|
+
});
|
|
4808
|
+
});
|
|
4809
|
+
|
|
4810
|
+
test('routes Change PIN by protocol and waits for the Pro2 operation result', async () => {
|
|
4811
|
+
const v1TypedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
|
|
4812
|
+
const v1Method = new DeviceChangePin({
|
|
4813
|
+
id: 1,
|
|
4814
|
+
payload: { method: 'deviceChangePin', remove: false },
|
|
4815
|
+
});
|
|
4816
|
+
v1Method.init();
|
|
4817
|
+
(v1Method as any).device = stubDevice({
|
|
4818
|
+
isProtocolV2: () => false,
|
|
4819
|
+
commands: { typedCall: v1TypedCall },
|
|
4820
|
+
});
|
|
4821
|
+
|
|
4822
|
+
await v1Method.run();
|
|
4823
|
+
|
|
4824
|
+
expect(v1TypedCall).toHaveBeenCalledWith('ChangePin', 'Success', { remove: false });
|
|
4825
|
+
|
|
4826
|
+
const v2TypedCall = jest.fn().mockResolvedValue({ message: { message: 'accepted' } });
|
|
4827
|
+
const v2Method = new DeviceChangePin({
|
|
4828
|
+
id: 2,
|
|
4829
|
+
payload: { method: 'deviceChangePin', remove: false },
|
|
4830
|
+
});
|
|
4831
|
+
v2Method.init();
|
|
4832
|
+
(v2Method as any).device = stubDevice({
|
|
4833
|
+
isProtocolV2: () => true,
|
|
4834
|
+
commands: { typedCall: v2TypedCall },
|
|
4835
|
+
});
|
|
4836
|
+
|
|
4837
|
+
await expect(v2Method.run()).resolves.toEqual({ message: 'accepted' });
|
|
4838
|
+
expect(v2TypedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
|
|
4839
|
+
page: DeviceSettingsPage.DevicePinChange,
|
|
4840
|
+
});
|
|
4841
|
+
expect(v2Method.protocolV2UiInteraction).toEqual({
|
|
4842
|
+
request: 'button',
|
|
4843
|
+
source: 'method-lifecycle',
|
|
4844
|
+
reason: 'change-pin',
|
|
4845
|
+
completion: 'operation-completed',
|
|
4846
|
+
deviceOnly: true,
|
|
4847
|
+
page: DeviceSettingsPage.DevicePinChange,
|
|
4848
|
+
});
|
|
4849
|
+
});
|
|
4850
|
+
|
|
4851
|
+
test('rejects removing a PIN through the Pro2 page-only Change PIN flow', async () => {
|
|
4852
|
+
const typedCall = jest.fn();
|
|
4853
|
+
const method = new DeviceChangePin({
|
|
4854
|
+
id: 1,
|
|
4855
|
+
payload: { method: 'deviceChangePin', remove: true },
|
|
4856
|
+
});
|
|
4857
|
+
method.init();
|
|
4858
|
+
(method as any).device = stubDevice({
|
|
4859
|
+
isProtocolV2: () => true,
|
|
4860
|
+
commands: { typedCall },
|
|
4861
|
+
});
|
|
4862
|
+
|
|
4863
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
4864
|
+
errorCode: HardwareErrorCode.CallMethodInvalidParameter,
|
|
4865
|
+
});
|
|
4866
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
4867
|
+
});
|
|
4868
|
+
|
|
4113
4869
|
test('sends ProtocolInfoRequest from protocolInfoRequest', async () => {
|
|
4114
4870
|
const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } });
|
|
4115
4871
|
const method = new ProtocolInfoRequest({
|
|
@@ -4200,6 +4956,44 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
4200
4956
|
brightness: 80,
|
|
4201
4957
|
},
|
|
4202
4958
|
});
|
|
4959
|
+
expect(method.protocolV2UiInteraction).toEqual({
|
|
4960
|
+
request: 'button',
|
|
4961
|
+
source: 'method-lifecycle',
|
|
4962
|
+
reason: 'device-management',
|
|
4963
|
+
completion: 'operation-completed',
|
|
4964
|
+
deviceOnly: true,
|
|
4965
|
+
operation: 'change-label',
|
|
4966
|
+
});
|
|
4967
|
+
});
|
|
4968
|
+
|
|
4969
|
+
test('keeps silent Protocol V2 device settings eventless', () => {
|
|
4970
|
+
const method = new DeviceSettingsSet({
|
|
4971
|
+
id: 1,
|
|
4972
|
+
payload: {
|
|
4973
|
+
method: 'deviceSettingsSet',
|
|
4974
|
+
settings: { brightness: 80 },
|
|
4975
|
+
},
|
|
4976
|
+
});
|
|
4977
|
+
method.init();
|
|
4978
|
+
|
|
4979
|
+
expect(method.protocolV2UiInteraction).toBeUndefined();
|
|
4980
|
+
});
|
|
4981
|
+
|
|
4982
|
+
test('marks explicit Protocol V2 unlock as an on-device PIN interaction', () => {
|
|
4983
|
+
const method = new DeviceUnlock({
|
|
4984
|
+
id: 1,
|
|
4985
|
+
payload: { method: 'deviceUnlock' },
|
|
4986
|
+
});
|
|
4987
|
+
method.init();
|
|
4988
|
+
|
|
4989
|
+
expect(method.protocolV2UiInteraction).toEqual({
|
|
4990
|
+
request: 'pin',
|
|
4991
|
+
source: 'method-lifecycle',
|
|
4992
|
+
reason: 'device-unlock',
|
|
4993
|
+
completion: 'operation-completed',
|
|
4994
|
+
deviceOnly: true,
|
|
4995
|
+
operation: 'unlock-device',
|
|
4996
|
+
});
|
|
4203
4997
|
});
|
|
4204
4998
|
|
|
4205
4999
|
test('opens non-passphrase Protocol V2 settings pages', async () => {
|
|
@@ -4221,6 +5015,14 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
4221
5015
|
page: 3,
|
|
4222
5016
|
field_name: 'airgap_mode',
|
|
4223
5017
|
});
|
|
5018
|
+
expect(method.protocolV2UiInteraction).toEqual({
|
|
5019
|
+
request: 'button',
|
|
5020
|
+
source: 'method-lifecycle',
|
|
5021
|
+
reason: 'settings-page',
|
|
5022
|
+
completion: 'operation-completed',
|
|
5023
|
+
deviceOnly: true,
|
|
5024
|
+
page: DeviceSettingsPage.DeviceAirgap,
|
|
5025
|
+
});
|
|
4224
5026
|
});
|
|
4225
5027
|
|
|
4226
5028
|
test('opens the Protocol V2 passphrase settings page', async () => {
|
|
@@ -4364,7 +5166,7 @@ describe('Protocol V2 raw device info method', () => {
|
|
|
4364
5166
|
'DeviceInfoGet',
|
|
4365
5167
|
'DeviceInfo',
|
|
4366
5168
|
{
|
|
4367
|
-
targets: { hw: true, fw: true, coprocessor: true
|
|
5169
|
+
targets: { hw: true, fw: true, coprocessor: true },
|
|
4368
5170
|
types: { version: true, specific: true },
|
|
4369
5171
|
},
|
|
4370
5172
|
{ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
|