@onekeyfe/hd-core 1.2.0-alpha.67 → 1.2.0-alpha.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +313 -0
- package/__tests__/DeviceCommands.test.ts +59 -13
- package/__tests__/DeviceEventRegistration.test.ts +6 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +198 -3
- package/__tests__/core-initialization.test.ts +23 -0
- package/__tests__/device-connector-protocol.test.ts +81 -0
- package/__tests__/device-lifecycle-events.test.ts +105 -3
- package/__tests__/device-pool-state.test.ts +22 -0
- package/__tests__/device-settings.test.ts +39 -18
- package/__tests__/device-state-contract.test.ts +1 -0
- package/__tests__/device-state-mapper.test.ts +13 -1
- package/__tests__/device-utils.test.ts +48 -1
- package/__tests__/deviceSettings.test.ts +11 -1
- package/__tests__/deviceUploadNft.test.ts +316 -0
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +5 -0
- package/__tests__/get-device-state.test.ts +132 -12
- package/__tests__/homescreen.test.ts +41 -1
- package/__tests__/kaspa-use-tweak-pro2.test.ts +2 -2
- package/__tests__/kaspaSignTransaction.test.ts +3 -26
- package/__tests__/logBlockEvent.test.ts +1 -1
- package/__tests__/method-protocol-support.test.ts +19 -0
- package/__tests__/open-wallet-session.test.ts +568 -37
- package/__tests__/pro2Nft.test.ts +108 -0
- package/__tests__/protocol-binding.test.ts +89 -0
- package/__tests__/protocol-v2-resources.test.ts +340 -0
- package/__tests__/protocol-v2-ui-lifecycle.test.ts +91 -0
- package/__tests__/protocol-v2-unlock-policy.test.ts +300 -37
- package/__tests__/protocol-v2.test.ts +1449 -867
- package/__tests__/protocolV2FileWrite.test.ts +152 -0
- package/__tests__/protocolV2UiInteraction.test.ts +63 -0
- package/__tests__/public-pro2-api-boundary.test.ts +1 -0
- package/__tests__/search-devices.test.ts +12 -3
- package/__tests__/tron-sign-message-init.test.ts +2 -2
- package/__tests__/uiProgressThrottle.test.ts +74 -0
- package/__tests__/uiPromiseRegistry.test.ts +115 -0
- package/dist/api/BaseMethod.d.ts +3 -1
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts +2 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/DetectDeviceConnectProtocol.d.ts +7 -0
- package/dist/api/DetectDeviceConnectProtocol.d.ts.map +1 -0
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +11 -4
- 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/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/device/DeviceUnlock.d.ts +2 -2
- package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +4 -3
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +9 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/kaspa/KaspaGetAddress.d.ts +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/api/polkadot/PolkadotGetAddress.d.ts +1 -1
- package/dist/api/polkadot/networks.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +31 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
- package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
- package/dist/api/solana/SolSignMessage.d.ts +1 -1
- package/dist/api/solana/SolSignOffchainMessage.d.ts +1 -1
- package/dist/api/solana/SolSignTransaction.d.ts +2 -2
- package/dist/api/sui/SuiGetAddress.d.ts +1 -1
- package/dist/api/sui/SuiGetPublicKey.d.ts +1 -1
- package/dist/api/sui/SuiSignMessage.d.ts +1 -1
- package/dist/api/sui/SuiSignTransaction.d.ts +1 -1
- package/dist/api/ton/TonGetAddress.d.ts +1 -1
- package/dist/api/ton/TonSignMessage.d.ts +1 -1
- package/dist/api/ton/TonSignProof.d.ts +1 -1
- package/dist/api/tron/TronSignMessage.d.ts +2 -2
- package/dist/api/tron/TronSignTransaction.d.ts +1 -1
- package/dist/core/deviceEventRegistration.d.ts +2 -0
- package/dist/core/deviceEventRegistration.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/uiPromiseRegistry.d.ts +6 -0
- package/dist/core/uiPromiseRegistry.d.ts.map +1 -0
- package/dist/data-manager/DataManager.d.ts +9 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +29 -5
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +2 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/deviceProfile/protocolV2DeviceIdentity.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/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-promise.d.ts +4 -2
- package/dist/events/ui-promise.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +31 -2
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/events/ui-response.d.ts +10 -2
- package/dist/events/ui-response.d.ts.map +1 -1
- package/dist/index.d.ts +243 -69
- package/dist/index.js +2563 -1217
- package/dist/inject.d.ts +6 -1
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/features.d.ts +0 -4
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +30 -0
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/uiInteraction.d.ts +7 -4
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts +6 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/detectDeviceConnectProtocol.d.ts +4 -0
- package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +1 -0
- package/dist/types/api/deviceUnlock.d.ts +5 -1
- package/dist/types/api/deviceUnlock.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +4 -2
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +6 -1
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -0
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts +2 -2
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts +1 -0
- package/dist/types/params.d.ts.map +1 -1
- package/dist/types/settings.d.ts +38 -8
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +4 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/dist/utils/deviceSettings.d.ts.map +1 -1
- package/dist/utils/homescreen.d.ts +4 -0
- package/dist/utils/homescreen.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +31 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -0
- package/dist/utils/pro2Wallpaper.d.ts +10 -0
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/dist/utils/uiProgressThrottle.d.ts +3 -0
- package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +8 -10
- package/src/api/CheckAllFirmwareRelease.ts +61 -11
- package/src/api/DetectDeviceConnectProtocol.ts +18 -0
- package/src/api/FirmwareUpdate.ts +6 -2
- package/src/api/FirmwareUpdateV2.ts +5 -2
- package/src/api/FirmwareUpdateV3.ts +6 -2
- package/src/api/FirmwareUpdateV4.ts +463 -258
- package/src/api/GetPassphraseState.ts +7 -1
- package/src/api/OpenWalletSession.ts +71 -17
- package/src/api/SearchDevices.ts +3 -1
- package/src/api/allnetwork/AllNetworkGetAddress.ts +21 -21
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +36 -11
- package/src/api/device/DeviceSettings.ts +5 -4
- package/src/api/device/DeviceUnlock.ts +8 -3
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +27 -12
- package/src/api/firmware/uploadFirmware.ts +17 -4
- package/src/api/helpers/protocolV2FileWrite.ts +202 -63
- package/src/api/index.ts +2 -0
- package/src/api/kaspa/KaspaGetAddress.ts +1 -1
- package/src/api/kaspa/KaspaSignTransaction.ts +2 -13
- package/src/api/polkadot/networks.ts +3 -3
- package/src/api/protocol-v2/DeviceUploadNft.ts +197 -0
- package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
- package/src/api/protocol-v2/helpers.ts +1 -1
- package/src/api/solana/SolSignMessage.ts +1 -1
- package/src/api/solana/SolSignOffchainMessage.ts +1 -1
- package/src/api/solana/SolSignTransaction.ts +2 -2
- package/src/api/sui/SuiGetAddress.ts +1 -1
- package/src/api/sui/SuiGetPublicKey.ts +1 -1
- package/src/api/sui/SuiSignMessage.ts +1 -1
- package/src/api/sui/SuiSignTransaction.ts +1 -1
- package/src/api/ton/TonGetAddress.ts +1 -1
- package/src/api/ton/TonSignMessage.ts +1 -1
- package/src/api/ton/TonSignProof.ts +1 -1
- package/src/api/tron/TronSignMessage.ts +2 -2
- package/src/api/tron/TronSignTransaction.ts +1 -1
- package/src/core/deviceEventRegistration.ts +4 -0
- package/src/core/index.ts +213 -111
- package/src/core/uiPromiseRegistry.ts +63 -0
- package/src/data/messages/messages-protocol-v2.json +25 -0
- package/src/data-manager/DataManager.ts +88 -11
- package/src/data-manager/TransportManager.ts +6 -0
- package/src/device/Device.ts +273 -43
- package/src/device/DeviceCommands.ts +31 -4
- package/src/device/DeviceConnector.ts +33 -13
- package/src/device/DevicePool.ts +21 -7
- package/src/deviceProfile/buildDeviceFeatures.ts +2 -2
- package/src/deviceProfile/protocolV2DeviceIdentity.ts +3 -0
- package/src/events/device.ts +4 -0
- package/src/events/logBlockEvent.ts +1 -0
- package/src/events/ui-promise.ts +4 -2
- package/src/events/ui-request.ts +36 -2
- package/src/events/ui-response.ts +12 -2
- package/src/inject.ts +58 -2
- package/src/lowLevelInject.ts +6 -3
- package/src/protocols/protocol-v2/features.ts +10 -7
- package/src/protocols/protocol-v2/firmware.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +390 -0
- package/src/protocols/protocol-v2/uiInteraction.ts +46 -6
- package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +131 -0
- package/src/protocols/protocol-v2/walletSession.ts +214 -36
- package/src/topLevelInject.ts +6 -3
- package/src/types/api/checkAllFirmwareRelease.ts +3 -1
- package/src/types/api/detectDeviceConnectProtocol.ts +6 -0
- package/src/types/api/deviceUnlock.ts +12 -1
- package/src/types/api/export.ts +1 -0
- package/src/types/api/firmwareUpdate.ts +6 -3
- package/src/types/api/index.ts +13 -0
- package/src/types/api/protocolV2.ts +13 -0
- package/src/types/device.ts +15 -3
- package/src/types/params.ts +7 -1
- package/src/types/settings.ts +60 -9
- package/src/utils/deviceFeaturesCompat.ts +5 -0
- package/src/utils/deviceFeaturesUtils.ts +14 -3
- package/src/utils/deviceInfoUtils.ts +2 -0
- package/src/utils/deviceSettings.ts +3 -0
- package/src/utils/homescreen.ts +32 -6
- package/src/utils/index.ts +6 -1
- package/src/utils/pro2Nft.ts +142 -0
- package/src/utils/pro2Wallpaper.ts +35 -8
- package/src/utils/uiProgressThrottle.ts +63 -0
- package/dist/api/firmware/progressThrottle.d.ts +0 -3
- package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
- package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
- package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
- package/src/api/firmware/progressThrottle.ts +0 -44
- package/src/protocols/protocol-v2/lockedError.ts +0 -10
- package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
import {
|
|
3
3
|
DeviceRebootType,
|
|
4
4
|
DeviceSessionPinType,
|
|
5
5
|
DeviceSettingsPage,
|
|
6
6
|
DeviceType,
|
|
7
7
|
} from '@onekeyfe/hd-transport';
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
8
9
|
|
|
9
10
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
10
11
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -61,6 +62,7 @@ import { createCoreApi } from '../src/inject';
|
|
|
61
62
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
62
63
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
63
64
|
import { DevicePool } from '../src/device/DevicePool';
|
|
65
|
+
import { DEVICE } from '../src/events';
|
|
64
66
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
65
67
|
import {
|
|
66
68
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
@@ -74,11 +76,15 @@ import {
|
|
|
74
76
|
requestProtocolV2ProtocolInfo,
|
|
75
77
|
supportsProtocolV2Message,
|
|
76
78
|
} from '../src/protocols/protocol-v2/features';
|
|
79
|
+
import { PROTOCOL_V2_RESOURCE_DEVICE_PATHS } from '../src/protocols/protocol-v2/resources';
|
|
77
80
|
import {
|
|
78
81
|
getProtocolV2WalletSession,
|
|
79
82
|
refreshProtocolV2DeviceStatus,
|
|
80
83
|
} from '../src/protocols/protocol-v2/walletSession';
|
|
81
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
createProtocolV2UnlockContext,
|
|
86
|
+
runMethodWithUnlockPolicy,
|
|
87
|
+
} from '../src/protocols/protocol-v2/unlockPolicyRunner';
|
|
82
88
|
import { BaseMethod } from '../src/api/BaseMethod';
|
|
83
89
|
import {
|
|
84
90
|
buildProtocolV1FeaturesPayload,
|
|
@@ -241,7 +247,7 @@ describe('UploadPortfolio', () => {
|
|
|
241
247
|
(method as any).device = device;
|
|
242
248
|
|
|
243
249
|
method.init();
|
|
244
|
-
await
|
|
250
|
+
await runMethodWithUnlockPolicy(method, device as any);
|
|
245
251
|
|
|
246
252
|
expect(unlockDevice).not.toHaveBeenCalled();
|
|
247
253
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
@@ -417,14 +423,16 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
417
423
|
}
|
|
418
424
|
return d.features;
|
|
419
425
|
};
|
|
426
|
+
d.ensureProtocolV2RuntimeContext ??= (timeoutMs?: number) =>
|
|
427
|
+
requestProtocolV2ProtocolInfo({
|
|
428
|
+
commands: d.getCommands?.() ?? d.commands,
|
|
429
|
+
timeoutMs,
|
|
430
|
+
});
|
|
420
431
|
d.probeProtocolV2RuntimeState ??= async (
|
|
421
432
|
deviceInfo: ProtocolV2DeviceInfo,
|
|
422
433
|
timeoutMs?: number
|
|
423
434
|
) => {
|
|
424
|
-
const protocolInfo = await
|
|
425
|
-
commands: d.getCommands(),
|
|
426
|
-
timeoutMs,
|
|
427
|
-
});
|
|
435
|
+
const protocolInfo = await d.ensureProtocolV2RuntimeContext(timeoutMs);
|
|
428
436
|
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
|
|
429
437
|
if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
|
|
430
438
|
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
@@ -582,7 +590,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
582
590
|
} as any);
|
|
583
591
|
const protocolV2 = buildProtocolV2FeaturesPayload({
|
|
584
592
|
deviceInfo: {
|
|
585
|
-
hw: { serial_no: 'P2-001' },
|
|
593
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'P2-001' },
|
|
586
594
|
coprocessor: { bt_adv_name: 'Pro 2 BLE' },
|
|
587
595
|
},
|
|
588
596
|
deviceStatus: {
|
|
@@ -781,6 +789,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
781
789
|
if (request === 'DeviceStatusGet') {
|
|
782
790
|
return { type: 'DeviceStatus', message: { unlocked: true } };
|
|
783
791
|
}
|
|
792
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
793
|
+
return { type: 'Success', message: {} };
|
|
794
|
+
}
|
|
784
795
|
if (request === 'DeviceSessionGet') {
|
|
785
796
|
return {
|
|
786
797
|
type: 'DeviceSession',
|
|
@@ -805,7 +816,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
805
816
|
).resolves.toEqual({
|
|
806
817
|
passphraseState: 'state-1',
|
|
807
818
|
newSession: 'session-1',
|
|
808
|
-
unlockedAttachPin:
|
|
819
|
+
unlockedAttachPin: false,
|
|
809
820
|
resumed: false,
|
|
810
821
|
});
|
|
811
822
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
@@ -814,7 +825,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
814
825
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
815
826
|
type: DeviceSessionPinType.Main,
|
|
816
827
|
});
|
|
817
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
828
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
829
|
+
passphrase: '',
|
|
830
|
+
on_device: false,
|
|
831
|
+
});
|
|
832
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
833
|
+
seed_domains: [],
|
|
834
|
+
});
|
|
818
835
|
device.passphraseState = 'state-1';
|
|
819
836
|
expect(device.getInternalState()).toBe('session-1');
|
|
820
837
|
});
|
|
@@ -847,6 +864,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
847
864
|
},
|
|
848
865
|
};
|
|
849
866
|
}
|
|
867
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: {} };
|
|
850
868
|
if (request === 'DeviceSessionGet') {
|
|
851
869
|
return {
|
|
852
870
|
message: {
|
|
@@ -863,9 +881,16 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
863
881
|
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
864
882
|
|
|
865
883
|
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
|
|
866
|
-
expect(
|
|
884
|
+
expect(
|
|
885
|
+
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
886
|
+
).toHaveLength(1);
|
|
887
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
888
|
+
seed_domains: [],
|
|
889
|
+
});
|
|
867
890
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
868
891
|
session_id: 'standard-session',
|
|
892
|
+
btc_test_address: 'standard-state',
|
|
893
|
+
seed_domains: [],
|
|
869
894
|
});
|
|
870
895
|
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
871
896
|
passphraseState: 'standard-state',
|
|
@@ -907,6 +932,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
907
932
|
},
|
|
908
933
|
};
|
|
909
934
|
}
|
|
935
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: {} };
|
|
910
936
|
if (request === 'DeviceSessionAskPin') {
|
|
911
937
|
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
912
938
|
}
|
|
@@ -1001,6 +1027,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1001
1027
|
if (request === 'DeviceStatusGet') {
|
|
1002
1028
|
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
1003
1029
|
}
|
|
1030
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: {} };
|
|
1004
1031
|
if (request === 'DeviceSessionGet') {
|
|
1005
1032
|
sessionGetCalls += 1;
|
|
1006
1033
|
return {
|
|
@@ -1227,7 +1254,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1227
1254
|
passphrase: 'host hidden wallet',
|
|
1228
1255
|
on_device: false,
|
|
1229
1256
|
});
|
|
1230
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1257
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1258
|
+
seed_domains: [],
|
|
1259
|
+
});
|
|
1231
1260
|
});
|
|
1232
1261
|
|
|
1233
1262
|
test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
|
|
@@ -1335,6 +1364,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1335
1364
|
|
|
1336
1365
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1337
1366
|
session_id: 'session-a',
|
|
1367
|
+
btc_test_address: 'state-a',
|
|
1368
|
+
seed_domains: [],
|
|
1338
1369
|
});
|
|
1339
1370
|
expect(device.getInternalState()).toBe('session-b');
|
|
1340
1371
|
});
|
|
@@ -1402,7 +1433,15 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1402
1433
|
});
|
|
1403
1434
|
expect(typedCall.mock.calls).toEqual([
|
|
1404
1435
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1405
|
-
[
|
|
1436
|
+
[
|
|
1437
|
+
'DeviceSessionGet',
|
|
1438
|
+
'DeviceSession',
|
|
1439
|
+
{
|
|
1440
|
+
session_id: 'session-a',
|
|
1441
|
+
btc_test_address: 'state-a',
|
|
1442
|
+
seed_domains: [],
|
|
1443
|
+
},
|
|
1444
|
+
],
|
|
1406
1445
|
]);
|
|
1407
1446
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1408
1447
|
expect(device.getInternalState()).toBeUndefined();
|
|
@@ -1450,7 +1489,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1450
1489
|
{ passphrase: 'host hidden wallet', on_device: false },
|
|
1451
1490
|
],
|
|
1452
1491
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1453
|
-
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1492
|
+
['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
|
|
1454
1493
|
]);
|
|
1455
1494
|
});
|
|
1456
1495
|
|
|
@@ -1553,6 +1592,73 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1553
1592
|
expect(device.getInternalState()).toBeUndefined();
|
|
1554
1593
|
});
|
|
1555
1594
|
|
|
1595
|
+
test.each([false, true])(
|
|
1596
|
+
'rejects a different Attach PIN wallet when Pro2 lock failure is %s',
|
|
1597
|
+
async lockFails => {
|
|
1598
|
+
const deviceId = 'attach-mismatch-device';
|
|
1599
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1600
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1601
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1602
|
+
{
|
|
1603
|
+
status: {
|
|
1604
|
+
device_id: deviceId,
|
|
1605
|
+
unlocked: true,
|
|
1606
|
+
passphrase_enabled: true,
|
|
1607
|
+
attach_to_pin_enabled: true,
|
|
1608
|
+
unlocked_by_attach_to_pin: false,
|
|
1609
|
+
},
|
|
1610
|
+
}
|
|
1611
|
+
);
|
|
1612
|
+
device.passphraseState = 'expected-state';
|
|
1613
|
+
|
|
1614
|
+
const typedCall = jest.fn((request: string) => {
|
|
1615
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
1616
|
+
if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
|
|
1617
|
+
if (request === 'DeviceStatusGet') {
|
|
1618
|
+
return {
|
|
1619
|
+
message: {
|
|
1620
|
+
device_id: deviceId,
|
|
1621
|
+
unlocked: true,
|
|
1622
|
+
passphrase_enabled: true,
|
|
1623
|
+
attach_to_pin_enabled: true,
|
|
1624
|
+
unlocked_by_attach_to_pin: true,
|
|
1625
|
+
},
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
if (request === 'DeviceSessionGet') {
|
|
1629
|
+
return {
|
|
1630
|
+
message: {
|
|
1631
|
+
btc_test_address: 'wrong-attach-state',
|
|
1632
|
+
session_id: 'wrong-attach-session',
|
|
1633
|
+
},
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (request === 'LockDevice') {
|
|
1637
|
+
if (lockFails) throw new Error('Lock unsupported');
|
|
1638
|
+
return { message: { message: 'Device locked' } };
|
|
1639
|
+
}
|
|
1640
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1641
|
+
});
|
|
1642
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
1643
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1644
|
+
|
|
1645
|
+
await expect(
|
|
1646
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1647
|
+
).rejects.toMatchObject({
|
|
1648
|
+
errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1652
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1653
|
+
btc_test_address: 'expected-state',
|
|
1654
|
+
seed_domains: [],
|
|
1655
|
+
});
|
|
1656
|
+
expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
|
|
1657
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
1658
|
+
expect(device.getInternalState()).toBeUndefined();
|
|
1659
|
+
}
|
|
1660
|
+
);
|
|
1661
|
+
|
|
1556
1662
|
test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
|
|
1557
1663
|
const device = Device.fromDescriptor({
|
|
1558
1664
|
id: 'cache-device-attach-recovery',
|
|
@@ -1609,7 +1715,17 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1609
1715
|
}),
|
|
1610
1716
|
{ cancelDeviceOnReject: false }
|
|
1611
1717
|
);
|
|
1612
|
-
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1718
|
+
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1719
|
+
DeviceSessionPinType.AttachToPin,
|
|
1720
|
+
expect.objectContaining({
|
|
1721
|
+
emitUiEvent: false,
|
|
1722
|
+
interaction: expect.objectContaining({
|
|
1723
|
+
phase: 'pin',
|
|
1724
|
+
transition: 'start',
|
|
1725
|
+
protocol: 'V2',
|
|
1726
|
+
}),
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1613
1729
|
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1614
1730
|
});
|
|
1615
1731
|
|
|
@@ -1893,12 +2009,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1893
2009
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
1894
2010
|
});
|
|
1895
2011
|
|
|
1896
|
-
test('
|
|
2012
|
+
test('uses the cached wallet address when the local session is missing', async () => {
|
|
1897
2013
|
const typedCall = jest
|
|
1898
2014
|
.fn()
|
|
1899
2015
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1900
|
-
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1901
|
-
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1902
2016
|
.mockResolvedValueOnce({
|
|
1903
2017
|
message: {
|
|
1904
2018
|
btc_test_address: 'expected-state',
|
|
@@ -1926,16 +2040,11 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1926
2040
|
await expect(
|
|
1927
2041
|
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1928
2042
|
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
1929
|
-
expect(
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
reason: 'session-recovery',
|
|
1935
|
-
expectedPassphraseState: 'expected-state',
|
|
1936
|
-
},
|
|
1937
|
-
{ cancelDeviceOnReject: false }
|
|
1938
|
-
);
|
|
2043
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2044
|
+
btc_test_address: 'expected-state',
|
|
2045
|
+
seed_domains: [],
|
|
2046
|
+
});
|
|
2047
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1939
2048
|
});
|
|
1940
2049
|
|
|
1941
2050
|
test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
|
|
@@ -2001,8 +2110,15 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2001
2110
|
true
|
|
2002
2111
|
);
|
|
2003
2112
|
expect(typedCall.mock.calls).toEqual([
|
|
2004
|
-
[
|
|
2005
|
-
|
|
2113
|
+
[
|
|
2114
|
+
'DeviceSessionGet',
|
|
2115
|
+
'DeviceSession',
|
|
2116
|
+
{
|
|
2117
|
+
session_id: 'session-pro2-app',
|
|
2118
|
+
btc_test_address: 'state-pro2-app',
|
|
2119
|
+
seed_domains: [],
|
|
2120
|
+
},
|
|
2121
|
+
],
|
|
2006
2122
|
]);
|
|
2007
2123
|
});
|
|
2008
2124
|
|
|
@@ -2191,7 +2307,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2191
2307
|
passphrase: 'host hidden wallet',
|
|
2192
2308
|
on_device: false,
|
|
2193
2309
|
});
|
|
2194
|
-
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2310
|
+
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2311
|
+
seed_domains: [],
|
|
2312
|
+
});
|
|
2195
2313
|
});
|
|
2196
2314
|
|
|
2197
2315
|
test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
|
|
@@ -2269,12 +2387,83 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2269
2387
|
);
|
|
2270
2388
|
|
|
2271
2389
|
expect(device.getInternalState()).toBeUndefined();
|
|
2272
|
-
expect(typedCall).
|
|
2390
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2391
|
+
btc_test_address: 'expected-state',
|
|
2392
|
+
seed_domains: [],
|
|
2393
|
+
});
|
|
2394
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2273
2395
|
passphrase: 'host hidden wallet',
|
|
2274
2396
|
on_device: false,
|
|
2275
2397
|
});
|
|
2276
|
-
expect(typedCall).
|
|
2277
|
-
expect(typedCall).
|
|
2398
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2399
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2400
|
+
seed_domains: [],
|
|
2401
|
+
});
|
|
2402
|
+
});
|
|
2403
|
+
|
|
2404
|
+
test('fails closed instead of switching to Main PIN during a standard-wallet safety check', async () => {
|
|
2405
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2406
|
+
const typedCall = jest.fn((request: string) => {
|
|
2407
|
+
if (request === 'ProtocolInfoRequest') {
|
|
2408
|
+
return { message: { version: 2 } };
|
|
2409
|
+
}
|
|
2410
|
+
if (request === 'DeviceStatusGet') {
|
|
2411
|
+
return {
|
|
2412
|
+
message: {
|
|
2413
|
+
device_id: 'attach-wallet-device',
|
|
2414
|
+
unlocked: true,
|
|
2415
|
+
passphrase_enabled: true,
|
|
2416
|
+
attach_to_pin_enabled: true,
|
|
2417
|
+
unlocked_by_attach_to_pin: true,
|
|
2418
|
+
},
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
if (request === 'DeviceSessionAskPin' || request === 'DeviceSessionAskPassphrase') {
|
|
2422
|
+
return { message: { message: 'Wallet selected' } };
|
|
2423
|
+
}
|
|
2424
|
+
if (request === 'DeviceSessionGet') {
|
|
2425
|
+
return {
|
|
2426
|
+
message: {
|
|
2427
|
+
session_id: 'main-wallet-session',
|
|
2428
|
+
btc_test_address: 'main-wallet-state',
|
|
2429
|
+
},
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
if (request === 'LockDevice') {
|
|
2433
|
+
return { message: { message: 'Device locked' } };
|
|
2434
|
+
}
|
|
2435
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
2436
|
+
});
|
|
2437
|
+
|
|
2438
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
2439
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
2440
|
+
{
|
|
2441
|
+
status: {
|
|
2442
|
+
device_id: 'attach-wallet-device',
|
|
2443
|
+
unlocked: true,
|
|
2444
|
+
passphrase_enabled: true,
|
|
2445
|
+
attach_to_pin_enabled: true,
|
|
2446
|
+
// 模拟缓存滞后:设备状态刷新后才确认当前由 Attach PIN 解锁。
|
|
2447
|
+
unlocked_by_attach_to_pin: false,
|
|
2448
|
+
},
|
|
2449
|
+
}
|
|
2450
|
+
);
|
|
2451
|
+
(device as any).commands = { typedCall };
|
|
2452
|
+
const clearInternalState = jest.spyOn(device, 'clearInternalState');
|
|
2453
|
+
|
|
2454
|
+
await expect(device.checkPassphraseStateSafety(undefined, true, false)).rejects.toMatchObject({
|
|
2455
|
+
errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2459
|
+
expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
|
|
2460
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
|
|
2461
|
+
expect(typedCall).not.toHaveBeenCalledWith(
|
|
2462
|
+
'DeviceSessionAskPassphrase',
|
|
2463
|
+
'Success',
|
|
2464
|
+
expect.anything()
|
|
2465
|
+
);
|
|
2466
|
+
expect(clearInternalState).toHaveBeenCalledTimes(1);
|
|
2278
2467
|
});
|
|
2279
2468
|
|
|
2280
2469
|
test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
|
|
@@ -2307,10 +2496,22 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2307
2496
|
await expect(
|
|
2308
2497
|
device.checkPassphraseStateSafety('stale-hidden-state', true, false)
|
|
2309
2498
|
).resolves.toBe(true);
|
|
2310
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2499
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
2311
2500
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2312
2501
|
eventless_wallet_session: true,
|
|
2313
2502
|
});
|
|
2503
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2504
|
+
seed_domains: [],
|
|
2505
|
+
});
|
|
2506
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2507
|
+
passphrase: '',
|
|
2508
|
+
on_device: false,
|
|
2509
|
+
});
|
|
2510
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2511
|
+
expect(typedCall.mock.calls.filter(([request]) => request === 'DeviceStatusGet')).toHaveLength(
|
|
2512
|
+
2
|
|
2513
|
+
);
|
|
2514
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
|
|
2314
2515
|
});
|
|
2315
2516
|
|
|
2316
2517
|
test('marks fallback features as unavailable when DeviceInfo is missing', () => {
|
|
@@ -2381,7 +2582,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2381
2582
|
uuid: 'CACHED-SERIAL',
|
|
2382
2583
|
deviceId: null,
|
|
2383
2584
|
bleName: 'Cached BLE',
|
|
2384
|
-
name: 'Cached
|
|
2585
|
+
name: 'Cached BLE',
|
|
2385
2586
|
label: 'Cached Label',
|
|
2386
2587
|
deviceType: 'pro2',
|
|
2387
2588
|
});
|
|
@@ -2408,7 +2609,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2408
2609
|
expect(device.features?.label).toBeNull();
|
|
2409
2610
|
});
|
|
2410
2611
|
|
|
2411
|
-
test('
|
|
2612
|
+
test('uses the BLE name for discovery while preserving the Protocol V2 label', () => {
|
|
2412
2613
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2413
2614
|
(device as any).features = {
|
|
2414
2615
|
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
|
|
@@ -2416,9 +2617,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2416
2617
|
bleName: 'Pro2 6136',
|
|
2417
2618
|
};
|
|
2418
2619
|
|
|
2620
|
+
expect(device.getCurrentDisplayName()).toBe('My Pro 2');
|
|
2419
2621
|
expect(device.toMessageObject()).toMatchObject({
|
|
2420
2622
|
bleName: 'Pro2 6136',
|
|
2421
|
-
name: '
|
|
2623
|
+
name: 'Pro2 6136',
|
|
2422
2624
|
label: 'My Pro 2',
|
|
2423
2625
|
});
|
|
2424
2626
|
});
|
|
@@ -2472,10 +2674,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2472
2674
|
hw: true,
|
|
2473
2675
|
fw: true,
|
|
2474
2676
|
coprocessor: true,
|
|
2475
|
-
se1: true,
|
|
2476
|
-
se2: true,
|
|
2477
|
-
se3: true,
|
|
2478
|
-
se4: true,
|
|
2479
2677
|
},
|
|
2480
2678
|
types: {
|
|
2481
2679
|
version: true,
|
|
@@ -2499,8 +2697,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2499
2697
|
).rejects.toThrow('DeviceInfo not supported');
|
|
2500
2698
|
});
|
|
2501
2699
|
|
|
2502
|
-
test('does not inherit Pro or
|
|
2503
|
-
const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any
|
|
2700
|
+
test('does not inherit Pro or model_touch ranges for Protocol V2 devices', () => {
|
|
2701
|
+
const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2702
|
+
hw: { Device_type: DeviceType.PRO2 },
|
|
2703
|
+
});
|
|
2504
2704
|
const checkedTypes: string[] = [];
|
|
2505
2705
|
|
|
2506
2706
|
const versionRange = getMethodVersionRange(features, type => {
|
|
@@ -2512,11 +2712,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2512
2712
|
});
|
|
2513
2713
|
|
|
2514
2714
|
expect(versionRange).toBeUndefined();
|
|
2515
|
-
expect(checkedTypes).toEqual(['pro2']);
|
|
2715
|
+
expect(checkedTypes).toEqual(['pro2', 'model_pro2']);
|
|
2516
2716
|
});
|
|
2517
2717
|
|
|
2518
2718
|
test('uses firmware-v1 as the Pro2 remote firmware config field', () => {
|
|
2519
|
-
const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any
|
|
2719
|
+
const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2720
|
+
hw: { Device_type: DeviceType.PRO2 },
|
|
2721
|
+
});
|
|
2520
2722
|
|
|
2521
2723
|
expect(
|
|
2522
2724
|
getFirmwareUpdateField({
|
|
@@ -2667,10 +2869,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2667
2869
|
hw: true,
|
|
2668
2870
|
fw: true,
|
|
2669
2871
|
coprocessor: true,
|
|
2670
|
-
se1: true,
|
|
2671
|
-
se2: true,
|
|
2672
|
-
se3: true,
|
|
2673
|
-
se4: true,
|
|
2674
2872
|
},
|
|
2675
2873
|
types: {
|
|
2676
2874
|
version: true,
|
|
@@ -2681,7 +2879,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2681
2879
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
2682
2880
|
}
|
|
2683
2881
|
);
|
|
2684
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2882
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2883
|
+
eventless_wallet_session: true,
|
|
2884
|
+
});
|
|
2685
2885
|
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2686
2886
|
});
|
|
2687
2887
|
|
|
@@ -2719,7 +2919,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2719
2919
|
}),
|
|
2720
2920
|
expect.anything()
|
|
2721
2921
|
);
|
|
2722
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2922
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2923
|
+
eventless_wallet_session: true,
|
|
2924
|
+
});
|
|
2723
2925
|
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2724
2926
|
expect(device.features).toMatchObject({
|
|
2725
2927
|
deviceId: null,
|
|
@@ -2985,10 +3187,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2985
3187
|
fw: { application: { version: '1.2.4' } },
|
|
2986
3188
|
},
|
|
2987
3189
|
})
|
|
2988
|
-
.mockResolvedValueOnce({
|
|
2989
|
-
type: 'ProtocolInfo',
|
|
2990
|
-
message: protocolV2ApplicationInfo,
|
|
2991
|
-
})
|
|
2992
3190
|
.mockResolvedValueOnce({
|
|
2993
3191
|
type: 'DeviceStatus',
|
|
2994
3192
|
message: { init_states: true, unlocked: true },
|
|
@@ -3010,7 +3208,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3010
3208
|
firmwareVersion: '1.2.4',
|
|
3011
3209
|
passphraseProtection: null,
|
|
3012
3210
|
});
|
|
3013
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3211
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
3014
3212
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3015
3213
|
4,
|
|
3016
3214
|
'DeviceInfoGet',
|
|
@@ -3020,10 +3218,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3020
3218
|
hw: true,
|
|
3021
3219
|
fw: true,
|
|
3022
3220
|
coprocessor: true,
|
|
3023
|
-
se1: true,
|
|
3024
|
-
se2: true,
|
|
3025
|
-
se3: true,
|
|
3026
|
-
se4: true,
|
|
3027
3221
|
},
|
|
3028
3222
|
types: {
|
|
3029
3223
|
version: true,
|
|
@@ -3124,9 +3318,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3124
3318
|
status: { init_states: true },
|
|
3125
3319
|
}
|
|
3126
3320
|
);
|
|
3321
|
+
const pinOnDevice = jest.fn();
|
|
3322
|
+
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
3127
3323
|
|
|
3128
3324
|
const features = await device.unlockDevice();
|
|
3129
3325
|
|
|
3326
|
+
expect(pinOnDevice).toHaveBeenCalledWith(
|
|
3327
|
+
device,
|
|
3328
|
+
DeviceSessionPinType.Main,
|
|
3329
|
+
expect.objectContaining({
|
|
3330
|
+
source: 'unlock-coordinator',
|
|
3331
|
+
reason: 'device-unlock',
|
|
3332
|
+
deviceOnly: true,
|
|
3333
|
+
})
|
|
3334
|
+
);
|
|
3335
|
+
expect(pinOnDevice.mock.invocationCallOrder[0]).toBeLessThan(
|
|
3336
|
+
typedCall.mock.invocationCallOrder[0]
|
|
3337
|
+
);
|
|
3130
3338
|
expect(typedCall.mock.calls).toEqual([
|
|
3131
3339
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
3132
3340
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -3172,9 +3380,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3172
3380
|
throw new Error(`Unexpected request: ${requestType}`);
|
|
3173
3381
|
});
|
|
3174
3382
|
(device as any).commands = { typedCall };
|
|
3383
|
+
const pinOnDevice = jest.fn();
|
|
3384
|
+
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
3175
3385
|
|
|
3176
|
-
await device.unlockDevice();
|
|
3386
|
+
await device.unlockDevice(DeviceSessionPinType.Main, { emitUiEvent: false });
|
|
3177
3387
|
|
|
3388
|
+
expect(pinOnDevice).not.toHaveBeenCalled();
|
|
3178
3389
|
expect(typedCall.mock.calls).toEqual([
|
|
3179
3390
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
3180
3391
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -3417,71 +3628,77 @@ describe('API compatibility handling', () => {
|
|
|
3417
3628
|
});
|
|
3418
3629
|
});
|
|
3419
3630
|
|
|
3420
|
-
test('
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3631
|
+
test.each(['pro2', 'neo'] as const)(
|
|
3632
|
+
'allows %s Solana signing methods through Protocol V2 version checks',
|
|
3633
|
+
deviceType => {
|
|
3634
|
+
const features = {
|
|
3635
|
+
deviceType,
|
|
3636
|
+
} as Features;
|
|
3424
3637
|
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3638
|
+
const solSignMessageMethod = new SolSignMessage({
|
|
3639
|
+
id: 1,
|
|
3640
|
+
payload: {
|
|
3641
|
+
method: 'solSignMessage',
|
|
3642
|
+
path: "m/44'/501'/0'/0'",
|
|
3643
|
+
messageHex: '48656c6c6f',
|
|
3644
|
+
},
|
|
3645
|
+
});
|
|
3646
|
+
const solSignOffchainMessageMethod = new SolSignOffchainMessage({
|
|
3647
|
+
id: 2,
|
|
3648
|
+
payload: {
|
|
3649
|
+
method: 'solSignOffchainMessage',
|
|
3650
|
+
path: "m/44'/501'/0'/0'",
|
|
3651
|
+
messageHex: '48656c6c6f',
|
|
3652
|
+
},
|
|
3653
|
+
});
|
|
3654
|
+
const solSignTransactionMethod = new SolSignTransaction({
|
|
3655
|
+
id: 3,
|
|
3656
|
+
payload: {
|
|
3657
|
+
method: 'solSignTransaction',
|
|
3658
|
+
path: "m/44'/501'/0'/0'",
|
|
3659
|
+
rawTx: '00',
|
|
3660
|
+
},
|
|
3661
|
+
});
|
|
3662
|
+
const solSignVersionedTransactionMethod = new SolSignTransaction({
|
|
3663
|
+
id: 4,
|
|
3664
|
+
payload: {
|
|
3665
|
+
method: 'solSignTransaction',
|
|
3666
|
+
path: "m/44'/501'/0'/0'",
|
|
3667
|
+
rawTx: '80',
|
|
3668
|
+
},
|
|
3669
|
+
});
|
|
3457
3670
|
|
|
3458
|
-
|
|
3459
|
-
|
|
3671
|
+
solSignTransactionMethod.init();
|
|
3672
|
+
solSignVersionedTransactionMethod.init();
|
|
3460
3673
|
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3674
|
+
expect(
|
|
3675
|
+
getMethodVersionRange(features, type => solSignMessageMethod.getVersionRange()[type])
|
|
3676
|
+
).toEqual({
|
|
3677
|
+
min: '0.0.0',
|
|
3678
|
+
});
|
|
3679
|
+
expect(
|
|
3680
|
+
getMethodVersionRange(
|
|
3681
|
+
features,
|
|
3682
|
+
type => solSignOffchainMessageMethod.getVersionRange()[type]
|
|
3683
|
+
)
|
|
3684
|
+
).toEqual({
|
|
3685
|
+
min: '0.0.0',
|
|
3686
|
+
});
|
|
3687
|
+
expect(
|
|
3688
|
+
getMethodVersionRange(features, type => solSignTransactionMethod.getVersionRange()[type])
|
|
3689
|
+
).toEqual({
|
|
3690
|
+
min: '0.0.0',
|
|
3691
|
+
});
|
|
3692
|
+
expect(
|
|
3693
|
+
getMethodVersionRange(
|
|
3694
|
+
features,
|
|
3695
|
+
type => solSignVersionedTransactionMethod.getVersionRange()[type]
|
|
3696
|
+
)
|
|
3697
|
+
).toEqual({
|
|
3698
|
+
min: '0.0.0',
|
|
3699
|
+
});
|
|
3700
|
+
}
|
|
3701
|
+
);
|
|
3485
3702
|
|
|
3486
3703
|
test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
|
|
3487
3704
|
const rawTx = '0x'.concat('ab'.repeat(5000));
|
|
@@ -3576,39 +3793,15 @@ describe('API compatibility handling', () => {
|
|
|
3576
3793
|
});
|
|
3577
3794
|
|
|
3578
3795
|
describe('Protocol V2 firmware update targets', () => {
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
version?: [number, number, number];
|
|
3589
|
-
payloadHash?: string;
|
|
3590
|
-
headerHash?: string;
|
|
3591
|
-
} = {}) => {
|
|
3592
|
-
const header = new Uint8Array(OKPP_HEADER_SIZE);
|
|
3593
|
-
const view = new DataView(header.buffer);
|
|
3594
|
-
'OKPP'.split('').forEach((char, index) => {
|
|
3595
|
-
header[index] = char.charCodeAt(0);
|
|
3596
|
-
});
|
|
3597
|
-
view.setUint32(0x04, 1, true);
|
|
3598
|
-
type.split('').forEach((char, index) => {
|
|
3599
|
-
header[0x08 + index] = char.charCodeAt(0);
|
|
3600
|
-
});
|
|
3601
|
-
view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
|
|
3602
|
-
view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
|
|
3603
|
-
const writeHex = (offset: number, hex: string) => {
|
|
3604
|
-
for (let i = 0; i < hex.length / 2; i++) {
|
|
3605
|
-
header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
3606
|
-
}
|
|
3607
|
-
};
|
|
3608
|
-
writeHex(0x200, payloadHash);
|
|
3609
|
-
writeHex(0x240, headerHash);
|
|
3610
|
-
return header;
|
|
3611
|
-
};
|
|
3796
|
+
let forceReloadDataSpy: jest.SpyInstance;
|
|
3797
|
+
|
|
3798
|
+
beforeEach(() => {
|
|
3799
|
+
forceReloadDataSpy = jest.spyOn(DataManager, 'forceReloadData').mockResolvedValue(undefined);
|
|
3800
|
+
});
|
|
3801
|
+
|
|
3802
|
+
afterEach(() => {
|
|
3803
|
+
jest.restoreAllMocks();
|
|
3804
|
+
});
|
|
3612
3805
|
|
|
3613
3806
|
test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', () => {
|
|
3614
3807
|
const method = new FirmwareUpdateV3({
|
|
@@ -3701,7 +3894,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3701
3894
|
specific: true,
|
|
3702
3895
|
},
|
|
3703
3896
|
},
|
|
3704
|
-
{ timeoutMs:
|
|
3897
|
+
{ timeoutMs: 30000 }
|
|
3705
3898
|
);
|
|
3706
3899
|
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
3707
3900
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
@@ -3765,18 +3958,84 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3765
3958
|
});
|
|
3766
3959
|
});
|
|
3767
3960
|
|
|
3768
|
-
test('
|
|
3961
|
+
test('enters Protocol V2 bootloader before reading and downloading remote resources', async () => {
|
|
3769
3962
|
const method = new FirmwareUpdateV4({
|
|
3770
3963
|
id: 1,
|
|
3771
3964
|
payload: {
|
|
3772
3965
|
method: 'firmwareUpdateV4',
|
|
3966
|
+
platform: 'web',
|
|
3967
|
+
targetsToUpdate: ['resource'],
|
|
3773
3968
|
},
|
|
3774
3969
|
});
|
|
3970
|
+
method.init();
|
|
3971
|
+
|
|
3775
3972
|
(method as any).device = stubDevice({
|
|
3776
|
-
originalDescriptor: { id: 'usb-id', path: '
|
|
3777
|
-
features: {
|
|
3778
|
-
|
|
3779
|
-
|
|
3973
|
+
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
3974
|
+
features: {
|
|
3975
|
+
deviceType: 'pro2',
|
|
3976
|
+
firmwareVersion: '1.0.0',
|
|
3977
|
+
mode: 'normal',
|
|
3978
|
+
bootloaderMode: false,
|
|
3979
|
+
capabilities: [],
|
|
3980
|
+
},
|
|
3981
|
+
isBootloader: () => false,
|
|
3982
|
+
isRomloader: () => false,
|
|
3983
|
+
});
|
|
3984
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
3985
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
3986
|
+
bootloaderBinary: null,
|
|
3987
|
+
fwBinaryMap: [],
|
|
3988
|
+
installItems: [],
|
|
3989
|
+
});
|
|
3990
|
+
|
|
3991
|
+
const order: string[] = [];
|
|
3992
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
3993
|
+
order.push('enter-bootloader');
|
|
3994
|
+
return Promise.resolve(true);
|
|
3995
|
+
});
|
|
3996
|
+
(method as any).prepareProtocolV2ResourceBundles = jest.fn().mockImplementation(() => {
|
|
3997
|
+
order.push('prepare-resources');
|
|
3998
|
+
return Promise.resolve([
|
|
3999
|
+
{
|
|
4000
|
+
name: 'images.okpkg',
|
|
4001
|
+
binary: new Uint8Array([1]).buffer,
|
|
4002
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
4003
|
+
},
|
|
4004
|
+
]);
|
|
4005
|
+
});
|
|
4006
|
+
(method as any).executeProtocolV2Update = jest.fn().mockImplementation(() => {
|
|
4007
|
+
order.push('execute-update');
|
|
4008
|
+
return Promise.resolve();
|
|
4009
|
+
});
|
|
4010
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
4011
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
4012
|
+
bootloaderVersion: '1.0.0',
|
|
4013
|
+
bleVersion: '1.0.0',
|
|
4014
|
+
firmwareVersion: '1.0.0',
|
|
4015
|
+
});
|
|
4016
|
+
method.postTipMessage = jest.fn();
|
|
4017
|
+
|
|
4018
|
+
await method.run();
|
|
4019
|
+
|
|
4020
|
+
expect(order).toEqual(['enter-bootloader', 'prepare-resources', 'execute-update']);
|
|
4021
|
+
});
|
|
4022
|
+
|
|
4023
|
+
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
4024
|
+
const method = new FirmwareUpdateV4({
|
|
4025
|
+
id: 1,
|
|
4026
|
+
payload: {
|
|
4027
|
+
method: 'firmwareUpdateV4',
|
|
4028
|
+
},
|
|
4029
|
+
});
|
|
4030
|
+
(method as any).device = stubDevice({
|
|
4031
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4032
|
+
features: { bootloader_mode: false, capabilities: [] },
|
|
4033
|
+
isBootloader: () => false,
|
|
4034
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4035
|
+
mode: 'bootloader',
|
|
4036
|
+
bootloaderMode: true,
|
|
4037
|
+
}),
|
|
4038
|
+
});
|
|
3780
4039
|
(method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
|
|
3781
4040
|
message: 'Device rebooted successfully',
|
|
3782
4041
|
});
|
|
@@ -3803,6 +4062,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3803
4062
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
3804
4063
|
(method as any).device.getCommands = () => ({ typedCall });
|
|
3805
4064
|
(method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
|
|
4065
|
+
(method as any).protocolV2ExpectedPath = 'usb-path';
|
|
3806
4066
|
method.postTipMessage = jest.fn();
|
|
3807
4067
|
|
|
3808
4068
|
await (method as any).enterProtocolV2BootloaderMode();
|
|
@@ -3814,6 +4074,66 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3814
4074
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
3815
4075
|
});
|
|
3816
4076
|
|
|
4077
|
+
test('announces bootloader success only after Protocol V2 bootloader mode is confirmed', async () => {
|
|
4078
|
+
const method = new FirmwareUpdateV4({
|
|
4079
|
+
id: 1,
|
|
4080
|
+
payload: {
|
|
4081
|
+
method: 'firmwareUpdateV4',
|
|
4082
|
+
},
|
|
4083
|
+
});
|
|
4084
|
+
(method as any).device = stubDevice({
|
|
4085
|
+
features: { bootloader_mode: false, capabilities: [] },
|
|
4086
|
+
isBootloader: () => false,
|
|
4087
|
+
isRomloader: () => false,
|
|
4088
|
+
});
|
|
4089
|
+
(method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
|
|
4090
|
+
message: 'Device rebooted successfully',
|
|
4091
|
+
});
|
|
4092
|
+
|
|
4093
|
+
const tipMessages: string[] = [];
|
|
4094
|
+
method.postTipMessage = jest.fn((message: string) => {
|
|
4095
|
+
tipMessages.push(message);
|
|
4096
|
+
});
|
|
4097
|
+
let successWasAnnouncedBeforeConfirmation = false;
|
|
4098
|
+
(method as any).waitForProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
4099
|
+
successWasAnnouncedBeforeConfirmation = tipMessages.includes('GoToBootloaderSuccess');
|
|
4100
|
+
return Promise.resolve({ mode: 'bootloader' });
|
|
4101
|
+
});
|
|
4102
|
+
|
|
4103
|
+
await (method as any).enterProtocolV2BootloaderMode();
|
|
4104
|
+
|
|
4105
|
+
expect(successWasAnnouncedBeforeConfirmation).toBe(false);
|
|
4106
|
+
expect(tipMessages).toEqual(['AutoRebootToBootloader', 'GoToBootloaderSuccess']);
|
|
4107
|
+
});
|
|
4108
|
+
|
|
4109
|
+
test('keeps Protocol V2 bootloader active before firmware transfer', async () => {
|
|
4110
|
+
const method = new FirmwareUpdateV4({
|
|
4111
|
+
id: 1,
|
|
4112
|
+
payload: {
|
|
4113
|
+
method: 'firmwareUpdateV4',
|
|
4114
|
+
},
|
|
4115
|
+
});
|
|
4116
|
+
(method as any).device = stubDevice({
|
|
4117
|
+
originalDescriptor: { id: 'usb-id', path: 'bootloader-path', protocolType: 'V2' },
|
|
4118
|
+
features: {
|
|
4119
|
+
deviceType: 'pro2',
|
|
4120
|
+
mode: 'bootloader',
|
|
4121
|
+
bootloaderMode: true,
|
|
4122
|
+
capabilities: [],
|
|
4123
|
+
},
|
|
4124
|
+
isBootloader: () => true,
|
|
4125
|
+
isRomloader: () => false,
|
|
4126
|
+
});
|
|
4127
|
+
(method as any).protocolV2Reboot = jest.fn();
|
|
4128
|
+
method.postTipMessage = jest.fn();
|
|
4129
|
+
|
|
4130
|
+
await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
|
|
4131
|
+
|
|
4132
|
+
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4133
|
+
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
4134
|
+
expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
|
|
4135
|
+
});
|
|
4136
|
+
|
|
3817
4137
|
test('keeps Protocol V2 romloader active before firmware transfer', async () => {
|
|
3818
4138
|
const method = new FirmwareUpdateV4({
|
|
3819
4139
|
id: 1,
|
|
@@ -3866,6 +4186,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3866
4186
|
},
|
|
3867
4187
|
});
|
|
3868
4188
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4189
|
+
const prepareResourceBundles = jest
|
|
4190
|
+
.spyOn(method as any, 'prepareProtocolV2ResourceBundles')
|
|
4191
|
+
.mockResolvedValue(undefined);
|
|
3869
4192
|
(method as any).protocolV2Reboot = jest.fn();
|
|
3870
4193
|
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
3871
4194
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
@@ -3878,6 +4201,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3878
4201
|
|
|
3879
4202
|
await method.run();
|
|
3880
4203
|
|
|
4204
|
+
expect(prepareResourceBundles).not.toHaveBeenCalled();
|
|
3881
4205
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
3882
4206
|
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
3883
4207
|
bootloaderBinary: null,
|
|
@@ -3897,6 +4221,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3897
4221
|
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
3898
4222
|
features: { bootloader_mode: false, capabilities: [] },
|
|
3899
4223
|
isBootloader: () => false,
|
|
4224
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4225
|
+
mode: 'bootloader',
|
|
4226
|
+
bootloaderMode: true,
|
|
4227
|
+
}),
|
|
3900
4228
|
});
|
|
3901
4229
|
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
3902
4230
|
if (requestType === 'DeviceInfoGet') {
|
|
@@ -3924,15 +4252,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3924
4252
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
3925
4253
|
(method as any).device.getCommands = () => ({ typedCall });
|
|
3926
4254
|
(method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
|
|
4255
|
+
(method as any).protocolV2ExpectedPath = 'app-path';
|
|
3927
4256
|
|
|
3928
4257
|
await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
|
|
3929
4258
|
|
|
3930
4259
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
|
|
3931
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3932
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3933
|
-
'DeviceInfoGet',
|
|
3934
|
-
'ProtocolInfoRequest',
|
|
3935
|
-
]);
|
|
4260
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
4261
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
3936
4262
|
});
|
|
3937
4263
|
|
|
3938
4264
|
test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
|
|
@@ -3960,12 +4286,16 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3960
4286
|
getCommands: () => commands,
|
|
3961
4287
|
mainId: 'usb-path',
|
|
3962
4288
|
});
|
|
3963
|
-
const cachedDevice = {
|
|
4289
|
+
const cachedDevice = {
|
|
4290
|
+
getConnectId: () => 'usb-path',
|
|
4291
|
+
getCurrentSerialNo: () => '',
|
|
4292
|
+
};
|
|
3964
4293
|
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
3965
4294
|
devices: {},
|
|
3966
4295
|
deviceList: [cachedDevice],
|
|
3967
4296
|
} as any);
|
|
3968
4297
|
(method as any).device = device;
|
|
4298
|
+
(method as any).protocolV2ExpectedPath = 'usb-path';
|
|
3969
4299
|
|
|
3970
4300
|
try {
|
|
3971
4301
|
await (method as any).reconnectProtocolV2Device();
|
|
@@ -3977,6 +4307,184 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3977
4307
|
expect(initialize).not.toHaveBeenCalled();
|
|
3978
4308
|
});
|
|
3979
4309
|
|
|
4310
|
+
test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
|
|
4311
|
+
const method = new FirmwareUpdateV4({
|
|
4312
|
+
id: 1,
|
|
4313
|
+
payload: {
|
|
4314
|
+
method: 'firmwareUpdateV4',
|
|
4315
|
+
},
|
|
4316
|
+
});
|
|
4317
|
+
const commands = { disposed: true, mainId: '' };
|
|
4318
|
+
const updateFromCache = jest.fn();
|
|
4319
|
+
const device = stubDevice({
|
|
4320
|
+
originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
|
|
4321
|
+
deviceConnector: {
|
|
4322
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
4323
|
+
descriptors: [
|
|
4324
|
+
{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
|
|
4325
|
+
{ id: 'usb-b', path: 'usb-b', protocolType: 'V2' },
|
|
4326
|
+
],
|
|
4327
|
+
}),
|
|
4328
|
+
},
|
|
4329
|
+
updateFromCache,
|
|
4330
|
+
hasDeviceAcquire: jest.fn(() => false),
|
|
4331
|
+
acquire: jest.fn().mockResolvedValue(undefined),
|
|
4332
|
+
commands,
|
|
4333
|
+
getCommands: () => commands,
|
|
4334
|
+
mainId: 'old-usb-path',
|
|
4335
|
+
});
|
|
4336
|
+
const otherDevice = {
|
|
4337
|
+
getConnectId: () => 'usb-a',
|
|
4338
|
+
getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
|
|
4339
|
+
};
|
|
4340
|
+
const expectedDevice = {
|
|
4341
|
+
getConnectId: () => 'usb-b',
|
|
4342
|
+
getCurrentSerialNo: () => 'PRO2-PHYSICAL-B',
|
|
4343
|
+
};
|
|
4344
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4345
|
+
devices: {},
|
|
4346
|
+
deviceList: [otherDevice, expectedDevice],
|
|
4347
|
+
} as any);
|
|
4348
|
+
(method as any).device = device;
|
|
4349
|
+
(method as any).protocolV2ExpectedPath = 'usb-path';
|
|
4350
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-B';
|
|
4351
|
+
|
|
4352
|
+
try {
|
|
4353
|
+
await (method as any).reconnectProtocolV2Device();
|
|
4354
|
+
} finally {
|
|
4355
|
+
getDevices.mockRestore();
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
expect(updateFromCache).toHaveBeenCalledWith(expectedDevice);
|
|
4359
|
+
});
|
|
4360
|
+
|
|
4361
|
+
test('rejects ambiguous Protocol V2 USB reconnect candidates without an identity match', async () => {
|
|
4362
|
+
const method = new FirmwareUpdateV4({
|
|
4363
|
+
id: 1,
|
|
4364
|
+
payload: {
|
|
4365
|
+
method: 'firmwareUpdateV4',
|
|
4366
|
+
},
|
|
4367
|
+
});
|
|
4368
|
+
const updateFromCache = jest.fn();
|
|
4369
|
+
const device = stubDevice({
|
|
4370
|
+
originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
|
|
4371
|
+
deviceConnector: {
|
|
4372
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
4373
|
+
descriptors: [
|
|
4374
|
+
{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
|
|
4375
|
+
{ id: 'usb-b', path: 'usb-b', protocolType: 'V2' },
|
|
4376
|
+
],
|
|
4377
|
+
}),
|
|
4378
|
+
},
|
|
4379
|
+
updateFromCache,
|
|
4380
|
+
});
|
|
4381
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4382
|
+
devices: {},
|
|
4383
|
+
deviceList: [
|
|
4384
|
+
{
|
|
4385
|
+
getConnectId: () => 'usb-a',
|
|
4386
|
+
getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
|
|
4387
|
+
},
|
|
4388
|
+
{
|
|
4389
|
+
getConnectId: () => 'usb-b',
|
|
4390
|
+
getCurrentSerialNo: () => 'PRO2-PHYSICAL-B',
|
|
4391
|
+
},
|
|
4392
|
+
],
|
|
4393
|
+
} as any);
|
|
4394
|
+
(method as any).device = device;
|
|
4395
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-C';
|
|
4396
|
+
|
|
4397
|
+
try {
|
|
4398
|
+
await expect((method as any).reconnectProtocolV2Device()).rejects.toBeDefined();
|
|
4399
|
+
} finally {
|
|
4400
|
+
getDevices.mockRestore();
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
expect(updateFromCache).not.toHaveBeenCalled();
|
|
4404
|
+
});
|
|
4405
|
+
|
|
4406
|
+
test('allows a Protocol V2 USB reconnect candidate without a serial only when its path matches', async () => {
|
|
4407
|
+
const method = new FirmwareUpdateV4({
|
|
4408
|
+
id: 1,
|
|
4409
|
+
payload: {
|
|
4410
|
+
method: 'firmwareUpdateV4',
|
|
4411
|
+
},
|
|
4412
|
+
});
|
|
4413
|
+
const commands = { disposed: true, mainId: '' };
|
|
4414
|
+
const updateFromCache = jest.fn();
|
|
4415
|
+
const device = stubDevice({
|
|
4416
|
+
originalDescriptor: { id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
|
|
4417
|
+
deviceConnector: {
|
|
4418
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
4419
|
+
descriptors: [{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' }],
|
|
4420
|
+
}),
|
|
4421
|
+
},
|
|
4422
|
+
updateFromCache,
|
|
4423
|
+
hasDeviceAcquire: jest.fn(() => false),
|
|
4424
|
+
acquire: jest.fn().mockResolvedValue(undefined),
|
|
4425
|
+
commands,
|
|
4426
|
+
getCommands: () => commands,
|
|
4427
|
+
mainId: 'old-usb-path',
|
|
4428
|
+
});
|
|
4429
|
+
const candidate = {
|
|
4430
|
+
getConnectId: () => 'usb-a',
|
|
4431
|
+
getCurrentSerialNo: () => '',
|
|
4432
|
+
};
|
|
4433
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4434
|
+
devices: {},
|
|
4435
|
+
deviceList: [candidate],
|
|
4436
|
+
} as any);
|
|
4437
|
+
(method as any).device = device;
|
|
4438
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-A';
|
|
4439
|
+
(method as any).protocolV2ExpectedPath = 'usb-a';
|
|
4440
|
+
|
|
4441
|
+
try {
|
|
4442
|
+
await (method as any).reconnectProtocolV2Device();
|
|
4443
|
+
} finally {
|
|
4444
|
+
getDevices.mockRestore();
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
expect(updateFromCache).toHaveBeenCalledWith(candidate);
|
|
4448
|
+
});
|
|
4449
|
+
|
|
4450
|
+
test('rejects a single Protocol V2 USB reconnect candidate with a different physical serial', async () => {
|
|
4451
|
+
const method = new FirmwareUpdateV4({
|
|
4452
|
+
id: 1,
|
|
4453
|
+
payload: {
|
|
4454
|
+
method: 'firmwareUpdateV4',
|
|
4455
|
+
},
|
|
4456
|
+
});
|
|
4457
|
+
const updateFromCache = jest.fn();
|
|
4458
|
+
const device = stubDevice({
|
|
4459
|
+
originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
|
|
4460
|
+
deviceConnector: {
|
|
4461
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
4462
|
+
descriptors: [{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' }],
|
|
4463
|
+
}),
|
|
4464
|
+
},
|
|
4465
|
+
updateFromCache,
|
|
4466
|
+
});
|
|
4467
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4468
|
+
devices: {},
|
|
4469
|
+
deviceList: [
|
|
4470
|
+
{
|
|
4471
|
+
getConnectId: () => 'usb-a',
|
|
4472
|
+
getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
|
|
4473
|
+
},
|
|
4474
|
+
],
|
|
4475
|
+
} as any);
|
|
4476
|
+
(method as any).device = device;
|
|
4477
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-B';
|
|
4478
|
+
|
|
4479
|
+
try {
|
|
4480
|
+
await expect((method as any).reconnectProtocolV2Device()).rejects.toBeDefined();
|
|
4481
|
+
} finally {
|
|
4482
|
+
getDevices.mockRestore();
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
expect(updateFromCache).not.toHaveBeenCalled();
|
|
4486
|
+
});
|
|
4487
|
+
|
|
3980
4488
|
test('reuses an acquired Protocol V2 USB session while polling reconnect readiness', async () => {
|
|
3981
4489
|
const method = new FirmwareUpdateV4({
|
|
3982
4490
|
id: 1,
|
|
@@ -4018,6 +4526,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4018
4526
|
deviceList: [{ getConnectId: () => 'usb-path' }],
|
|
4019
4527
|
} as any);
|
|
4020
4528
|
(method as any).device = device;
|
|
4529
|
+
(method as any).protocolV2ExpectedPath = 'usb-path';
|
|
4021
4530
|
|
|
4022
4531
|
try {
|
|
4023
4532
|
await (method as any).reconnectProtocolV2Device();
|
|
@@ -4048,6 +4557,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4048
4557
|
(method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
|
|
4049
4558
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4050
4559
|
(method as any).device = stubDevice({
|
|
4560
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4051
4561
|
getCommands: () => ({ typedCall }),
|
|
4052
4562
|
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4053
4563
|
deviceId: 'changed-device',
|
|
@@ -4071,7 +4581,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4071
4581
|
});
|
|
4072
4582
|
method.postTipMessage = jest.fn();
|
|
4073
4583
|
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4074
|
-
|
|
4584
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4585
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4586
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(false);
|
|
4075
4587
|
(method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
|
|
4076
4588
|
message: 'Device rebooted successfully',
|
|
4077
4589
|
});
|
|
@@ -4080,9 +4592,29 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4080
4592
|
|
|
4081
4593
|
expect(method.postTipMessage).not.toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
|
|
4082
4594
|
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4595
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
4083
4596
|
expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal);
|
|
4084
4597
|
});
|
|
4085
4598
|
|
|
4599
|
+
test('skips a second Normal reboot after Pro2 already returned to App mode', async () => {
|
|
4600
|
+
const method = new FirmwareUpdateV4({
|
|
4601
|
+
id: 1,
|
|
4602
|
+
payload: {
|
|
4603
|
+
method: 'firmwareUpdateV4',
|
|
4604
|
+
},
|
|
4605
|
+
});
|
|
4606
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4607
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4608
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4609
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4610
|
+
(method as any).protocolV2Reboot = jest.fn();
|
|
4611
|
+
|
|
4612
|
+
await (method as any).exitProtocolV2BootloaderToNormal();
|
|
4613
|
+
|
|
4614
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
4615
|
+
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4616
|
+
});
|
|
4617
|
+
|
|
4086
4618
|
test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
|
|
4087
4619
|
const method = new FirmwareUpdateV4({
|
|
4088
4620
|
id: 1,
|
|
@@ -4125,6 +4657,27 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4125
4657
|
});
|
|
4126
4658
|
});
|
|
4127
4659
|
|
|
4660
|
+
test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
|
|
4661
|
+
const method = new FirmwareUpdateV4({
|
|
4662
|
+
id: 1,
|
|
4663
|
+
payload: {
|
|
4664
|
+
method: 'firmwareUpdateV4',
|
|
4665
|
+
},
|
|
4666
|
+
});
|
|
4667
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
4668
|
+
|
|
4669
|
+
(method as any).device = stubDevice({
|
|
4670
|
+
getCommands: () => ({ typedCall }),
|
|
4671
|
+
});
|
|
4672
|
+
|
|
4673
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Bootloader)).resolves.toEqual({
|
|
4674
|
+
message: 'Device rebooted successfully',
|
|
4675
|
+
});
|
|
4676
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
4677
|
+
DeviceRebootType.Bootloader
|
|
4678
|
+
);
|
|
4679
|
+
});
|
|
4680
|
+
|
|
4128
4681
|
test('requires an explicit Protocol V2 update ACK before entering install state', async () => {
|
|
4129
4682
|
const method = new FirmwareUpdateV4({
|
|
4130
4683
|
id: 1,
|
|
@@ -4215,7 +4768,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4215
4768
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
4216
4769
|
]);
|
|
4217
4770
|
|
|
4218
|
-
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(
|
|
4771
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
|
|
4219
4772
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
4220
4773
|
'DeviceFirmwareUpdateStatusGet',
|
|
4221
4774
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -4253,6 +4806,46 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4253
4806
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4254
4807
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
|
|
4255
4808
|
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
4809
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4810
|
+
});
|
|
4811
|
+
|
|
4812
|
+
test('accepts an empty firmware status dump only after confirming normal mode', async () => {
|
|
4813
|
+
const method = new FirmwareUpdateV4({
|
|
4814
|
+
id: 1,
|
|
4815
|
+
payload: {
|
|
4816
|
+
method: 'firmwareUpdateV4',
|
|
4817
|
+
},
|
|
4818
|
+
});
|
|
4819
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
4820
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
4821
|
+
message: { records: [] },
|
|
4822
|
+
});
|
|
4823
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4824
|
+
let now = 0;
|
|
4825
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
4826
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
4827
|
+
now += 31 * 1000;
|
|
4828
|
+
callback();
|
|
4829
|
+
return 0 as any;
|
|
4830
|
+
}) as typeof setTimeout);
|
|
4831
|
+
|
|
4832
|
+
(method as any).device = stubDevice({
|
|
4833
|
+
getCommands: () => ({ typedCall }),
|
|
4834
|
+
});
|
|
4835
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4836
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4837
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4838
|
+
method.postProgressMessage = jest.fn();
|
|
4839
|
+
|
|
4840
|
+
await expect(
|
|
4841
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
4842
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
4843
|
+
])
|
|
4844
|
+
).resolves.toBeUndefined();
|
|
4845
|
+
|
|
4846
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
4847
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
4848
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4256
4849
|
});
|
|
4257
4850
|
|
|
4258
4851
|
test.each([
|
|
@@ -4535,6 +5128,151 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4535
5128
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4536
5129
|
});
|
|
4537
5130
|
|
|
5131
|
+
test('fails clearly when a requested target stays absent from the full status dump', async () => {
|
|
5132
|
+
const method = new FirmwareUpdateV4({
|
|
5133
|
+
id: 1,
|
|
5134
|
+
payload: {
|
|
5135
|
+
method: 'firmwareUpdateV4',
|
|
5136
|
+
},
|
|
5137
|
+
});
|
|
5138
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
5139
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5140
|
+
message: {
|
|
5141
|
+
records: [{ target_id: 4, status: 2 }],
|
|
5142
|
+
},
|
|
5143
|
+
});
|
|
5144
|
+
let now = 0;
|
|
5145
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
5146
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
5147
|
+
now += 31 * 1000;
|
|
5148
|
+
callback();
|
|
5149
|
+
return 0 as any;
|
|
5150
|
+
}) as typeof setTimeout);
|
|
5151
|
+
|
|
5152
|
+
(method as any).device = stubDevice({
|
|
5153
|
+
getCommands: () => ({ typedCall }),
|
|
5154
|
+
});
|
|
5155
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5156
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5157
|
+
method.postProgressMessage = jest.fn();
|
|
5158
|
+
|
|
5159
|
+
await expect(
|
|
5160
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5161
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
5162
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5163
|
+
])
|
|
5164
|
+
).rejects.toMatchObject({
|
|
5165
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
5166
|
+
message: expect.stringContaining('targetIds=3'),
|
|
5167
|
+
});
|
|
5168
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
5169
|
+
});
|
|
5170
|
+
|
|
5171
|
+
test('resets the missing-record grace period after a Pro2 reboot interruption', async () => {
|
|
5172
|
+
const method = new FirmwareUpdateV4({
|
|
5173
|
+
id: 1,
|
|
5174
|
+
payload: {
|
|
5175
|
+
method: 'firmwareUpdateV4',
|
|
5176
|
+
},
|
|
5177
|
+
});
|
|
5178
|
+
const typedCall = jest
|
|
5179
|
+
.fn()
|
|
5180
|
+
.mockResolvedValueOnce({
|
|
5181
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5182
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5183
|
+
})
|
|
5184
|
+
.mockRejectedValueOnce(new Error('Pro2 is rebooting'))
|
|
5185
|
+
.mockResolvedValueOnce({
|
|
5186
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5187
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5188
|
+
})
|
|
5189
|
+
.mockResolvedValueOnce({
|
|
5190
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5191
|
+
message: {
|
|
5192
|
+
records: [
|
|
5193
|
+
{ target_id: 3, status: 2 },
|
|
5194
|
+
{ target_id: 4, status: 2 },
|
|
5195
|
+
],
|
|
5196
|
+
},
|
|
5197
|
+
});
|
|
5198
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5199
|
+
let now = 0;
|
|
5200
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
5201
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
5202
|
+
now += 31 * 1000;
|
|
5203
|
+
callback();
|
|
5204
|
+
return 0 as any;
|
|
5205
|
+
}) as typeof setTimeout);
|
|
5206
|
+
|
|
5207
|
+
(method as any).device = stubDevice({
|
|
5208
|
+
getCommands: () => ({ typedCall }),
|
|
5209
|
+
});
|
|
5210
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5211
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5212
|
+
method.postProgressMessage = jest.fn();
|
|
5213
|
+
|
|
5214
|
+
await expect(
|
|
5215
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5216
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
5217
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5218
|
+
])
|
|
5219
|
+
).resolves.toBeUndefined();
|
|
5220
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
|
|
5221
|
+
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
5222
|
+
});
|
|
5223
|
+
|
|
5224
|
+
test('uses an eight-minute Pro2 install status window', async () => {
|
|
5225
|
+
const method = new FirmwareUpdateV4({
|
|
5226
|
+
id: 1,
|
|
5227
|
+
payload: {
|
|
5228
|
+
method: 'firmwareUpdateV4',
|
|
5229
|
+
},
|
|
5230
|
+
});
|
|
5231
|
+
jest
|
|
5232
|
+
.spyOn(Date, 'now')
|
|
5233
|
+
.mockReturnValueOnce(0)
|
|
5234
|
+
.mockReturnValue(8 * 60 * 1000);
|
|
5235
|
+
|
|
5236
|
+
await expect(
|
|
5237
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5238
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5239
|
+
])
|
|
5240
|
+
).rejects.toThrow('within 480s');
|
|
5241
|
+
});
|
|
5242
|
+
|
|
5243
|
+
test('uses a 90-second Pro2 bootloader reconnect window', async () => {
|
|
5244
|
+
const method = new FirmwareUpdateV4({
|
|
5245
|
+
id: 1,
|
|
5246
|
+
payload: {
|
|
5247
|
+
method: 'firmwareUpdateV4',
|
|
5248
|
+
},
|
|
5249
|
+
});
|
|
5250
|
+
jest
|
|
5251
|
+
.spyOn(Date, 'now')
|
|
5252
|
+
.mockReturnValueOnce(0)
|
|
5253
|
+
.mockReturnValue(90 * 1000);
|
|
5254
|
+
|
|
5255
|
+
await expect((method as any).waitForProtocolV2BootloaderMode()).rejects.toThrow('within 90s');
|
|
5256
|
+
});
|
|
5257
|
+
|
|
5258
|
+
test('uses a three-minute Pro2 final reconnect window', async () => {
|
|
5259
|
+
const method = new FirmwareUpdateV4({
|
|
5260
|
+
id: 1,
|
|
5261
|
+
payload: {
|
|
5262
|
+
method: 'firmwareUpdateV4',
|
|
5263
|
+
},
|
|
5264
|
+
});
|
|
5265
|
+
const waitForProtocolV2ReconnectAndFeatures = jest
|
|
5266
|
+
.fn()
|
|
5267
|
+
.mockRejectedValue(new Error('stop after capturing timeout'));
|
|
5268
|
+
(method as any).waitForProtocolV2ReconnectAndFeatures = waitForProtocolV2ReconnectAndFeatures;
|
|
5269
|
+
|
|
5270
|
+
await expect((method as any).waitForProtocolV2FinalFeatures()).rejects.toThrow(
|
|
5271
|
+
'stop after capturing timeout'
|
|
5272
|
+
);
|
|
5273
|
+
expect(waitForProtocolV2ReconnectAndFeatures).toHaveBeenCalledWith(3 * 60 * 1000);
|
|
5274
|
+
});
|
|
5275
|
+
|
|
4538
5276
|
test('uses SDK decoded enum names for Protocol V2 install polling', () => {
|
|
4539
5277
|
const method = new FirmwareUpdateV4({
|
|
4540
5278
|
id: 1,
|
|
@@ -4667,7 +5405,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4667
5405
|
path: true,
|
|
4668
5406
|
},
|
|
4669
5407
|
},
|
|
4670
|
-
{ timeoutMs:
|
|
5408
|
+
{ timeoutMs: 15000 }
|
|
4671
5409
|
);
|
|
4672
5410
|
});
|
|
4673
5411
|
|
|
@@ -5079,33 +5817,42 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5079
5817
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5080
5818
|
});
|
|
5081
5819
|
|
|
5082
|
-
test('
|
|
5820
|
+
test('does not download Pro2 firmware components that App did not select', async () => {
|
|
5083
5821
|
const method = new FirmwareUpdateV4({
|
|
5084
5822
|
id: 1,
|
|
5085
5823
|
payload: {
|
|
5086
5824
|
method: 'firmwareUpdateV4',
|
|
5087
5825
|
platform: 'web',
|
|
5088
|
-
targetsToUpdate: ['resource'],
|
|
5089
5826
|
},
|
|
5090
5827
|
});
|
|
5091
5828
|
method.init();
|
|
5092
5829
|
|
|
5830
|
+
const getSysResourceBinarySpy = jest
|
|
5831
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5832
|
+
.mockResolvedValue({ binary: new Uint8Array([1]).buffer });
|
|
5093
5833
|
const getFirmwareLatestReleaseSpy = jest
|
|
5094
5834
|
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5095
5835
|
.mockReturnValue({
|
|
5096
5836
|
required: false,
|
|
5097
5837
|
version: [1, 0, 0],
|
|
5098
|
-
url: '',
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
headerHash: 'cd'.repeat(64),
|
|
5838
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
5839
|
+
bootloaderVersion: [1, 0, 0],
|
|
5840
|
+
upgradeType: 'payload-package-set',
|
|
5841
|
+
installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
|
|
5842
|
+
components: {
|
|
5843
|
+
bootloader: {
|
|
5844
|
+
target: 'BOOTLOADER',
|
|
5845
|
+
url: 'https://example.com/bootloader.pp.bin',
|
|
5107
5846
|
},
|
|
5108
|
-
|
|
5847
|
+
applicationP1: {
|
|
5848
|
+
target: 'APPLICATION_P1',
|
|
5849
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
5850
|
+
},
|
|
5851
|
+
applicationP2: {
|
|
5852
|
+
target: 'APPLICATION_P2',
|
|
5853
|
+
url: 'https://example.com/applicationP2.pp.bin',
|
|
5854
|
+
},
|
|
5855
|
+
},
|
|
5109
5856
|
fingerprint: '',
|
|
5110
5857
|
changelog: {
|
|
5111
5858
|
'zh-CN': '',
|
|
@@ -5113,78 +5860,69 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5113
5860
|
},
|
|
5114
5861
|
});
|
|
5115
5862
|
|
|
5116
|
-
const
|
|
5863
|
+
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
|
|
5117
5864
|
deviceType: 'pro2',
|
|
5118
5865
|
firmwareVersion: '1.0.0',
|
|
5866
|
+
bootloaderVersion: '1.0.0',
|
|
5119
5867
|
capabilities: [],
|
|
5120
5868
|
});
|
|
5121
5869
|
|
|
5122
|
-
expect(
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
version: [2, 0, 0],
|
|
5129
|
-
payloadHash: 'ab'.repeat(64),
|
|
5130
|
-
headerHash: 'cd'.repeat(64),
|
|
5131
|
-
},
|
|
5132
|
-
]);
|
|
5133
|
-
expect(bundles[0].binary.byteLength).toBe(0);
|
|
5870
|
+
expect(remoteBinaries).toEqual({
|
|
5871
|
+
bootloaderBinary: null,
|
|
5872
|
+
fwBinaryMap: [],
|
|
5873
|
+
installItems: [],
|
|
5874
|
+
});
|
|
5875
|
+
expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
|
|
5134
5876
|
|
|
5877
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5135
5878
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5136
5879
|
});
|
|
5137
5880
|
|
|
5138
|
-
test('
|
|
5881
|
+
test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
|
|
5139
5882
|
const method = new FirmwareUpdateV4({
|
|
5140
5883
|
id: 1,
|
|
5141
5884
|
payload: {
|
|
5142
5885
|
method: 'firmwareUpdateV4',
|
|
5143
5886
|
platform: 'web',
|
|
5144
|
-
targetsToUpdate: ['resource'],
|
|
5145
5887
|
},
|
|
5146
5888
|
});
|
|
5147
5889
|
method.init();
|
|
5148
5890
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5149
5891
|
|
|
5150
|
-
const
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
resourceBundles: [
|
|
5158
|
-
{
|
|
5159
|
-
name: 'images',
|
|
5160
|
-
url: 'https://example.com/images.okpkg',
|
|
5161
|
-
devicePath: 'vol0:/resource/../images.okpkg',
|
|
5162
|
-
},
|
|
5163
|
-
],
|
|
5164
|
-
fingerprint: '',
|
|
5165
|
-
changelog: {
|
|
5166
|
-
'zh-CN': '',
|
|
5167
|
-
'en-US': '',
|
|
5892
|
+
const remoteBinaries = {
|
|
5893
|
+
bootloaderBinary: new Uint8Array([1]).buffer,
|
|
5894
|
+
fwBinaryMap: [
|
|
5895
|
+
{
|
|
5896
|
+
fileName: 'application_p1.bin',
|
|
5897
|
+
binary: new Uint8Array([2]).buffer,
|
|
5898
|
+
targetId: 3,
|
|
5168
5899
|
},
|
|
5169
|
-
|
|
5900
|
+
],
|
|
5901
|
+
};
|
|
5170
5902
|
|
|
5171
5903
|
(method as any).device = stubDevice({
|
|
5172
5904
|
originalDescriptor: { protocolType: 'V2' },
|
|
5173
|
-
features: { deviceType: 'pro2', firmwareVersion: '
|
|
5905
|
+
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
5174
5906
|
});
|
|
5175
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(
|
|
5176
|
-
|
|
5177
|
-
|
|
5907
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
|
|
5908
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
5909
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
5910
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5911
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
5912
|
+
bootloaderVersion: '1.0.0',
|
|
5913
|
+
bleVersion: '0.0.0',
|
|
5914
|
+
firmwareVersion: '1.0.0',
|
|
5178
5915
|
});
|
|
5179
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
5180
5916
|
method.postTipMessage = jest.fn();
|
|
5181
5917
|
|
|
5182
|
-
await
|
|
5183
|
-
expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
|
|
5184
|
-
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
5918
|
+
await method.run();
|
|
5185
5919
|
|
|
5186
|
-
|
|
5187
|
-
|
|
5920
|
+
expect(forceReloadDataSpy).toHaveBeenCalledTimes(1);
|
|
5921
|
+
expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
|
|
5922
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
5923
|
+
bootloaderBinary: remoteBinaries.bootloaderBinary,
|
|
5924
|
+
fwBinaryMap: remoteBinaries.fwBinaryMap,
|
|
5925
|
+
});
|
|
5188
5926
|
});
|
|
5189
5927
|
|
|
5190
5928
|
test('rejects prepared RESC bundle paths before opening an artifact source', async () => {
|
|
@@ -5239,273 +5977,34 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5239
5977
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5240
5978
|
});
|
|
5241
5979
|
|
|
5242
|
-
test('
|
|
5980
|
+
test('stops a remote update before reboot when the latest config cannot be refreshed', async () => {
|
|
5243
5981
|
const method = new FirmwareUpdateV4({
|
|
5244
5982
|
id: 1,
|
|
5245
5983
|
payload: {
|
|
5246
5984
|
method: 'firmwareUpdateV4',
|
|
5247
5985
|
platform: 'web',
|
|
5248
|
-
targetsToUpdate: ['resource'],
|
|
5249
5986
|
},
|
|
5250
5987
|
});
|
|
5251
5988
|
method.init();
|
|
5252
5989
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5253
|
-
|
|
5254
|
-
const order: string[] = [];
|
|
5255
|
-
const resourceBinary = buildOkppHeader().buffer as ArrayBuffer;
|
|
5256
|
-
const getSysResourceBinarySpy = jest
|
|
5257
|
-
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5258
|
-
.mockImplementation(() => {
|
|
5259
|
-
order.push('download');
|
|
5260
|
-
return Promise.resolve({ binary: resourceBinary });
|
|
5261
|
-
});
|
|
5262
|
-
const getFirmwareLatestReleaseSpy = jest
|
|
5263
|
-
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5264
|
-
.mockReturnValue({
|
|
5265
|
-
required: false,
|
|
5266
|
-
version: [1, 0, 0],
|
|
5267
|
-
url: '',
|
|
5268
|
-
resourceBundles: [
|
|
5269
|
-
{
|
|
5270
|
-
name: 'images',
|
|
5271
|
-
url: 'https://example.com/images.okpkg',
|
|
5272
|
-
devicePath: ' VOL0:/resource/images/images.okpkg ',
|
|
5273
|
-
},
|
|
5274
|
-
],
|
|
5275
|
-
fingerprint: '',
|
|
5276
|
-
changelog: {
|
|
5277
|
-
'zh-CN': '',
|
|
5278
|
-
'en-US': '',
|
|
5279
|
-
},
|
|
5280
|
-
});
|
|
5281
|
-
|
|
5282
|
-
(method as any).device = stubDevice({
|
|
5283
|
-
originalDescriptor: { protocolType: 'V2' },
|
|
5284
|
-
features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
|
|
5285
|
-
});
|
|
5286
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
5287
|
-
bootloaderBinary: null,
|
|
5288
|
-
fwBinaryMap: [],
|
|
5289
|
-
});
|
|
5290
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
5291
|
-
order.push('bootloader');
|
|
5292
|
-
return Promise.resolve(true);
|
|
5293
|
-
});
|
|
5294
|
-
const executeProtocolV2UpdateSpy = jest.spyOn(method as any, 'executeProtocolV2Update');
|
|
5295
|
-
(method as any).executeProtocolV2TransferPhase = jest.fn().mockResolvedValue(undefined);
|
|
5296
|
-
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue(undefined);
|
|
5297
|
-
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5298
|
-
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
5299
|
-
bootloaderVersion: '1.0.0',
|
|
5300
|
-
bleVersion: '0.0.0',
|
|
5301
|
-
firmwareVersion: '1.0.0',
|
|
5302
|
-
});
|
|
5303
|
-
method.postTipMessage = jest.fn();
|
|
5304
|
-
|
|
5305
|
-
await method.run();
|
|
5306
|
-
|
|
5307
|
-
expect(order).toEqual(['download', 'bootloader']);
|
|
5308
|
-
expect(executeProtocolV2UpdateSpy).toHaveBeenCalledWith({
|
|
5309
|
-
bootloaderBinary: null,
|
|
5310
|
-
fwBinaryMap: [],
|
|
5311
|
-
resourceBundles: [
|
|
5312
|
-
{
|
|
5313
|
-
name: 'images',
|
|
5314
|
-
binary: resourceBinary,
|
|
5315
|
-
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5316
|
-
url: 'https://example.com/images.okpkg',
|
|
5317
|
-
},
|
|
5318
|
-
],
|
|
5319
|
-
});
|
|
5320
|
-
|
|
5321
|
-
executeProtocolV2UpdateSpy.mockRestore();
|
|
5322
|
-
getSysResourceBinarySpy.mockRestore();
|
|
5323
|
-
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5324
|
-
});
|
|
5325
|
-
|
|
5326
|
-
test('rejects a remote RESC bundle without an OKPP header when metadata is absent', async () => {
|
|
5327
|
-
const method = new FirmwareUpdateV4({
|
|
5328
|
-
id: 1,
|
|
5329
|
-
payload: {
|
|
5330
|
-
method: 'firmwareUpdateV4',
|
|
5331
|
-
},
|
|
5332
|
-
});
|
|
5333
|
-
method.init();
|
|
5334
|
-
const getSysResourceBinarySpy = jest
|
|
5335
|
-
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5336
|
-
.mockResolvedValue({ binary: new Uint8Array([1, 2, 3]).buffer });
|
|
5337
|
-
|
|
5338
|
-
await expect(
|
|
5339
|
-
(method as any).materializeProtocolV2ResourceBundles([
|
|
5340
|
-
{
|
|
5341
|
-
name: 'images',
|
|
5342
|
-
binary: new ArrayBuffer(0),
|
|
5343
|
-
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5344
|
-
url: 'https://example.com/images.okpkg',
|
|
5345
|
-
},
|
|
5346
|
-
])
|
|
5347
|
-
).rejects.toThrow('Invalid Protocol V2 RESC bundle header: images');
|
|
5348
|
-
|
|
5349
|
-
getSysResourceBinarySpy.mockRestore();
|
|
5350
|
-
});
|
|
5351
|
-
|
|
5352
|
-
test('rejects an empty remote RESC bundle before entering bootloader', async () => {
|
|
5353
|
-
const method = new FirmwareUpdateV4({
|
|
5354
|
-
id: 1,
|
|
5355
|
-
payload: {
|
|
5356
|
-
method: 'firmwareUpdateV4',
|
|
5357
|
-
platform: 'web',
|
|
5358
|
-
targetsToUpdate: ['resource'],
|
|
5359
|
-
},
|
|
5360
|
-
});
|
|
5361
|
-
method.init();
|
|
5362
|
-
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5363
|
-
|
|
5364
|
-
const getSysResourceBinarySpy = jest
|
|
5365
|
-
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5366
|
-
.mockResolvedValue({ binary: new ArrayBuffer(0) });
|
|
5367
|
-
const getFirmwareLatestReleaseSpy = jest
|
|
5368
|
-
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5369
|
-
.mockReturnValue({
|
|
5370
|
-
required: false,
|
|
5371
|
-
version: [1, 0, 0],
|
|
5372
|
-
url: '',
|
|
5373
|
-
resourceBundles: [
|
|
5374
|
-
{
|
|
5375
|
-
name: 'images',
|
|
5376
|
-
url: 'https://example.com/images.okpkg',
|
|
5377
|
-
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5378
|
-
},
|
|
5379
|
-
],
|
|
5380
|
-
fingerprint: '',
|
|
5381
|
-
changelog: {
|
|
5382
|
-
'zh-CN': '',
|
|
5383
|
-
'en-US': '',
|
|
5384
|
-
},
|
|
5385
|
-
});
|
|
5386
5990
|
(method as any).device = stubDevice({
|
|
5387
5991
|
originalDescriptor: { protocolType: 'V2' },
|
|
5388
|
-
features: { deviceType: 'pro2', firmwareVersion: '
|
|
5389
|
-
});
|
|
5390
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
5391
|
-
bootloaderBinary: null,
|
|
5392
|
-
fwBinaryMap: [],
|
|
5992
|
+
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
5393
5993
|
});
|
|
5994
|
+
forceReloadDataSpy.mockRejectedValue(
|
|
5995
|
+
ERRORS.TypedError(HardwareErrorCode.NetworkError, 'latest config unavailable')
|
|
5996
|
+
);
|
|
5997
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
5394
5998
|
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
5395
5999
|
method.postTipMessage = jest.fn();
|
|
5396
6000
|
|
|
5397
|
-
await expect(method.run()).rejects.
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
getSysResourceBinarySpy.mockRestore();
|
|
5401
|
-
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5402
|
-
});
|
|
5403
|
-
|
|
5404
|
-
test('does not download Pro2 firmware components that App did not select', async () => {
|
|
5405
|
-
const method = new FirmwareUpdateV4({
|
|
5406
|
-
id: 1,
|
|
5407
|
-
payload: {
|
|
5408
|
-
method: 'firmwareUpdateV4',
|
|
5409
|
-
platform: 'web',
|
|
5410
|
-
},
|
|
5411
|
-
});
|
|
5412
|
-
method.init();
|
|
5413
|
-
|
|
5414
|
-
const getSysResourceBinarySpy = jest
|
|
5415
|
-
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5416
|
-
.mockResolvedValue({ binary: new Uint8Array([1]).buffer });
|
|
5417
|
-
const getFirmwareLatestReleaseSpy = jest
|
|
5418
|
-
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5419
|
-
.mockReturnValue({
|
|
5420
|
-
required: false,
|
|
5421
|
-
version: [1, 0, 0],
|
|
5422
|
-
url: 'https://example.com/applicationP1.pp.bin',
|
|
5423
|
-
bootloaderVersion: [1, 0, 0],
|
|
5424
|
-
upgradeType: 'payload-package-set',
|
|
5425
|
-
installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
|
|
5426
|
-
components: {
|
|
5427
|
-
bootloader: {
|
|
5428
|
-
target: 'BOOTLOADER',
|
|
5429
|
-
url: 'https://example.com/bootloader.pp.bin',
|
|
5430
|
-
},
|
|
5431
|
-
applicationP1: {
|
|
5432
|
-
target: 'APPLICATION_P1',
|
|
5433
|
-
url: 'https://example.com/applicationP1.pp.bin',
|
|
5434
|
-
},
|
|
5435
|
-
applicationP2: {
|
|
5436
|
-
target: 'APPLICATION_P2',
|
|
5437
|
-
url: 'https://example.com/applicationP2.pp.bin',
|
|
5438
|
-
},
|
|
5439
|
-
},
|
|
5440
|
-
fingerprint: '',
|
|
5441
|
-
changelog: {
|
|
5442
|
-
'zh-CN': '',
|
|
5443
|
-
'en-US': '',
|
|
5444
|
-
},
|
|
5445
|
-
});
|
|
5446
|
-
|
|
5447
|
-
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
|
|
5448
|
-
deviceType: 'pro2',
|
|
5449
|
-
firmwareVersion: '1.0.0',
|
|
5450
|
-
bootloaderVersion: '1.0.0',
|
|
5451
|
-
capabilities: [],
|
|
5452
|
-
});
|
|
5453
|
-
|
|
5454
|
-
expect(remoteBinaries).toEqual({
|
|
5455
|
-
bootloaderBinary: null,
|
|
5456
|
-
fwBinaryMap: [],
|
|
5457
|
-
installItems: [],
|
|
5458
|
-
});
|
|
5459
|
-
expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
|
|
5460
|
-
|
|
5461
|
-
getSysResourceBinarySpy.mockRestore();
|
|
5462
|
-
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5463
|
-
});
|
|
5464
|
-
|
|
5465
|
-
test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
|
|
5466
|
-
const method = new FirmwareUpdateV4({
|
|
5467
|
-
id: 1,
|
|
5468
|
-
payload: {
|
|
5469
|
-
method: 'firmwareUpdateV4',
|
|
5470
|
-
platform: 'web',
|
|
5471
|
-
},
|
|
5472
|
-
});
|
|
5473
|
-
method.init();
|
|
5474
|
-
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5475
|
-
|
|
5476
|
-
const remoteBinaries = {
|
|
5477
|
-
bootloaderBinary: new Uint8Array([1]).buffer,
|
|
5478
|
-
fwBinaryMap: [
|
|
5479
|
-
{
|
|
5480
|
-
fileName: 'application_p1.bin',
|
|
5481
|
-
binary: new Uint8Array([2]).buffer,
|
|
5482
|
-
targetId: 3,
|
|
5483
|
-
},
|
|
5484
|
-
],
|
|
5485
|
-
};
|
|
5486
|
-
|
|
5487
|
-
(method as any).device = stubDevice({
|
|
5488
|
-
originalDescriptor: { protocolType: 'V2' },
|
|
5489
|
-
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
5490
|
-
});
|
|
5491
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
|
|
5492
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
5493
|
-
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
5494
|
-
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5495
|
-
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
5496
|
-
bootloaderVersion: '1.0.0',
|
|
5497
|
-
bleVersion: '0.0.0',
|
|
5498
|
-
firmwareVersion: '1.0.0',
|
|
6001
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
6002
|
+
errorCode: HardwareErrorCode.NetworkError,
|
|
6003
|
+
message: 'latest config unavailable',
|
|
5499
6004
|
});
|
|
5500
|
-
method.postTipMessage = jest.fn();
|
|
5501
|
-
|
|
5502
|
-
await method.run();
|
|
5503
6005
|
|
|
5504
|
-
expect((method as any).prepareRemoteProtocolV2Binaries).
|
|
5505
|
-
expect((method as any).
|
|
5506
|
-
bootloaderBinary: remoteBinaries.bootloaderBinary,
|
|
5507
|
-
fwBinaryMap: remoteBinaries.fwBinaryMap,
|
|
5508
|
-
});
|
|
6006
|
+
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6007
|
+
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
5509
6008
|
});
|
|
5510
6009
|
|
|
5511
6010
|
test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
|
|
@@ -5544,14 +6043,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5544
6043
|
});
|
|
5545
6044
|
});
|
|
5546
6045
|
|
|
5547
|
-
test('treats manual resource
|
|
6046
|
+
test('treats manual resource files as explicit payload without remote firmware auto-fill', async () => {
|
|
5548
6047
|
const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
|
|
5549
6048
|
const method = new FirmwareUpdateV4({
|
|
5550
6049
|
id: 1,
|
|
5551
6050
|
payload: {
|
|
5552
6051
|
method: 'firmwareUpdateV4',
|
|
5553
6052
|
platform: 'web',
|
|
5554
|
-
|
|
6053
|
+
resourceFiles: [
|
|
5555
6054
|
{
|
|
5556
6055
|
binary: resourceBundle,
|
|
5557
6056
|
devicePath: ' VOL0:/resource/images/images.okpkg ',
|
|
@@ -5599,7 +6098,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5599
6098
|
payload: {
|
|
5600
6099
|
method: 'firmwareUpdateV4',
|
|
5601
6100
|
platform: 'web',
|
|
5602
|
-
|
|
6101
|
+
resourceFiles: [
|
|
5603
6102
|
{
|
|
5604
6103
|
binary: new Uint8Array([1, 2, 3]).buffer,
|
|
5605
6104
|
devicePath: 'vol2:/resource/images/images.okpkg',
|
|
@@ -5618,7 +6117,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5618
6117
|
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
5619
6118
|
method.postTipMessage = jest.fn();
|
|
5620
6119
|
|
|
5621
|
-
await expect(method.run()).rejects.toThrow('
|
|
6120
|
+
await expect(method.run()).rejects.toThrow('resourceFiles[0].devicePath');
|
|
5622
6121
|
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
5623
6122
|
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
5624
6123
|
});
|
|
@@ -5680,8 +6179,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5680
6179
|
|
|
5681
6180
|
(method as any).device = stubDevice({
|
|
5682
6181
|
getCommands: () => ({ typedCall }),
|
|
6182
|
+
getCurrentDeviceType: () => 'pro2',
|
|
6183
|
+
toMessageObject: () => ({ connectId: 'firmware-device' }),
|
|
5683
6184
|
});
|
|
5684
|
-
method.
|
|
6185
|
+
method.postMessage = jest.fn();
|
|
5685
6186
|
|
|
5686
6187
|
const source = await openFirmwareByteSource({
|
|
5687
6188
|
binary: new Uint8Array(4097).buffer,
|
|
@@ -5703,6 +6204,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5703
6204
|
expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
|
|
5704
6205
|
expect(writePayloads.every(payload => payload.append === false)).toBe(true);
|
|
5705
6206
|
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
|
|
6207
|
+
expect(method.postMessage).toHaveBeenLastCalledWith({
|
|
6208
|
+
event: 'UI_EVENT',
|
|
6209
|
+
type: UI_REQUEST.FIRMWARE_PROGRESS,
|
|
6210
|
+
payload: expect.objectContaining({
|
|
6211
|
+
progress: 99,
|
|
6212
|
+
progressType: 'transferData',
|
|
6213
|
+
transferredBytes: 4097,
|
|
6214
|
+
totalBytes: 4097,
|
|
6215
|
+
elapsedMs: expect.any(Number),
|
|
6216
|
+
}),
|
|
6217
|
+
});
|
|
5706
6218
|
});
|
|
5707
6219
|
|
|
5708
6220
|
test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => {
|
|
@@ -5861,6 +6373,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5861
6373
|
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
|
|
5862
6374
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
|
|
5863
6375
|
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
|
|
6376
|
+
expect(typedCall.mock.calls.map(call => call[3])).toEqual([
|
|
6377
|
+
{ writeWithResponse: true },
|
|
6378
|
+
{ writeWithResponse: true },
|
|
6379
|
+
]);
|
|
5864
6380
|
});
|
|
5865
6381
|
|
|
5866
6382
|
test('ends device confirmation and starts install progress only after Protocol V2 ACK', async () => {
|
|
@@ -6028,7 +6544,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6028
6544
|
expect(typedCall).not.toHaveBeenCalled();
|
|
6029
6545
|
});
|
|
6030
6546
|
|
|
6031
|
-
test.each([0, 2])(
|
|
6547
|
+
test.each([0, 1, 2])(
|
|
6032
6548
|
'rejects firmware target %s because the Pro2 bootloader cannot install it',
|
|
6033
6549
|
async targetId => {
|
|
6034
6550
|
const typedCall = jest.fn();
|
|
@@ -6061,8 +6577,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6061
6577
|
targets: [
|
|
6062
6578
|
{
|
|
6063
6579
|
target_id: undefined,
|
|
6064
|
-
targetId:
|
|
6065
|
-
path: 'vol0:
|
|
6580
|
+
targetId: 3,
|
|
6581
|
+
path: 'vol0:bootloader.okpkg',
|
|
6066
6582
|
},
|
|
6067
6583
|
],
|
|
6068
6584
|
} as any,
|
|
@@ -6074,7 +6590,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6074
6590
|
|
|
6075
6591
|
await method.run();
|
|
6076
6592
|
expect(typedCall.mock.calls[0][2]).toEqual({
|
|
6077
|
-
targets: [{ target_id:
|
|
6593
|
+
targets: [{ target_id: 3, path: 'vol0:bootloader.okpkg' }],
|
|
6078
6594
|
});
|
|
6079
6595
|
});
|
|
6080
6596
|
|
|
@@ -6139,19 +6655,77 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6139
6655
|
});
|
|
6140
6656
|
|
|
6141
6657
|
describe('Protocol V2 firmware reconnect identity', () => {
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6658
|
+
const createResourceFilesystemTypedCall = (
|
|
6659
|
+
stable: Array<{ type: string; size: number; headerHash: string }>,
|
|
6660
|
+
missingTypes: string[] = []
|
|
6661
|
+
) => {
|
|
6662
|
+
const missing = new Set(missingTypes);
|
|
6663
|
+
const resourceByPath = new Map(
|
|
6664
|
+
stable.map(resource => [
|
|
6665
|
+
PROTOCOL_V2_RESOURCE_DEVICE_PATHS[
|
|
6666
|
+
resource.type as keyof typeof PROTOCOL_V2_RESOURCE_DEVICE_PATHS
|
|
6667
|
+
],
|
|
6668
|
+
resource,
|
|
6669
|
+
])
|
|
6670
|
+
);
|
|
6671
|
+
return jest.fn((requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
6672
|
+
if (requestType === 'ResourceInventoryGet') {
|
|
6673
|
+
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
6674
|
+
}
|
|
6675
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
6676
|
+
const resource = resourceByPath.get(payload.path);
|
|
6677
|
+
const exists = Boolean(resource && !missing.has(resource.type));
|
|
6678
|
+
return {
|
|
6679
|
+
message: {
|
|
6680
|
+
exist: exists,
|
|
6681
|
+
directory: false,
|
|
6682
|
+
size: exists ? resource?.size : 0,
|
|
6683
|
+
},
|
|
6684
|
+
};
|
|
6685
|
+
}
|
|
6686
|
+
if (requestType === 'FilesystemFileRead') {
|
|
6687
|
+
const resource = resourceByPath.get(payload.file.path);
|
|
6688
|
+
if (!resource || missing.has(resource.type)) throw new Error('missing resource');
|
|
6689
|
+
const header = new Uint8Array(0x52a0);
|
|
6690
|
+
const view = new DataView(header.buffer);
|
|
6691
|
+
'OKPP'.split('').forEach((char, index) => {
|
|
6692
|
+
header[index] = char.charCodeAt(0);
|
|
6693
|
+
});
|
|
6694
|
+
'RESC'.split('').forEach((char, index) => {
|
|
6695
|
+
header[0x08 + index] = char.charCodeAt(0);
|
|
6696
|
+
});
|
|
6697
|
+
view.setUint32(0x0c, header.byteLength, true);
|
|
6698
|
+
for (let index = 0; index < resource.headerHash.length / 2; index++) {
|
|
6699
|
+
header[0x240 + index] = Number.parseInt(
|
|
6700
|
+
resource.headerHash.slice(index * 2, index * 2 + 2),
|
|
6701
|
+
16
|
|
6702
|
+
);
|
|
6703
|
+
}
|
|
6704
|
+
const offset = Number(payload.file.offset);
|
|
6705
|
+
const chunkLength = Number(payload.chunk_len);
|
|
6706
|
+
return {
|
|
6707
|
+
message: {
|
|
6708
|
+
data: header.slice(offset, offset + chunkLength),
|
|
6709
|
+
},
|
|
6710
|
+
};
|
|
6711
|
+
}
|
|
6712
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
6713
|
+
});
|
|
6714
|
+
};
|
|
6715
|
+
|
|
6716
|
+
test('keeps the host plan identity as the V4 reconnect anchor', () => {
|
|
6717
|
+
const method = new FirmwareUpdateV4({
|
|
6718
|
+
id: 1,
|
|
6719
|
+
payload: {
|
|
6720
|
+
method: 'firmwareUpdateV4',
|
|
6721
|
+
platform: 'native',
|
|
6722
|
+
expectedDeviceId: 'persisted-device-id',
|
|
6723
|
+
},
|
|
6724
|
+
});
|
|
6725
|
+
|
|
6726
|
+
method.init();
|
|
6727
|
+
|
|
6728
|
+
expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
|
|
6155
6729
|
});
|
|
6156
6730
|
|
|
6157
6731
|
test('rejects a different physical serial before firmware transfer resumes', () => {
|
|
@@ -6252,6 +6826,197 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6252
6826
|
expect(readChunkLengths.length).toBeGreaterThan(1);
|
|
6253
6827
|
expect(Math.max(...readChunkLengths)).toBe(900);
|
|
6254
6828
|
});
|
|
6829
|
+
|
|
6830
|
+
test('uses filesystem reads instead of ResourceInventoryGet for resource comparison', async () => {
|
|
6831
|
+
const method = new FirmwareUpdateV4({
|
|
6832
|
+
id: 1,
|
|
6833
|
+
payload: {
|
|
6834
|
+
method: 'firmwareUpdateV4',
|
|
6835
|
+
platform: 'web',
|
|
6836
|
+
targetsToUpdate: ['resource'],
|
|
6837
|
+
},
|
|
6838
|
+
});
|
|
6839
|
+
method.init();
|
|
6840
|
+
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
6841
|
+
(type, index) => ({
|
|
6842
|
+
type,
|
|
6843
|
+
url: `https://example.com/${type}.okpkg`,
|
|
6844
|
+
size: 0x52a0 + index + 1,
|
|
6845
|
+
fileHash: 'a'.repeat(64),
|
|
6846
|
+
headerHash: index.toString(16).padStart(128, '0'),
|
|
6847
|
+
})
|
|
6848
|
+
);
|
|
6849
|
+
const typedCall = createResourceFilesystemTypedCall(stable);
|
|
6850
|
+
(method as any).device = stubDevice({
|
|
6851
|
+
getCommands: () => ({ typedCall }),
|
|
6852
|
+
getCurrentDeviceType: () => 'pro2',
|
|
6853
|
+
});
|
|
6854
|
+
const resourcesSpy = jest
|
|
6855
|
+
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
6856
|
+
.mockReturnValue(stable as any);
|
|
6857
|
+
(method as any).downloadProtocolV2Resource = jest.fn();
|
|
6858
|
+
|
|
6859
|
+
await expect((method as any).prepareProtocolV2ResourceBundles()).resolves.toEqual([]);
|
|
6860
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
6861
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
6862
|
+
expect((method as any).downloadProtocolV2Resource).not.toHaveBeenCalled();
|
|
6863
|
+
resourcesSpy.mockRestore();
|
|
6864
|
+
});
|
|
6865
|
+
|
|
6866
|
+
test('downloads only changed resources from loader inventory', async () => {
|
|
6867
|
+
const method = new FirmwareUpdateV4({
|
|
6868
|
+
id: 1,
|
|
6869
|
+
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
6870
|
+
});
|
|
6871
|
+
method.init();
|
|
6872
|
+
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
6873
|
+
(type, index) => ({
|
|
6874
|
+
type,
|
|
6875
|
+
url: `https://example.com/${type}.okpkg`,
|
|
6876
|
+
size: 0x52a0 + index + 1,
|
|
6877
|
+
fileHash: 'a'.repeat(64),
|
|
6878
|
+
headerHash: index.toString(16).padStart(128, '0'),
|
|
6879
|
+
})
|
|
6880
|
+
);
|
|
6881
|
+
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
6882
|
+
(method as any).device = stubDevice({
|
|
6883
|
+
getCommands: () => ({ typedCall }),
|
|
6884
|
+
getCurrentDeviceType: () => 'pro2',
|
|
6885
|
+
});
|
|
6886
|
+
const resourcesSpy = jest
|
|
6887
|
+
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
6888
|
+
.mockReturnValue(stable as any);
|
|
6889
|
+
(method as any).downloadProtocolV2Resource = jest.fn(resource =>
|
|
6890
|
+
Promise.resolve({
|
|
6891
|
+
name: `${resource.type}.okpkg`,
|
|
6892
|
+
binary: new ArrayBuffer(resource.size),
|
|
6893
|
+
devicePath: `vol0:/${resource.type}.okpkg`,
|
|
6894
|
+
})
|
|
6895
|
+
);
|
|
6896
|
+
|
|
6897
|
+
const bundles = await (method as any).prepareProtocolV2ResourceBundles();
|
|
6898
|
+
|
|
6899
|
+
expect(bundles).toHaveLength(1);
|
|
6900
|
+
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
6901
|
+
resourcesSpy.mockRestore();
|
|
6902
|
+
});
|
|
6903
|
+
|
|
6904
|
+
test('uses filesystem inventory for incremental Bootloader recovery', async () => {
|
|
6905
|
+
const method = new FirmwareUpdateV4({
|
|
6906
|
+
id: 1,
|
|
6907
|
+
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
6908
|
+
});
|
|
6909
|
+
method.init();
|
|
6910
|
+
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
6911
|
+
(type, index) => ({
|
|
6912
|
+
type,
|
|
6913
|
+
url: `https://example.com/${type}.okpkg`,
|
|
6914
|
+
size: 0x52a0 + index + 1,
|
|
6915
|
+
fileHash: 'a'.repeat(64),
|
|
6916
|
+
headerHash: index.toString(16).padStart(128, '0'),
|
|
6917
|
+
})
|
|
6918
|
+
);
|
|
6919
|
+
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
6920
|
+
(method as any).device = stubDevice({
|
|
6921
|
+
getCommands: () => ({ typedCall }),
|
|
6922
|
+
getCurrentDeviceType: () => 'pro2',
|
|
6923
|
+
});
|
|
6924
|
+
const resourcesSpy = jest
|
|
6925
|
+
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
6926
|
+
.mockReturnValue(stable as any);
|
|
6927
|
+
(method as any).downloadProtocolV2Resource = jest.fn(resource =>
|
|
6928
|
+
Promise.resolve({
|
|
6929
|
+
name: `${resource.type}.okpkg`,
|
|
6930
|
+
binary: new ArrayBuffer(resource.size),
|
|
6931
|
+
devicePath: `vol0:/${resource.type}.okpkg`,
|
|
6932
|
+
})
|
|
6933
|
+
);
|
|
6934
|
+
|
|
6935
|
+
const bundles = await (method as any).prepareProtocolV2ResourceBundles();
|
|
6936
|
+
|
|
6937
|
+
expect(bundles).toHaveLength(1);
|
|
6938
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
6939
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemPathInfoQuery')).toBe(true);
|
|
6940
|
+
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
6941
|
+
resourcesSpy.mockRestore();
|
|
6942
|
+
});
|
|
6943
|
+
|
|
6944
|
+
test('does not resolve boot resources unless the optional target is selected', async () => {
|
|
6945
|
+
const method = new FirmwareUpdateV4({
|
|
6946
|
+
id: 1,
|
|
6947
|
+
payload: { method: 'firmwareUpdateV4', platform: 'web' },
|
|
6948
|
+
});
|
|
6949
|
+
method.init();
|
|
6950
|
+
const configSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
|
|
6951
|
+
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
6952
|
+
|
|
6953
|
+
await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
|
|
6954
|
+
|
|
6955
|
+
expect(configSpy).not.toHaveBeenCalled();
|
|
6956
|
+
expect(downloadSpy).not.toHaveBeenCalled();
|
|
6957
|
+
});
|
|
6958
|
+
|
|
6959
|
+
test('downloads and maps selected boot resources as direct RES files', async () => {
|
|
6960
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
6961
|
+
const binary = bytes.buffer as ArrayBuffer;
|
|
6962
|
+
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
6963
|
+
const resource = {
|
|
6964
|
+
required: false as const,
|
|
6965
|
+
target: 'RES' as const,
|
|
6966
|
+
files: [
|
|
6967
|
+
{
|
|
6968
|
+
name: 'bootloader_crest.bin',
|
|
6969
|
+
url: 'https://example.com/bootloader_crest.bin',
|
|
6970
|
+
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
6971
|
+
size: bytes.byteLength,
|
|
6972
|
+
fileHash,
|
|
6973
|
+
},
|
|
6974
|
+
],
|
|
6975
|
+
};
|
|
6976
|
+
const method = new FirmwareUpdateV4({
|
|
6977
|
+
id: 1,
|
|
6978
|
+
payload: {
|
|
6979
|
+
method: 'firmwareUpdateV4',
|
|
6980
|
+
platform: 'web',
|
|
6981
|
+
targetsToUpdate: ['boot_resources'],
|
|
6982
|
+
},
|
|
6983
|
+
});
|
|
6984
|
+
method.init();
|
|
6985
|
+
(method as any).device = stubDevice({ getCurrentDeviceType: () => 'pro2' });
|
|
6986
|
+
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(resource);
|
|
6987
|
+
jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary').mockResolvedValue({ binary });
|
|
6988
|
+
|
|
6989
|
+
await expect((method as any).prepareProtocolV2BootResources()).resolves.toEqual([
|
|
6990
|
+
{
|
|
6991
|
+
name: 'bootloader_crest.bin',
|
|
6992
|
+
binary,
|
|
6993
|
+
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
6994
|
+
},
|
|
6995
|
+
]);
|
|
6996
|
+
});
|
|
6997
|
+
|
|
6998
|
+
test('rejects a manually supplied resource file when its manifest hash does not match', () => {
|
|
6999
|
+
const method = new FirmwareUpdateV4({
|
|
7000
|
+
id: 1,
|
|
7001
|
+
payload: {
|
|
7002
|
+
method: 'firmwareUpdateV4',
|
|
7003
|
+
platform: 'web',
|
|
7004
|
+
resourceFiles: [
|
|
7005
|
+
{
|
|
7006
|
+
binary: new Uint8Array([1, 2, 3]).buffer,
|
|
7007
|
+
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
7008
|
+
size: 3,
|
|
7009
|
+
fileHash: '00'.repeat(32),
|
|
7010
|
+
},
|
|
7011
|
+
],
|
|
7012
|
+
},
|
|
7013
|
+
});
|
|
7014
|
+
method.init();
|
|
7015
|
+
|
|
7016
|
+
expect(() => (method as any).prepareExplicitProtocolV2ResourceFiles()).toThrow(
|
|
7017
|
+
'SHA-256 mismatch'
|
|
7018
|
+
);
|
|
7019
|
+
});
|
|
6255
7020
|
});
|
|
6256
7021
|
|
|
6257
7022
|
describe('Protocol V2 explicit USB device selection', () => {
|
|
@@ -6390,311 +7155,230 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6390
7155
|
expect(method.unlockPolicy).toBe('none');
|
|
6391
7156
|
});
|
|
6392
7157
|
|
|
6393
|
-
test('
|
|
7158
|
+
test('checks fresh status for a wallet business method without a name allowlist', async () => {
|
|
6394
7159
|
const calls: string[] = [];
|
|
7160
|
+
const features = { unlocked: true };
|
|
6395
7161
|
const method = {
|
|
6396
|
-
name: '
|
|
6397
|
-
unlockPolicy: '
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
.
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
})
|
|
6405
|
-
.mockImplementationOnce(() => {
|
|
6406
|
-
calls.push('run-2');
|
|
6407
|
-
return Promise.resolve({ message: 'ok' });
|
|
6408
|
-
}),
|
|
7162
|
+
name: 'confluxSignMessageCIP23',
|
|
7163
|
+
unlockPolicy: 'none',
|
|
7164
|
+
useDevicePassphraseState: true,
|
|
7165
|
+
protocolV2UiInteraction: { reason: 'address-confirmation' },
|
|
7166
|
+
run: jest.fn(() => {
|
|
7167
|
+
calls.push('run');
|
|
7168
|
+
return Promise.resolve({ address: '0x1' });
|
|
7169
|
+
}),
|
|
6409
7170
|
};
|
|
6410
7171
|
const device = {
|
|
7172
|
+
features,
|
|
7173
|
+
commands: {
|
|
7174
|
+
typedCall: jest.fn(() => {
|
|
7175
|
+
calls.push('status');
|
|
7176
|
+
return Promise.resolve({ message: { unlocked: true } });
|
|
7177
|
+
}),
|
|
7178
|
+
},
|
|
6411
7179
|
isProtocolV2: () => true,
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
7180
|
+
isBootloader: () => false,
|
|
7181
|
+
isRomloader: () => false,
|
|
7182
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
7183
|
+
features.unlocked = status.unlocked === true;
|
|
7184
|
+
return features;
|
|
6415
7185
|
}),
|
|
7186
|
+
unlockDevice: jest.fn(),
|
|
6416
7187
|
};
|
|
6417
7188
|
const uiCoordinator = {
|
|
6418
7189
|
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6419
|
-
enterUnlockInteraction: jest.fn(
|
|
6420
|
-
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
7190
|
+
enterUnlockInteraction: jest.fn(),
|
|
6421
7191
|
};
|
|
6422
7192
|
|
|
6423
7193
|
await expect(
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
]);
|
|
7194
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
7195
|
+
uiCoordinator: uiCoordinator as any,
|
|
7196
|
+
})
|
|
7197
|
+
).resolves.toEqual({ address: '0x1' });
|
|
7198
|
+
|
|
7199
|
+
expect(calls).toEqual(['status', 'method-prompt', 'run']);
|
|
7200
|
+
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
7201
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
7202
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6434
7203
|
});
|
|
6435
7204
|
|
|
6436
|
-
test('
|
|
7205
|
+
test('validates the fresh device identity before starting unlock', async () => {
|
|
6437
7206
|
const calls: string[] = [];
|
|
7207
|
+
const identityError = new Error('Unexpected device');
|
|
6438
7208
|
const method = {
|
|
6439
7209
|
name: 'evmSignMessage',
|
|
6440
|
-
|
|
7210
|
+
unlockPolicy: 'none',
|
|
6441
7211
|
useDevicePassphraseState: true,
|
|
6442
|
-
|
|
6443
|
-
run: jest
|
|
6444
|
-
.fn()
|
|
6445
|
-
.mockImplementationOnce(() => {
|
|
6446
|
-
calls.push('run-1');
|
|
6447
|
-
return Promise.reject(deviceLockedError());
|
|
6448
|
-
})
|
|
6449
|
-
.mockImplementationOnce(() => {
|
|
6450
|
-
calls.push('run-2');
|
|
6451
|
-
return Promise.resolve({ message: 'ok' });
|
|
6452
|
-
}),
|
|
7212
|
+
run: jest.fn(),
|
|
6453
7213
|
};
|
|
6454
|
-
const
|
|
6455
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
6456
|
-
calls.push('negotiate-session');
|
|
6457
|
-
return Promise.resolve({ message: { version: 2 } });
|
|
6458
|
-
}
|
|
6459
|
-
if (requestType === 'DeviceSessionGet') {
|
|
6460
|
-
calls.push('resume-hidden-session');
|
|
6461
|
-
expect(request).toEqual({ session_id: 'hidden-session' });
|
|
6462
|
-
return Promise.resolve({
|
|
6463
|
-
message: {
|
|
6464
|
-
session_id: 'hidden-session',
|
|
6465
|
-
btc_test_address: 'hidden-state',
|
|
6466
|
-
},
|
|
6467
|
-
});
|
|
6468
|
-
}
|
|
6469
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6470
|
-
});
|
|
7214
|
+
const features = { unlocked: false };
|
|
6471
7215
|
const device = {
|
|
6472
|
-
features
|
|
6473
|
-
|
|
6474
|
-
|
|
7216
|
+
features,
|
|
7217
|
+
commands: {
|
|
7218
|
+
typedCall: jest.fn(() => {
|
|
7219
|
+
calls.push('status');
|
|
7220
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
7221
|
+
}),
|
|
7222
|
+
},
|
|
6475
7223
|
isProtocolV2: () => true,
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
getCurrentPassphraseProtection: () => true,
|
|
6481
|
-
getInternalState: () => 'hidden-session',
|
|
6482
|
-
clearInternalState: jest.fn(),
|
|
6483
|
-
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6484
|
-
updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
|
|
7224
|
+
isBootloader: () => false,
|
|
7225
|
+
isRomloader: () => false,
|
|
7226
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
7227
|
+
unlockDevice: jest.fn(),
|
|
6485
7228
|
};
|
|
6486
7229
|
|
|
6487
|
-
await expect(
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
'validate-hidden-session',
|
|
6496
|
-
'run-2',
|
|
6497
|
-
]);
|
|
6498
|
-
});
|
|
6499
|
-
|
|
6500
|
-
test('does not replay a business method when the hidden-wallet session cannot be restored after unlock', async () => {
|
|
6501
|
-
const initialError = deviceLockedError();
|
|
6502
|
-
const restoreError = new Error('Invalid wallet session');
|
|
6503
|
-
const method = {
|
|
6504
|
-
name: 'evmSignMessage',
|
|
6505
|
-
payload: { passphraseState: 'hidden-state' },
|
|
6506
|
-
useDevicePassphraseState: true,
|
|
6507
|
-
unlockPolicy: 'retry-on-locked',
|
|
6508
|
-
run: jest.fn().mockRejectedValueOnce(initialError),
|
|
6509
|
-
};
|
|
6510
|
-
const typedCall = jest.fn((requestType: string) => {
|
|
6511
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
6512
|
-
return Promise.resolve({ message: { version: 2 } });
|
|
6513
|
-
}
|
|
6514
|
-
if (requestType === 'DeviceSessionGet') {
|
|
6515
|
-
return Promise.reject(restoreError);
|
|
6516
|
-
}
|
|
6517
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6518
|
-
});
|
|
6519
|
-
const device = {
|
|
6520
|
-
features: { unlocked: true, passphraseProtection: true },
|
|
6521
|
-
passphraseState: 'hidden-state',
|
|
6522
|
-
commands: { typedCall },
|
|
6523
|
-
isProtocolV2: () => true,
|
|
6524
|
-
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6525
|
-
getCurrentPassphraseProtection: () => true,
|
|
6526
|
-
getInternalState: () => 'hidden-session',
|
|
6527
|
-
clearInternalState: jest.fn(),
|
|
6528
|
-
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6529
|
-
updateInternalState: jest.fn(),
|
|
6530
|
-
};
|
|
7230
|
+
await expect(
|
|
7231
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
7232
|
+
afterStatusBeforeUnlock: () => {
|
|
7233
|
+
calls.push('identity');
|
|
7234
|
+
throw identityError;
|
|
7235
|
+
},
|
|
7236
|
+
})
|
|
7237
|
+
).rejects.toBe(identityError);
|
|
6531
7238
|
|
|
6532
|
-
|
|
6533
|
-
expect(
|
|
6534
|
-
expect(
|
|
6535
|
-
expect(device.clearInternalState).toHaveBeenCalledTimes(1);
|
|
6536
|
-
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
7239
|
+
expect(calls).toEqual(['status', 'identity']);
|
|
7240
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
7241
|
+
expect(method.run).not.toHaveBeenCalled();
|
|
6537
7242
|
});
|
|
6538
7243
|
|
|
6539
|
-
test('
|
|
7244
|
+
test('unlocks before business and never replays the callback', async () => {
|
|
6540
7245
|
const calls: string[] = [];
|
|
7246
|
+
const features = { unlocked: true };
|
|
6541
7247
|
const method = {
|
|
6542
7248
|
name: 'evmSignMessage',
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
unlockPolicy: 'retry-on-locked',
|
|
7249
|
+
unlockPolicy: 'unlock-before-run',
|
|
7250
|
+
protocolV2UiInteraction: { reason: 'signing-confirmation' },
|
|
6546
7251
|
run: jest.fn(() => {
|
|
6547
7252
|
calls.push('run');
|
|
6548
|
-
return Promise.resolve({
|
|
7253
|
+
return Promise.resolve({ signature: 'signed' });
|
|
6549
7254
|
}),
|
|
6550
7255
|
};
|
|
6551
|
-
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
6552
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
6553
|
-
calls.push('negotiate-session');
|
|
6554
|
-
return Promise.resolve({ message: { version: 2 } });
|
|
6555
|
-
}
|
|
6556
|
-
if (requestType === 'DeviceSessionGet') {
|
|
6557
|
-
calls.push('resume-hidden-session');
|
|
6558
|
-
expect(request).toEqual({ session_id: 'hidden-session' });
|
|
6559
|
-
return Promise.resolve({
|
|
6560
|
-
message: {
|
|
6561
|
-
session_id: 'hidden-session',
|
|
6562
|
-
btc_test_address: 'hidden-state',
|
|
6563
|
-
},
|
|
6564
|
-
});
|
|
6565
|
-
}
|
|
6566
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6567
|
-
});
|
|
6568
|
-
const features = {
|
|
6569
|
-
unlocked: false,
|
|
6570
|
-
unlockedAttachPin: true,
|
|
6571
|
-
passphraseProtection: true,
|
|
6572
|
-
};
|
|
6573
7256
|
const device = {
|
|
6574
7257
|
features,
|
|
6575
|
-
|
|
6576
|
-
|
|
7258
|
+
commands: {
|
|
7259
|
+
typedCall: jest.fn(() => {
|
|
7260
|
+
calls.push('status');
|
|
7261
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
7262
|
+
}),
|
|
7263
|
+
},
|
|
6577
7264
|
isProtocolV2: () => true,
|
|
7265
|
+
isBootloader: () => false,
|
|
7266
|
+
isRomloader: () => false,
|
|
7267
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
7268
|
+
features.unlocked = status.unlocked === true;
|
|
7269
|
+
return features;
|
|
7270
|
+
}),
|
|
6578
7271
|
unlockDevice: jest.fn(() => {
|
|
6579
|
-
calls.push('unlock
|
|
7272
|
+
calls.push('unlock');
|
|
6580
7273
|
features.unlocked = true;
|
|
6581
|
-
features
|
|
6582
|
-
|
|
7274
|
+
return Promise.resolve(features);
|
|
7275
|
+
}),
|
|
7276
|
+
};
|
|
7277
|
+
const uiCoordinator = {
|
|
7278
|
+
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
7279
|
+
enterUnlockInteraction: jest.fn(() => {
|
|
7280
|
+
calls.push('unlock-prompt');
|
|
7281
|
+
return undefined;
|
|
6583
7282
|
}),
|
|
6584
|
-
getCurrentPassphraseProtection: () => true,
|
|
6585
|
-
getInternalState: () => 'hidden-session',
|
|
6586
|
-
clearInternalState: jest.fn(),
|
|
6587
|
-
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6588
|
-
updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
|
|
6589
7283
|
};
|
|
6590
7284
|
|
|
6591
|
-
await expect(
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
7285
|
+
await expect(
|
|
7286
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
7287
|
+
uiCoordinator: uiCoordinator as any,
|
|
7288
|
+
prepare: () => {
|
|
7289
|
+
calls.push('wallet-session');
|
|
7290
|
+
return Promise.resolve();
|
|
7291
|
+
},
|
|
7292
|
+
})
|
|
7293
|
+
).resolves.toEqual({ signature: 'signed' });
|
|
7294
|
+
|
|
6595
7295
|
expect(calls).toEqual([
|
|
6596
|
-
'
|
|
6597
|
-
'
|
|
6598
|
-
'
|
|
6599
|
-
'
|
|
7296
|
+
'status',
|
|
7297
|
+
'unlock-prompt',
|
|
7298
|
+
'unlock',
|
|
7299
|
+
'wallet-session',
|
|
7300
|
+
'method-prompt',
|
|
6600
7301
|
'run',
|
|
6601
7302
|
]);
|
|
7303
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
7304
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6602
7305
|
});
|
|
6603
7306
|
|
|
6604
|
-
test('
|
|
7307
|
+
test('fails closed when fresh status does not explicitly report unlocked state', async () => {
|
|
6605
7308
|
const method = {
|
|
6606
7309
|
name: 'evmGetAddress',
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
unlockPolicy: 'retry-on-locked',
|
|
6610
|
-
run: jest.fn().mockResolvedValue({ address: 'standard-wallet-address' }),
|
|
7310
|
+
unlockPolicy: 'unlock-before-run',
|
|
7311
|
+
run: jest.fn(),
|
|
6611
7312
|
};
|
|
6612
7313
|
const device = {
|
|
6613
|
-
features: { unlocked:
|
|
6614
|
-
|
|
7314
|
+
features: { unlocked: undefined },
|
|
7315
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: {} }) },
|
|
6615
7316
|
isProtocolV2: () => true,
|
|
6616
|
-
|
|
7317
|
+
isBootloader: () => false,
|
|
7318
|
+
isRomloader: () => false,
|
|
7319
|
+
updateProtocolV2Status: jest.fn(() => ({ unlocked: undefined })),
|
|
7320
|
+
unlockDevice: jest.fn(),
|
|
6617
7321
|
};
|
|
6618
7322
|
|
|
6619
|
-
await expect(
|
|
6620
|
-
|
|
7323
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toMatchObject({
|
|
7324
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
6621
7325
|
});
|
|
6622
|
-
expect(device.unlockDevice).
|
|
6623
|
-
expect(method.run).
|
|
7326
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
7327
|
+
expect(method.run).not.toHaveBeenCalled();
|
|
6624
7328
|
});
|
|
6625
7329
|
|
|
6626
|
-
test('
|
|
6627
|
-
const
|
|
7330
|
+
test('returns a business DeviceLocked error without unlocking or replaying', async () => {
|
|
7331
|
+
const error = deviceLockedError();
|
|
6628
7332
|
const method = {
|
|
6629
|
-
name: '
|
|
7333
|
+
name: 'evmSignMessage',
|
|
6630
7334
|
unlockPolicy: 'unlock-before-run',
|
|
6631
|
-
|
|
6632
|
-
run: jest.fn(() => {
|
|
6633
|
-
calls.push('run');
|
|
6634
|
-
return Promise.resolve({ message: 'ok' });
|
|
6635
|
-
}),
|
|
7335
|
+
run: jest.fn().mockRejectedValue(error),
|
|
6636
7336
|
};
|
|
6637
7337
|
const features = { unlocked: true };
|
|
6638
7338
|
const device = {
|
|
6639
7339
|
features,
|
|
6640
|
-
commands: {
|
|
6641
|
-
typedCall: jest.fn(() => {
|
|
6642
|
-
calls.push('refresh-status');
|
|
6643
|
-
return Promise.resolve({ message: { unlocked: false } });
|
|
6644
|
-
}),
|
|
6645
|
-
},
|
|
7340
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
|
|
6646
7341
|
isProtocolV2: () => true,
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
unlockDevice: jest.fn(() => {
|
|
6652
|
-
calls.push('unlock');
|
|
6653
|
-
features.unlocked = true;
|
|
6654
|
-
return Promise.resolve();
|
|
6655
|
-
}),
|
|
6656
|
-
};
|
|
6657
|
-
const uiCoordinator = {
|
|
6658
|
-
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6659
|
-
enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
|
|
6660
|
-
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
7342
|
+
isBootloader: () => false,
|
|
7343
|
+
isRomloader: () => false,
|
|
7344
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
7345
|
+
unlockDevice: jest.fn(),
|
|
6661
7346
|
};
|
|
6662
7347
|
|
|
6663
|
-
await expect(
|
|
6664
|
-
|
|
6665
|
-
).resolves.toEqual({ message: 'ok' });
|
|
6666
|
-
expect(calls).toEqual(['refresh-status', 'unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6667
|
-
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
7348
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toBe(error);
|
|
7349
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6668
7350
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6669
|
-
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
6670
|
-
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6671
7351
|
});
|
|
6672
7352
|
|
|
6673
|
-
test('
|
|
6674
|
-
const features = { unlocked:
|
|
6675
|
-
const
|
|
6676
|
-
|
|
7353
|
+
test('reuses one lightweight preflight context across nested protected methods', async () => {
|
|
7354
|
+
const features = { unlocked: true };
|
|
7355
|
+
const context = createProtocolV2UnlockContext();
|
|
7356
|
+
const firstMethod = {
|
|
7357
|
+
name: 'allNetworkGetAddress',
|
|
7358
|
+
unlockPolicy: 'unlock-before-run',
|
|
7359
|
+
run: jest.fn().mockResolvedValue({ root: true }),
|
|
7360
|
+
};
|
|
7361
|
+
const nestedMethod = {
|
|
7362
|
+
name: 'evmGetAddress',
|
|
6677
7363
|
unlockPolicy: 'unlock-before-run',
|
|
6678
|
-
run: jest.fn().mockResolvedValue({
|
|
7364
|
+
run: jest.fn().mockResolvedValue({ address: '0x1' }),
|
|
6679
7365
|
};
|
|
6680
7366
|
const device = {
|
|
6681
7367
|
features,
|
|
6682
|
-
commands: {
|
|
6683
|
-
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
|
|
6684
|
-
},
|
|
7368
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
|
|
6685
7369
|
isProtocolV2: () => true,
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
}),
|
|
7370
|
+
isBootloader: () => false,
|
|
7371
|
+
isRomloader: () => false,
|
|
7372
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
6690
7373
|
unlockDevice: jest.fn(),
|
|
6691
7374
|
};
|
|
6692
7375
|
|
|
6693
|
-
await
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
expect(device.
|
|
6697
|
-
expect(
|
|
7376
|
+
await runMethodWithUnlockPolicy(firstMethod as any, device as any, { context });
|
|
7377
|
+
await runMethodWithUnlockPolicy(nestedMethod as any, device as any, { context });
|
|
7378
|
+
|
|
7379
|
+
expect(device.commands.typedCall).toHaveBeenCalledTimes(1);
|
|
7380
|
+
expect(firstMethod.run).toHaveBeenCalledTimes(1);
|
|
7381
|
+
expect(nestedMethod.run).toHaveBeenCalledTimes(1);
|
|
6698
7382
|
});
|
|
6699
7383
|
|
|
6700
7384
|
test.each(['bootloader', 'romloader'] as const)(
|
|
@@ -6714,7 +7398,7 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6714
7398
|
unlockDevice: jest.fn(),
|
|
6715
7399
|
};
|
|
6716
7400
|
|
|
6717
|
-
await expect(
|
|
7401
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
|
|
6718
7402
|
message: 'updating',
|
|
6719
7403
|
});
|
|
6720
7404
|
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
@@ -6723,157 +7407,30 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6723
7407
|
}
|
|
6724
7408
|
);
|
|
6725
7409
|
|
|
6726
|
-
test('
|
|
6727
|
-
const method = {
|
|
6728
|
-
name: 'evmSignMessage',
|
|
6729
|
-
params: { message: 'not-exposed-in-event' },
|
|
6730
|
-
payload: {},
|
|
6731
|
-
unlockPolicy: 'retry-on-locked',
|
|
6732
|
-
run: jest.fn().mockResolvedValue({ signature: 'test' }),
|
|
6733
|
-
};
|
|
6734
|
-
const device = {
|
|
6735
|
-
isProtocolV2: () => true,
|
|
6736
|
-
unlockDevice: jest.fn(),
|
|
6737
|
-
};
|
|
6738
|
-
const uiCoordinator = {
|
|
6739
|
-
enterMethodInteraction: jest.fn(),
|
|
6740
|
-
enterUnlockInteraction: jest.fn(),
|
|
6741
|
-
resumeMethodInteraction: jest.fn(),
|
|
6742
|
-
};
|
|
6743
|
-
|
|
6744
|
-
await runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any);
|
|
6745
|
-
|
|
6746
|
-
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledWith({
|
|
6747
|
-
request: 'button',
|
|
6748
|
-
source: 'method-lifecycle',
|
|
6749
|
-
reason: 'signing-confirmation',
|
|
6750
|
-
completion: 'operation-completed',
|
|
6751
|
-
deviceOnly: true,
|
|
6752
|
-
operation: 'evmSignMessage',
|
|
6753
|
-
});
|
|
6754
|
-
});
|
|
6755
|
-
|
|
6756
|
-
test('does not unlock a Protocol V1 device or a method without the policy', async () => {
|
|
7410
|
+
test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
|
|
6757
7411
|
for (const [isProtocolV2, unlockPolicy] of [
|
|
6758
|
-
[false, '
|
|
7412
|
+
[false, 'unlock-before-run'],
|
|
6759
7413
|
[true, 'none'],
|
|
6760
7414
|
] as const) {
|
|
6761
|
-
const error = deviceLockedError();
|
|
6762
7415
|
const method = {
|
|
6763
7416
|
unlockPolicy,
|
|
6764
|
-
|
|
7417
|
+
useDevicePassphraseState: false,
|
|
7418
|
+
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6765
7419
|
};
|
|
6766
7420
|
const device = {
|
|
7421
|
+
commands: { typedCall: jest.fn() },
|
|
6767
7422
|
isProtocolV2: () => isProtocolV2,
|
|
6768
7423
|
unlockDevice: jest.fn(),
|
|
6769
7424
|
};
|
|
6770
7425
|
|
|
6771
|
-
await expect(
|
|
7426
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
|
|
7427
|
+
message: 'ok',
|
|
7428
|
+
});
|
|
7429
|
+
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
6772
7430
|
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6773
7431
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6774
7432
|
}
|
|
6775
7433
|
});
|
|
6776
|
-
|
|
6777
|
-
test('runs lock-free eventless methods without unlocking or synthesized UI', async () => {
|
|
6778
|
-
const method = {
|
|
6779
|
-
name: 'uploadPortfolio',
|
|
6780
|
-
unlockPolicy: 'none',
|
|
6781
|
-
protocolV2UiMode: 'none',
|
|
6782
|
-
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6783
|
-
};
|
|
6784
|
-
const device = {
|
|
6785
|
-
features: { unlocked: false },
|
|
6786
|
-
isProtocolV2: () => true,
|
|
6787
|
-
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6788
|
-
};
|
|
6789
|
-
const uiCoordinator = {
|
|
6790
|
-
enterMethodInteraction: jest.fn(),
|
|
6791
|
-
enterUnlockInteraction: jest.fn(),
|
|
6792
|
-
resumeMethodInteraction: jest.fn(),
|
|
6793
|
-
};
|
|
6794
|
-
|
|
6795
|
-
await expect(
|
|
6796
|
-
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
6797
|
-
).resolves.toEqual({ message: 'ok' });
|
|
6798
|
-
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6799
|
-
expect(uiCoordinator.enterMethodInteraction).not.toHaveBeenCalled();
|
|
6800
|
-
expect(uiCoordinator.enterUnlockInteraction).not.toHaveBeenCalled();
|
|
6801
|
-
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6802
|
-
});
|
|
6803
|
-
|
|
6804
|
-
test('does not unlock or replay a lock-free state-changing method after a locked response', async () => {
|
|
6805
|
-
const error = deviceLockedError();
|
|
6806
|
-
const method = {
|
|
6807
|
-
name: 'uploadPortfolio',
|
|
6808
|
-
unlockPolicy: 'none',
|
|
6809
|
-
run: jest.fn().mockRejectedValue(error),
|
|
6810
|
-
};
|
|
6811
|
-
const device = {
|
|
6812
|
-
features: { unlocked: true },
|
|
6813
|
-
isProtocolV2: () => true,
|
|
6814
|
-
unlockDevice: jest.fn(),
|
|
6815
|
-
};
|
|
6816
|
-
|
|
6817
|
-
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
|
|
6818
|
-
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6819
|
-
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6820
|
-
});
|
|
6821
|
-
|
|
6822
|
-
test('does not retry when unlock fails or when the retry is still locked', async () => {
|
|
6823
|
-
const initialError = deviceLockedError();
|
|
6824
|
-
const unlockError = new Error('PIN cancelled');
|
|
6825
|
-
const unlockFailMethod = {
|
|
6826
|
-
unlockPolicy: 'retry-on-locked',
|
|
6827
|
-
run: jest.fn().mockRejectedValue(initialError),
|
|
6828
|
-
};
|
|
6829
|
-
const unlockFailDevice = {
|
|
6830
|
-
isProtocolV2: () => true,
|
|
6831
|
-
unlockDevice: jest.fn().mockRejectedValue(unlockError),
|
|
6832
|
-
};
|
|
6833
|
-
const unlockFailCoordinator = {
|
|
6834
|
-
enterMethodInteraction: jest.fn(),
|
|
6835
|
-
enterUnlockInteraction: jest.fn(),
|
|
6836
|
-
resumeMethodInteraction: jest.fn(),
|
|
6837
|
-
};
|
|
6838
|
-
|
|
6839
|
-
await expect(
|
|
6840
|
-
runMethodWithUnlockRetry(
|
|
6841
|
-
unlockFailMethod as any,
|
|
6842
|
-
unlockFailDevice as any,
|
|
6843
|
-
unlockFailCoordinator as any
|
|
6844
|
-
)
|
|
6845
|
-
).rejects.toBe(unlockError);
|
|
6846
|
-
expect(unlockFailMethod.run).toHaveBeenCalledTimes(1);
|
|
6847
|
-
expect(unlockFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
6848
|
-
expect(unlockFailCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6849
|
-
|
|
6850
|
-
const retryError = deviceLockedError();
|
|
6851
|
-
const retryFailMethod = {
|
|
6852
|
-
unlockPolicy: 'retry-on-locked',
|
|
6853
|
-
run: jest.fn().mockRejectedValueOnce(initialError).mockRejectedValueOnce(retryError),
|
|
6854
|
-
};
|
|
6855
|
-
const retryFailDevice = {
|
|
6856
|
-
isProtocolV2: () => true,
|
|
6857
|
-
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6858
|
-
};
|
|
6859
|
-
const retryFailCoordinator = {
|
|
6860
|
-
enterMethodInteraction: jest.fn(),
|
|
6861
|
-
enterUnlockInteraction: jest.fn(),
|
|
6862
|
-
resumeMethodInteraction: jest.fn(),
|
|
6863
|
-
};
|
|
6864
|
-
|
|
6865
|
-
await expect(
|
|
6866
|
-
runMethodWithUnlockRetry(
|
|
6867
|
-
retryFailMethod as any,
|
|
6868
|
-
retryFailDevice as any,
|
|
6869
|
-
retryFailCoordinator as any
|
|
6870
|
-
)
|
|
6871
|
-
).rejects.toBe(retryError);
|
|
6872
|
-
expect(retryFailMethod.run).toHaveBeenCalledTimes(2);
|
|
6873
|
-
expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6874
|
-
expect(retryFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
6875
|
-
expect(retryFailCoordinator.resumeMethodInteraction).toHaveBeenCalledTimes(1);
|
|
6876
|
-
});
|
|
6877
7434
|
});
|
|
6878
7435
|
|
|
6879
7436
|
describe('Protocol V2 current low-level methods', () => {
|
|
@@ -7005,7 +7562,9 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
7005
7562
|
|
|
7006
7563
|
await method.run();
|
|
7007
7564
|
|
|
7008
|
-
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
7565
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
7566
|
+
eventless_wallet_session: true,
|
|
7567
|
+
});
|
|
7009
7568
|
});
|
|
7010
7569
|
|
|
7011
7570
|
test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
|
|
@@ -7081,6 +7640,29 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
7081
7640
|
|
|
7082
7641
|
await expect(method.run()).resolves.toBe(features);
|
|
7083
7642
|
expect(unlockDevice).toHaveBeenCalledTimes(1);
|
|
7643
|
+
expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
|
|
7644
|
+
emitUiEvent: false,
|
|
7645
|
+
});
|
|
7646
|
+
});
|
|
7647
|
+
|
|
7648
|
+
test('forwards an explicit PIN type when unlocking a Protocol V2 device', async () => {
|
|
7649
|
+
const method = new DeviceUnlock({
|
|
7650
|
+
id: 1,
|
|
7651
|
+
payload: {
|
|
7652
|
+
method: 'deviceUnlock',
|
|
7653
|
+
pinType: DeviceSessionPinType.Any,
|
|
7654
|
+
},
|
|
7655
|
+
});
|
|
7656
|
+
method.init();
|
|
7657
|
+
|
|
7658
|
+
const unlockDevice = jest.fn().mockResolvedValue({ unlocked: true });
|
|
7659
|
+
(method as any).device = { unlockDevice };
|
|
7660
|
+
|
|
7661
|
+
await method.run();
|
|
7662
|
+
|
|
7663
|
+
expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, {
|
|
7664
|
+
emitUiEvent: false,
|
|
7665
|
+
});
|
|
7084
7666
|
});
|
|
7085
7667
|
|
|
7086
7668
|
test.each([
|