@onekeyfe/hd-core 1.2.0-alpha.32 → 1.2.0-alpha.34
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__/check-all-firmware-release-protocol-v2.test.ts +31 -0
- package/__tests__/core-dispose.test.ts +3 -3
- package/__tests__/device-lifecycle-events.test.ts +52 -0
- package/__tests__/device-pool-state.test.ts +14 -2
- package/__tests__/device-settings.test.ts +22 -0
- package/__tests__/device-state-mapper.test.ts +13 -2
- package/__tests__/device-state-projector.test.ts +2 -2
- package/__tests__/device-utils.test.ts +1 -0
- package/__tests__/device-wallet-session-store.test.ts +64 -17
- package/__tests__/kaspaSignTransaction.test.ts +9 -0
- package/__tests__/known-device-public-types.type-test.ts +3 -0
- package/__tests__/logBlockEvent.test.ts +89 -5
- package/__tests__/open-wallet-session.test.ts +334 -30
- package/__tests__/pro2Wallpaper.test.ts +1 -1
- package/__tests__/protocol-v2.test.ts +708 -75
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +7 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -0
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/device/DeviceStateProjector.d.ts.map +1 -1
- package/dist/device/DeviceWalletSessionStore.d.ts +8 -0
- package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
- package/dist/events/logBlockEvent.d.ts +2 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +456 -222
- package/dist/protocols/protocol-v2/walletSession.d.ts +0 -10
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -1
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/device.d.ts +4 -2
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +11 -81
- package/src/api/FirmwareUpdateV4.ts +33 -17
- package/src/api/GetPassphraseState.ts +1 -3
- package/src/api/OpenWalletSession.ts +3 -3
- package/src/api/device/DeviceSettings.ts +8 -1
- package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
- package/src/core/index.ts +7 -5
- package/src/device/Device.ts +37 -9
- package/src/device/DeviceConnector.ts +10 -7
- package/src/device/DevicePool.ts +5 -3
- package/src/device/DeviceStateMapper.ts +5 -3
- package/src/device/DeviceStateProjector.ts +0 -7
- package/src/device/DeviceWalletSessionStore.ts +57 -4
- package/src/deviceProfile/buildDeviceFeatures.ts +10 -3
- package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
- package/src/events/logBlockEvent.ts +84 -1
- package/src/protocols/protocol-v2/deviceSession.ts +1 -1
- package/src/protocols/protocol-v2/walletSession.ts +116 -34
- package/src/types/api/openWalletSession.ts +1 -1
- package/src/types/device.ts +6 -2
- package/src/types/params.ts +2 -1
- package/src/utils/deviceFeaturesCompat.ts +8 -1
- package/src/utils/pro2Wallpaper.ts +3 -9
- package/tsconfig.type-tests.json +5 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DeviceRebootType,
|
|
4
|
+
DeviceSessionPinType,
|
|
5
|
+
DeviceSettingsPage,
|
|
6
|
+
DeviceType,
|
|
7
|
+
} from '@onekeyfe/hd-transport';
|
|
3
8
|
|
|
4
9
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
5
10
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -54,6 +59,7 @@ import { getBitcoinForkSupportedProtocols } from '../src/api/btc/helpers/version
|
|
|
54
59
|
import { DataManager } from '../src/data-manager';
|
|
55
60
|
import { createCoreApi } from '../src/inject';
|
|
56
61
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
62
|
+
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
57
63
|
import { DevicePool } from '../src/device/DevicePool';
|
|
58
64
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
59
65
|
import {
|
|
@@ -96,11 +102,19 @@ jest.mock('../src/data/config', () => ({
|
|
|
96
102
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
97
103
|
}));
|
|
98
104
|
|
|
99
|
-
const createWalletSessionTypedCall = (
|
|
105
|
+
const createWalletSessionTypedCall = (
|
|
106
|
+
implementation = jest.fn(),
|
|
107
|
+
status: Partial<DeviceStatus> = { passphrase_enabled: true }
|
|
108
|
+
) =>
|
|
100
109
|
jest.fn((request: string, ...args: unknown[]) => {
|
|
101
110
|
if (request === 'ProtocolInfoRequest') {
|
|
102
111
|
return Promise.resolve({ message: { version: 2 } });
|
|
103
112
|
}
|
|
113
|
+
if (request === 'DeviceStatusGet') {
|
|
114
|
+
return Promise.resolve({
|
|
115
|
+
message: { unlocked: true, unlocked_attach_pin: false, ...status },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
104
118
|
return implementation(request, ...args);
|
|
105
119
|
});
|
|
106
120
|
|
|
@@ -464,6 +478,10 @@ async function requestProtocolV2Features({
|
|
|
464
478
|
}
|
|
465
479
|
|
|
466
480
|
describe('Protocol V2 feature adapter', () => {
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
deviceWalletSessionStore.clear();
|
|
483
|
+
});
|
|
484
|
+
|
|
467
485
|
test('treats passphrase status as unknown until the Protocol V2 device is unlocked', () => {
|
|
468
486
|
const features = buildProtocolV2FeaturesPayload({
|
|
469
487
|
deviceInfo: {
|
|
@@ -575,6 +593,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
575
593
|
const features = normalizeProtocolV2Features(descriptor as any, {
|
|
576
594
|
protocol_version: 1,
|
|
577
595
|
hw: {
|
|
596
|
+
Device_type: DeviceType.PRO2,
|
|
578
597
|
serial_no: 'PR2SERIAL',
|
|
579
598
|
},
|
|
580
599
|
fw: {
|
|
@@ -733,12 +752,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
733
752
|
features.firmwareVersion = '1.2.3';
|
|
734
753
|
features.passphraseProtection = true;
|
|
735
754
|
const typedCall = createWalletSessionTypedCall(
|
|
736
|
-
jest.fn()
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
755
|
+
jest.fn((request: string) => {
|
|
756
|
+
if (request === 'DeviceSessionAskPin') {
|
|
757
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
758
|
+
}
|
|
759
|
+
if (request === 'DeviceStatusGet') {
|
|
760
|
+
return { type: 'DeviceStatus', message: { unlocked: true } };
|
|
761
|
+
}
|
|
762
|
+
if (request === 'DeviceSessionGet') {
|
|
763
|
+
return {
|
|
764
|
+
type: 'DeviceSession',
|
|
765
|
+
message: {
|
|
766
|
+
btc_test_address: 'state-1',
|
|
767
|
+
session_id: 'session-1',
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
742
772
|
})
|
|
743
773
|
);
|
|
744
774
|
const commands = { typedCall } as unknown as DeviceCommands;
|
|
@@ -751,18 +781,236 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
751
781
|
onlyMainPin: true,
|
|
752
782
|
})
|
|
753
783
|
).resolves.toEqual({
|
|
754
|
-
passphraseState:
|
|
755
|
-
newSession:
|
|
756
|
-
unlockedAttachPin:
|
|
784
|
+
passphraseState: 'state-1',
|
|
785
|
+
newSession: 'session-1',
|
|
786
|
+
unlockedAttachPin: undefined,
|
|
757
787
|
resumed: false,
|
|
758
788
|
});
|
|
759
789
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
760
790
|
eventless_wallet_session: true,
|
|
761
791
|
});
|
|
762
|
-
expect(
|
|
792
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
793
|
+
type: DeviceSessionPinType.Main,
|
|
794
|
+
});
|
|
795
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
796
|
+
device.passphraseState = 'state-1';
|
|
797
|
+
expect(device.getInternalState()).toBe('session-1');
|
|
763
798
|
});
|
|
764
799
|
|
|
765
|
-
test('
|
|
800
|
+
test('reuses the cached Protocol V2 standard wallet instead of asking Main PIN again', async () => {
|
|
801
|
+
const deviceId = 'standard-reuse-device';
|
|
802
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
803
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
804
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
805
|
+
{
|
|
806
|
+
status: {
|
|
807
|
+
device_id: deviceId,
|
|
808
|
+
unlocked: true,
|
|
809
|
+
passphrase_enabled: true,
|
|
810
|
+
},
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
const typedCall = jest.fn((request: string) => {
|
|
815
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
816
|
+
if (request === 'DeviceSessionAskPin') {
|
|
817
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
818
|
+
}
|
|
819
|
+
if (request === 'DeviceStatusGet') {
|
|
820
|
+
return {
|
|
821
|
+
message: {
|
|
822
|
+
device_id: deviceId,
|
|
823
|
+
unlocked: true,
|
|
824
|
+
passphrase_enabled: true,
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (request === 'DeviceSessionGet') {
|
|
829
|
+
return {
|
|
830
|
+
message: {
|
|
831
|
+
btc_test_address: 'standard-state',
|
|
832
|
+
session_id: 'standard-session',
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
837
|
+
});
|
|
838
|
+
(device as any).commands = { typedCall };
|
|
839
|
+
|
|
840
|
+
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
841
|
+
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
842
|
+
|
|
843
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
|
|
844
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
845
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
846
|
+
session_id: 'standard-session',
|
|
847
|
+
});
|
|
848
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
849
|
+
passphraseState: 'standard-state',
|
|
850
|
+
sessionId: 'standard-session',
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test('reopens the Protocol V2 standard wallet when the cached session resolves to another wallet', async () => {
|
|
855
|
+
const deviceId = 'standard-invalid-device';
|
|
856
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
857
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
858
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
859
|
+
{
|
|
860
|
+
status: {
|
|
861
|
+
device_id: deviceId,
|
|
862
|
+
unlocked: true,
|
|
863
|
+
passphrase_enabled: true,
|
|
864
|
+
},
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
deviceWalletSessionStore.set(deviceId, 'hidden-state', 'hidden-session');
|
|
868
|
+
deviceWalletSessionStore.setStandard(deviceId, 'old-standard-state', 'old-standard-session');
|
|
869
|
+
|
|
870
|
+
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
871
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
872
|
+
if (request === 'DeviceSessionGet' && params.session_id) {
|
|
873
|
+
return {
|
|
874
|
+
message: {
|
|
875
|
+
btc_test_address: 'hidden-state',
|
|
876
|
+
session_id: 'hidden-session',
|
|
877
|
+
},
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (request === 'DeviceSessionGet') {
|
|
881
|
+
return {
|
|
882
|
+
message: {
|
|
883
|
+
btc_test_address: 'old-standard-state',
|
|
884
|
+
session_id: 'new-standard-session',
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (request === 'DeviceSessionAskPin') {
|
|
889
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
890
|
+
}
|
|
891
|
+
if (request === 'DeviceStatusGet') {
|
|
892
|
+
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
893
|
+
}
|
|
894
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
895
|
+
});
|
|
896
|
+
(device as any).commands = { typedCall };
|
|
897
|
+
|
|
898
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).resolves.toMatchObject({
|
|
899
|
+
passphraseState: 'old-standard-state',
|
|
900
|
+
newSession: 'new-standard-session',
|
|
901
|
+
resumed: false,
|
|
902
|
+
});
|
|
903
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
|
|
904
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
905
|
+
passphraseState: 'old-standard-state',
|
|
906
|
+
sessionId: 'new-standard-session',
|
|
907
|
+
});
|
|
908
|
+
expect(deviceWalletSessionStore.get(deviceId, 'hidden-state')).toBe('hidden-session');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test('opens Main PIN directly instead of resuming a standard session while locked', async () => {
|
|
912
|
+
const deviceId = 'standard-locked-device';
|
|
913
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
914
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
915
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
916
|
+
{
|
|
917
|
+
status: {
|
|
918
|
+
device_id: deviceId,
|
|
919
|
+
unlocked: false,
|
|
920
|
+
passphrase_enabled: true,
|
|
921
|
+
},
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
deviceWalletSessionStore.setStandard(deviceId, 'standard-state', 'cached-standard-session');
|
|
925
|
+
|
|
926
|
+
let sessionGetCalls = 0;
|
|
927
|
+
const typedCall = jest.fn((request: string) => {
|
|
928
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
929
|
+
if (request === 'DeviceSessionAskPin') {
|
|
930
|
+
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
931
|
+
}
|
|
932
|
+
if (request === 'DeviceStatusGet') {
|
|
933
|
+
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
934
|
+
}
|
|
935
|
+
if (request === 'DeviceSessionGet') {
|
|
936
|
+
sessionGetCalls += 1;
|
|
937
|
+
if (sessionGetCalls === 1) {
|
|
938
|
+
throw Object.assign(new Error('Device is locked'), {
|
|
939
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
message: {
|
|
944
|
+
btc_test_address: 'standard-state',
|
|
945
|
+
session_id: 'cached-standard-session',
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
950
|
+
});
|
|
951
|
+
(device as any).commands = { typedCall };
|
|
952
|
+
|
|
953
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).resolves.toMatchObject({
|
|
954
|
+
newSession: 'cached-standard-session',
|
|
955
|
+
resumed: true,
|
|
956
|
+
});
|
|
957
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(2);
|
|
958
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)?.sessionId).toBe(
|
|
959
|
+
'cached-standard-session'
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test('recovers a mismatched cached standard wallet without deleting hidden sessions', async () => {
|
|
964
|
+
const deviceId = 'standard-mismatch-device';
|
|
965
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
966
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
967
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
968
|
+
{
|
|
969
|
+
status: {
|
|
970
|
+
device_id: deviceId,
|
|
971
|
+
unlocked: true,
|
|
972
|
+
passphrase_enabled: true,
|
|
973
|
+
},
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
deviceWalletSessionStore.set(deviceId, 'hidden-state', 'hidden-session');
|
|
977
|
+
deviceWalletSessionStore.setStandard(deviceId, 'standard-state', 'standard-session');
|
|
978
|
+
let sessionGetCalls = 0;
|
|
979
|
+
const typedCall = jest.fn((request: string) => {
|
|
980
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
981
|
+
if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
|
|
982
|
+
if (request === 'DeviceStatusGet') {
|
|
983
|
+
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
984
|
+
}
|
|
985
|
+
if (request === 'DeviceSessionGet') {
|
|
986
|
+
sessionGetCalls += 1;
|
|
987
|
+
return {
|
|
988
|
+
message: {
|
|
989
|
+
btc_test_address: sessionGetCalls === 1 ? 'unexpected-wallet-state' : 'standard-state',
|
|
990
|
+
session_id: sessionGetCalls === 1 ? 'unexpected-session' : 'renewed-standard-session',
|
|
991
|
+
},
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
995
|
+
});
|
|
996
|
+
(device as any).commands = { typedCall };
|
|
997
|
+
|
|
998
|
+
await expect(getProtocolV2WalletSession(device, { onlyMainPin: true })).resolves.toMatchObject({
|
|
999
|
+
passphraseState: 'standard-state',
|
|
1000
|
+
newSession: 'renewed-standard-session',
|
|
1001
|
+
resumed: false,
|
|
1002
|
+
});
|
|
1003
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
1004
|
+
type: DeviceSessionPinType.Main,
|
|
1005
|
+
});
|
|
1006
|
+
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
1007
|
+
passphraseState: 'standard-state',
|
|
1008
|
+
sessionId: 'renewed-standard-session',
|
|
1009
|
+
});
|
|
1010
|
+
expect(deviceWalletSessionStore.get(deviceId, 'hidden-state')).toBe('hidden-session');
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
test('selects a Protocol V2 wallet explicitly even when passphrase protection was disabled', async () => {
|
|
766
1014
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
767
1015
|
(device as any).features = normalizeProtocolV2Features(
|
|
768
1016
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
@@ -783,13 +1031,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
783
1031
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
784
1032
|
|
|
785
1033
|
await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
|
|
786
|
-
passphraseState:
|
|
787
|
-
newSession:
|
|
788
|
-
unlockedAttachPin:
|
|
1034
|
+
passphraseState: 'main-wallet-state',
|
|
1035
|
+
newSession: 'main-wallet-session',
|
|
1036
|
+
unlockedAttachPin: undefined,
|
|
789
1037
|
resumed: false,
|
|
790
1038
|
});
|
|
791
1039
|
|
|
792
|
-
expect(promptPassphrase).
|
|
1040
|
+
expect(promptPassphrase).toHaveBeenCalled();
|
|
793
1041
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
794
1042
|
eventless_wallet_session: true,
|
|
795
1043
|
});
|
|
@@ -846,15 +1094,20 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
846
1094
|
},
|
|
847
1095
|
}
|
|
848
1096
|
);
|
|
1097
|
+
const lockedError = Object.assign(new Error('Device is locked'), {
|
|
1098
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
1099
|
+
});
|
|
849
1100
|
const typedCall = createWalletSessionTypedCall(
|
|
850
|
-
jest
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1101
|
+
jest
|
|
1102
|
+
.fn()
|
|
1103
|
+
.mockRejectedValueOnce(lockedError)
|
|
1104
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1105
|
+
.mockResolvedValueOnce({
|
|
1106
|
+
message: {
|
|
1107
|
+
btc_test_address: 'hidden-after-unlock-state',
|
|
1108
|
+
session_id: 'hidden-after-unlock-session',
|
|
1109
|
+
},
|
|
1110
|
+
})
|
|
858
1111
|
);
|
|
859
1112
|
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden-after-unlock' });
|
|
860
1113
|
const unlockDevice = jest.spyOn(device, 'unlockDevice').mockImplementation(() => {
|
|
@@ -868,14 +1121,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
868
1121
|
(device as any).commands = { typedCall, promptPassphrase };
|
|
869
1122
|
|
|
870
1123
|
await expect(getProtocolV2WalletSession(device)).resolves.toEqual({
|
|
871
|
-
passphraseState:
|
|
872
|
-
newSession:
|
|
873
|
-
unlockedAttachPin:
|
|
1124
|
+
passphraseState: 'hidden-after-unlock-state',
|
|
1125
|
+
newSession: 'hidden-after-unlock-session',
|
|
1126
|
+
unlockedAttachPin: undefined,
|
|
874
1127
|
resumed: false,
|
|
875
1128
|
});
|
|
876
1129
|
|
|
877
1130
|
expect(unlockDevice).toHaveBeenCalledTimes(1);
|
|
878
|
-
expect(promptPassphrase).
|
|
1131
|
+
expect(promptPassphrase).toHaveBeenCalled();
|
|
879
1132
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
880
1133
|
eventless_wallet_session: true,
|
|
881
1134
|
});
|
|
@@ -905,8 +1158,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
905
1158
|
onlyMainPin: true,
|
|
906
1159
|
})
|
|
907
1160
|
).resolves.toMatchObject({
|
|
908
|
-
passphraseState:
|
|
909
|
-
newSession:
|
|
1161
|
+
passphraseState: 'main-wallet-state',
|
|
1162
|
+
newSession: 'main-wallet-session',
|
|
910
1163
|
});
|
|
911
1164
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
912
1165
|
eventless_wallet_session: true,
|
|
@@ -933,9 +1186,17 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
933
1186
|
}
|
|
934
1187
|
);
|
|
935
1188
|
const typedCall = createWalletSessionTypedCall(
|
|
936
|
-
jest.fn()
|
|
937
|
-
|
|
938
|
-
|
|
1189
|
+
jest.fn((request: string) => {
|
|
1190
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
1191
|
+
return { type: 'Success', message: { message: 'Passphrase prepared' } };
|
|
1192
|
+
}
|
|
1193
|
+
if (request === 'DeviceSessionGet') {
|
|
1194
|
+
return {
|
|
1195
|
+
type: 'DeviceSession',
|
|
1196
|
+
message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
939
1200
|
})
|
|
940
1201
|
);
|
|
941
1202
|
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
@@ -1148,11 +1409,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1148
1409
|
}
|
|
1149
1410
|
);
|
|
1150
1411
|
const typedCall = createWalletSessionTypedCall(
|
|
1151
|
-
jest
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1412
|
+
jest
|
|
1413
|
+
.fn()
|
|
1414
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1415
|
+
.mockRejectedValueOnce(
|
|
1416
|
+
Object.assign(new Error('Invalid session'), {
|
|
1417
|
+
errorCode: HardwareErrorCode.WalletSessionInvalid,
|
|
1418
|
+
})
|
|
1419
|
+
)
|
|
1156
1420
|
);
|
|
1157
1421
|
(device as any).commands = {
|
|
1158
1422
|
typedCall,
|
|
@@ -1165,10 +1429,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1165
1429
|
expect(typedCall.mock.calls).toEqual([
|
|
1166
1430
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1167
1431
|
['DeviceSessionAskPassphrase', 'Success', { on_device: true }],
|
|
1432
|
+
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1433
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1168
1434
|
]);
|
|
1169
1435
|
});
|
|
1170
1436
|
|
|
1171
|
-
test('
|
|
1437
|
+
test('reselects a hidden wallet when the cached Pro2 session resolves to another wallet', async () => {
|
|
1172
1438
|
const device = Device.fromDescriptor({
|
|
1173
1439
|
id: 'cache-device-mismatch',
|
|
1174
1440
|
path: 'cache-path-mismatch',
|
|
@@ -1186,25 +1452,147 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1186
1452
|
);
|
|
1187
1453
|
device.passphraseState = 'state-a';
|
|
1188
1454
|
preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a');
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1455
|
+
let sessionGetCalls = 0;
|
|
1456
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1457
|
+
jest.fn((request: string) => {
|
|
1458
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: { message: 'prepared' } };
|
|
1459
|
+
if (request === 'DeviceSessionGet') {
|
|
1460
|
+
sessionGetCalls += 1;
|
|
1461
|
+
return {
|
|
1462
|
+
type: 'DeviceSession',
|
|
1463
|
+
message:
|
|
1464
|
+
sessionGetCalls === 1
|
|
1465
|
+
? { session_id: 'session-b', btc_test_address: 'state-b' }
|
|
1466
|
+
: { session_id: 'session-a-new', btc_test_address: 'state-a' },
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1470
|
+
})
|
|
1471
|
+
);
|
|
1472
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
1473
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1197
1474
|
|
|
1198
1475
|
await expect(
|
|
1199
1476
|
getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
|
|
1200
|
-
).
|
|
1477
|
+
).resolves.toMatchObject({
|
|
1478
|
+
passphraseState: 'state-a',
|
|
1479
|
+
newSession: 'session-a-new',
|
|
1480
|
+
resumed: false,
|
|
1481
|
+
});
|
|
1482
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1201
1483
|
expect.objectContaining({
|
|
1202
|
-
|
|
1484
|
+
source: 'wallet-session-coordinator',
|
|
1485
|
+
reason: 'session-recovery',
|
|
1486
|
+
expectedPassphraseState: 'state-a',
|
|
1487
|
+
}),
|
|
1488
|
+
{ cancelDeviceOnReject: false }
|
|
1489
|
+
);
|
|
1490
|
+
expect(device.getInternalState()).toBe('session-a-new');
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
test('rejects a hidden wallet when one recovery still returns the wrong state', async () => {
|
|
1494
|
+
const device = Device.fromDescriptor({
|
|
1495
|
+
id: 'cache-device-final-mismatch',
|
|
1496
|
+
path: 'cache-path-final-mismatch',
|
|
1497
|
+
protocolType: 'V2',
|
|
1498
|
+
} as any);
|
|
1499
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1500
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1501
|
+
{
|
|
1502
|
+
status: {
|
|
1503
|
+
device_id: 'stable-device-final-mismatch',
|
|
1504
|
+
unlocked: true,
|
|
1505
|
+
passphrase_enabled: true,
|
|
1506
|
+
},
|
|
1507
|
+
}
|
|
1508
|
+
);
|
|
1509
|
+
device.passphraseState = 'state-a';
|
|
1510
|
+
preloadSessionCache('stable-device-final-mismatch', 'state-a', 'session-a');
|
|
1511
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1512
|
+
jest.fn((request: string) => {
|
|
1513
|
+
if (request === 'DeviceSessionAskPassphrase') return { message: { message: 'prepared' } };
|
|
1514
|
+
if (request === 'DeviceSessionGet') {
|
|
1515
|
+
return {
|
|
1516
|
+
type: 'DeviceSession',
|
|
1517
|
+
message: { session_id: 'wrong-session', btc_test_address: 'wrong-state' },
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1203
1521
|
})
|
|
1204
1522
|
);
|
|
1523
|
+
(device as any).commands = {
|
|
1524
|
+
typedCall,
|
|
1525
|
+
promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
await expect(
|
|
1529
|
+
getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
|
|
1530
|
+
).rejects.toMatchObject({
|
|
1531
|
+
errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
|
|
1532
|
+
});
|
|
1205
1533
|
expect(device.getInternalState()).toBeUndefined();
|
|
1206
1534
|
});
|
|
1207
1535
|
|
|
1536
|
+
test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
|
|
1537
|
+
const device = Device.fromDescriptor({
|
|
1538
|
+
id: 'cache-device-attach-recovery',
|
|
1539
|
+
path: 'cache-path-attach-recovery',
|
|
1540
|
+
protocolType: 'V2',
|
|
1541
|
+
} as any);
|
|
1542
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1543
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1544
|
+
{
|
|
1545
|
+
status: {
|
|
1546
|
+
device_id: 'stable-device-attach-recovery',
|
|
1547
|
+
unlocked: true,
|
|
1548
|
+
passphrase_enabled: true,
|
|
1549
|
+
attach_to_pin_enabled: true,
|
|
1550
|
+
},
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
device.passphraseState = 'attach-state';
|
|
1554
|
+
preloadSessionCache('stable-device-attach-recovery', 'attach-state', 'expired-attach-session');
|
|
1555
|
+
let sessionGetCalls = 0;
|
|
1556
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1557
|
+
jest.fn((request: string) => {
|
|
1558
|
+
if (request === 'DeviceSessionGet') {
|
|
1559
|
+
sessionGetCalls += 1;
|
|
1560
|
+
return {
|
|
1561
|
+
type: 'DeviceSession',
|
|
1562
|
+
message:
|
|
1563
|
+
sessionGetCalls === 1
|
|
1564
|
+
? { session_id: 'current-standard-session', btc_test_address: 'standard-state' }
|
|
1565
|
+
: { session_id: 'new-attach-session', btc_test_address: 'attach-state' },
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1571
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
1572
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1573
|
+
const unlockDevice = jest
|
|
1574
|
+
.spyOn(device, 'unlockDevice')
|
|
1575
|
+
.mockResolvedValue(device.features as any);
|
|
1576
|
+
|
|
1577
|
+
await expect(
|
|
1578
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'attach-state' })
|
|
1579
|
+
).resolves.toMatchObject({
|
|
1580
|
+
passphraseState: 'attach-state',
|
|
1581
|
+
newSession: 'new-attach-session',
|
|
1582
|
+
resumed: false,
|
|
1583
|
+
});
|
|
1584
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1585
|
+
expect.objectContaining({
|
|
1586
|
+
reason: 'session-recovery',
|
|
1587
|
+
expectedPassphraseState: 'attach-state',
|
|
1588
|
+
existsAttachPinUser: true,
|
|
1589
|
+
}),
|
|
1590
|
+
{ cancelDeviceOnReject: false }
|
|
1591
|
+
);
|
|
1592
|
+
expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
|
|
1593
|
+
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1208
1596
|
test('unlocks and retries DeviceSessionAskPassphrase when wallet selection is locked', async () => {
|
|
1209
1597
|
const device = Device.fromDescriptor({
|
|
1210
1598
|
id: 'cache-device-4',
|
|
@@ -1222,7 +1610,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1222
1610
|
jest
|
|
1223
1611
|
.fn()
|
|
1224
1612
|
.mockRejectedValueOnce(lockedError)
|
|
1225
|
-
.mockResolvedValueOnce({ message: { message: '
|
|
1613
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1226
1614
|
.mockResolvedValueOnce({
|
|
1227
1615
|
message: {
|
|
1228
1616
|
session_id: 'session-after-unlock',
|
|
@@ -1241,7 +1629,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1241
1629
|
passphraseState: 'state-after-unlock',
|
|
1242
1630
|
});
|
|
1243
1631
|
expect(unlockDevice).toHaveBeenCalledTimes(1);
|
|
1244
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
1632
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
1245
1633
|
});
|
|
1246
1634
|
|
|
1247
1635
|
test('does not request a Pro2 wallet session before features are initialized', async () => {
|
|
@@ -1385,7 +1773,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1385
1773
|
const typedCall = jest
|
|
1386
1774
|
.fn()
|
|
1387
1775
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1388
|
-
.mockResolvedValueOnce({ message: { message: '
|
|
1776
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1777
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1389
1778
|
.mockResolvedValueOnce({
|
|
1390
1779
|
type: 'DeviceSession',
|
|
1391
1780
|
message: {
|
|
@@ -1421,7 +1810,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1421
1810
|
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1422
1811
|
{
|
|
1423
1812
|
existsAttachPinUser: false,
|
|
1424
|
-
deviceOnly:
|
|
1813
|
+
deviceOnly: false,
|
|
1425
1814
|
source: 'wallet-session-coordinator',
|
|
1426
1815
|
reason: 'open-wallet',
|
|
1427
1816
|
},
|
|
@@ -1438,6 +1827,51 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1438
1827
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
1439
1828
|
});
|
|
1440
1829
|
|
|
1830
|
+
test('marks an interactive cache miss as a wallet-session recovery', async () => {
|
|
1831
|
+
const typedCall = jest
|
|
1832
|
+
.fn()
|
|
1833
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1834
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1835
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1836
|
+
.mockResolvedValueOnce({
|
|
1837
|
+
message: {
|
|
1838
|
+
btc_test_address: 'expected-state',
|
|
1839
|
+
session_id: 'recovered-session',
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
1843
|
+
const device = stubDevice({
|
|
1844
|
+
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
1845
|
+
passphraseState: 'expected-state',
|
|
1846
|
+
features: {
|
|
1847
|
+
unlocked: true,
|
|
1848
|
+
passphraseProtection: true,
|
|
1849
|
+
attachToPinEnabled: false,
|
|
1850
|
+
},
|
|
1851
|
+
commands: { typedCall, promptPassphrase },
|
|
1852
|
+
getInternalState: jest.fn().mockReturnValue(undefined),
|
|
1853
|
+
getCurrentDeviceId: () => 'pro2-device-id',
|
|
1854
|
+
getCurrentPassphraseProtection: () => true,
|
|
1855
|
+
updateInternalState: jest.fn(),
|
|
1856
|
+
clearInternalState: jest.fn(),
|
|
1857
|
+
emit: jest.fn(),
|
|
1858
|
+
}) as any;
|
|
1859
|
+
|
|
1860
|
+
await expect(
|
|
1861
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1862
|
+
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
1863
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1864
|
+
{
|
|
1865
|
+
existsAttachPinUser: false,
|
|
1866
|
+
deviceOnly: false,
|
|
1867
|
+
source: 'wallet-session-coordinator',
|
|
1868
|
+
reason: 'session-recovery',
|
|
1869
|
+
expectedPassphraseState: 'expected-state',
|
|
1870
|
+
},
|
|
1871
|
+
{ cancelDeviceOnReject: false }
|
|
1872
|
+
);
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1441
1875
|
test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
|
|
1442
1876
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1443
1877
|
const typedCall = jest.fn().mockImplementation((request: string) => {
|
|
@@ -1445,7 +1879,16 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1445
1879
|
return Promise.resolve({ message: { version: 2 } });
|
|
1446
1880
|
}
|
|
1447
1881
|
if (request === 'DeviceSessionAskPassphrase') {
|
|
1448
|
-
return Promise.resolve({
|
|
1882
|
+
return Promise.resolve({
|
|
1883
|
+
type: 'Success',
|
|
1884
|
+
message: { message: 'Passphrase prepared' },
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (request === 'DeviceStatusGet') {
|
|
1888
|
+
return Promise.resolve({
|
|
1889
|
+
type: 'DeviceStatus',
|
|
1890
|
+
message: { unlocked: true, unlocked_attach_pin: false, passphrase_enabled: true },
|
|
1891
|
+
});
|
|
1449
1892
|
}
|
|
1450
1893
|
if (request === 'DeviceSessionGet') {
|
|
1451
1894
|
return Promise.resolve({
|
|
@@ -1497,7 +1940,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1497
1940
|
]);
|
|
1498
1941
|
});
|
|
1499
1942
|
|
|
1500
|
-
test('
|
|
1943
|
+
test('returns the Pro2 standard-wallet passphraseState when passphrase is disabled', async () => {
|
|
1501
1944
|
const features = {
|
|
1502
1945
|
deviceId: 'pro2-device-id',
|
|
1503
1946
|
deviceType: 'pro2',
|
|
@@ -1505,7 +1948,15 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1505
1948
|
passphraseProtection: false,
|
|
1506
1949
|
unlocked: true,
|
|
1507
1950
|
};
|
|
1508
|
-
const typedCall = createWalletSessionTypedCall(
|
|
1951
|
+
const typedCall = createWalletSessionTypedCall(
|
|
1952
|
+
jest.fn().mockResolvedValue({
|
|
1953
|
+
type: 'DeviceSession',
|
|
1954
|
+
message: {
|
|
1955
|
+
btc_test_address: 'main-wallet-state',
|
|
1956
|
+
session_id: 'main-wallet-session',
|
|
1957
|
+
},
|
|
1958
|
+
})
|
|
1959
|
+
);
|
|
1509
1960
|
const updateInternalState = jest.fn();
|
|
1510
1961
|
const method = new GetPassphraseState({
|
|
1511
1962
|
payload: {
|
|
@@ -1514,7 +1965,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1514
1965
|
useEmptyPassphrase: true,
|
|
1515
1966
|
},
|
|
1516
1967
|
});
|
|
1517
|
-
|
|
1968
|
+
const device = stubDevice({
|
|
1518
1969
|
originalDescriptor: { ...descriptor, protocolType: 'V2' },
|
|
1519
1970
|
features,
|
|
1520
1971
|
commands: {
|
|
@@ -1526,12 +1977,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1526
1977
|
getCurrentDeviceId: () => 'pro2-device-id',
|
|
1527
1978
|
getCurrentPassphraseProtection: () => false,
|
|
1528
1979
|
}) as any;
|
|
1980
|
+
device.unlockDevice = jest.fn().mockResolvedValue(features);
|
|
1981
|
+
method.device = device;
|
|
1529
1982
|
|
|
1530
|
-
await expect(method.run()).resolves.
|
|
1983
|
+
await expect(method.run()).resolves.toBe('main-wallet-state');
|
|
1531
1984
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
1532
1985
|
eventless_wallet_session: true,
|
|
1533
1986
|
});
|
|
1534
|
-
expect(updateInternalState).
|
|
1987
|
+
expect(updateInternalState).toHaveBeenCalled();
|
|
1535
1988
|
});
|
|
1536
1989
|
|
|
1537
1990
|
test('honors initSession when getting Pro2 passphrase state', async () => {
|
|
@@ -1546,7 +1999,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1546
1999
|
const typedCall = jest
|
|
1547
2000
|
.fn()
|
|
1548
2001
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1549
|
-
.mockResolvedValueOnce({ message: { message: '
|
|
2002
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
2003
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1550
2004
|
.mockResolvedValueOnce({
|
|
1551
2005
|
type: 'DeviceSession',
|
|
1552
2006
|
message: {
|
|
@@ -1666,6 +2120,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1666
2120
|
expect(device.getInternalState()).toBeUndefined();
|
|
1667
2121
|
device.passphraseState = 'state-auto';
|
|
1668
2122
|
expect(device.getInternalState()).toBe('session-auto');
|
|
2123
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2124
|
+
on_device: true,
|
|
2125
|
+
});
|
|
1669
2126
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1670
2127
|
});
|
|
1671
2128
|
|
|
@@ -1678,7 +2135,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1678
2135
|
session_id: 'main-pin-session',
|
|
1679
2136
|
btc_test_address: 'main-wallet-state',
|
|
1680
2137
|
},
|
|
1681
|
-
})
|
|
2138
|
+
}),
|
|
2139
|
+
{ passphrase_enabled: false }
|
|
1682
2140
|
);
|
|
1683
2141
|
|
|
1684
2142
|
(device as any).features = normalizeProtocolV2Features(
|
|
@@ -1700,8 +2158,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1700
2158
|
await expect(
|
|
1701
2159
|
getPassphraseStateWithRefreshDeviceInfo(device, { onlyMainPin: true })
|
|
1702
2160
|
).resolves.toMatchObject({
|
|
1703
|
-
passphraseState:
|
|
1704
|
-
newSession:
|
|
2161
|
+
passphraseState: 'main-wallet-state',
|
|
2162
|
+
newSession: 'main-pin-session',
|
|
1705
2163
|
});
|
|
1706
2164
|
|
|
1707
2165
|
expect(device.features?.passphraseProtection).toBe(false);
|
|
@@ -1746,6 +2204,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1746
2204
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
1747
2205
|
on_device: true,
|
|
1748
2206
|
});
|
|
2207
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2208
|
+
expect(typedCall).toHaveBeenNthCalledWith(4, 'DeviceSessionGet', 'DeviceSession', {});
|
|
1749
2209
|
});
|
|
1750
2210
|
|
|
1751
2211
|
test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
|
|
@@ -1778,7 +2238,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1778
2238
|
await expect(
|
|
1779
2239
|
device.checkPassphraseStateSafety('stale-hidden-state', true, false)
|
|
1780
2240
|
).resolves.toBe(true);
|
|
1781
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2241
|
+
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
1782
2242
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
1783
2243
|
eventless_wallet_session: true,
|
|
1784
2244
|
});
|
|
@@ -1836,7 +2296,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1836
2296
|
test('uses Protocol V2 features directly when profile is absent', () => {
|
|
1837
2297
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1838
2298
|
(device as any).features = {
|
|
1839
|
-
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any
|
|
2299
|
+
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2300
|
+
hw: { Device_type: DeviceType.PRO2 },
|
|
2301
|
+
}),
|
|
1840
2302
|
serialNo: 'CACHED-SERIAL',
|
|
1841
2303
|
label: 'Cached Label',
|
|
1842
2304
|
bleName: 'Cached BLE',
|
|
@@ -1845,6 +2307,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1845
2307
|
|
|
1846
2308
|
const message = device.toMessageObject();
|
|
1847
2309
|
expect(message).toMatchObject({
|
|
2310
|
+
connectProtocol: 'V2',
|
|
1848
2311
|
serialNo: 'CACHED-SERIAL',
|
|
1849
2312
|
uuid: 'CACHED-SERIAL',
|
|
1850
2313
|
deviceId: null,
|
|
@@ -1916,7 +2379,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1916
2379
|
typedCall: jest.fn().mockResolvedValueOnce({
|
|
1917
2380
|
type: 'DeviceInfo',
|
|
1918
2381
|
message: {
|
|
1919
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2382
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
1920
2383
|
fw: { application: { version: '1.2.3' } },
|
|
1921
2384
|
},
|
|
1922
2385
|
}),
|
|
@@ -2097,7 +2560,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2097
2560
|
.mockResolvedValueOnce({
|
|
2098
2561
|
type: 'DeviceInfo',
|
|
2099
2562
|
message: {
|
|
2100
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2563
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2101
2564
|
fw: { application: { version: '1.2.3' } },
|
|
2102
2565
|
},
|
|
2103
2566
|
})
|
|
@@ -2229,7 +2692,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2229
2692
|
.mockResolvedValueOnce({
|
|
2230
2693
|
type: 'DeviceInfo',
|
|
2231
2694
|
message: {
|
|
2232
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2695
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2233
2696
|
fw: { application: { version: '1.2.3' } },
|
|
2234
2697
|
},
|
|
2235
2698
|
})
|
|
@@ -2240,7 +2703,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2240
2703
|
.mockResolvedValueOnce({
|
|
2241
2704
|
type: 'DeviceInfo',
|
|
2242
2705
|
message: {
|
|
2243
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2706
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2244
2707
|
fw: { application: { version: '1.2.4' } },
|
|
2245
2708
|
},
|
|
2246
2709
|
})
|
|
@@ -2343,7 +2806,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2343
2806
|
if (requestType === 'DeviceSessionAskPin') {
|
|
2344
2807
|
return {
|
|
2345
2808
|
type: 'Success',
|
|
2346
|
-
message: { message: '
|
|
2809
|
+
message: { message: 'PIN verified' },
|
|
2347
2810
|
};
|
|
2348
2811
|
}
|
|
2349
2812
|
if (requestType === 'DeviceStatusGet') {
|
|
@@ -2365,7 +2828,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2365
2828
|
(device as any).features = normalizeProtocolV2Features(
|
|
2366
2829
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
2367
2830
|
{
|
|
2368
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
2831
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2369
2832
|
fw: { application: { version: '1.2.3' } },
|
|
2370
2833
|
status: { init_states: true },
|
|
2371
2834
|
}
|
|
@@ -2400,7 +2863,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2400
2863
|
if (requestType === 'DeviceSessionAskPin') {
|
|
2401
2864
|
return {
|
|
2402
2865
|
type: 'Success',
|
|
2403
|
-
message: { message: '
|
|
2866
|
+
message: { message: 'PIN verified' },
|
|
2404
2867
|
};
|
|
2405
2868
|
}
|
|
2406
2869
|
if (requestType === 'DeviceStatusGet') {
|
|
@@ -3396,7 +3859,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3396
3859
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
3397
3860
|
});
|
|
3398
3861
|
|
|
3399
|
-
test('accepts a missing firmware status handler
|
|
3862
|
+
test('accepts a missing firmware status handler only after confirming normal mode', async () => {
|
|
3400
3863
|
const method = new FirmwareUpdateV4({
|
|
3401
3864
|
id: 1,
|
|
3402
3865
|
payload: {
|
|
@@ -3412,7 +3875,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3412
3875
|
getCommands: () => ({ typedCall }),
|
|
3413
3876
|
});
|
|
3414
3877
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
3415
|
-
|
|
3878
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
3879
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
3880
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
3416
3881
|
method.postProgressMessage = jest.fn();
|
|
3417
3882
|
|
|
3418
3883
|
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
@@ -3421,6 +3886,70 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3421
3886
|
|
|
3422
3887
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
3423
3888
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
|
|
3889
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
3890
|
+
});
|
|
3891
|
+
|
|
3892
|
+
test.each([
|
|
3893
|
+
[{ mode: 'normal', bootloaderMode: false }, true],
|
|
3894
|
+
[{ mode: 'bootloader', bootloaderMode: true }, false],
|
|
3895
|
+
])('derives firmware completion from the runtime-state probe: %o', async (features, expected) => {
|
|
3896
|
+
const method = new FirmwareUpdateV4({
|
|
3897
|
+
id: 1,
|
|
3898
|
+
payload: {
|
|
3899
|
+
method: 'firmwareUpdateV4',
|
|
3900
|
+
},
|
|
3901
|
+
});
|
|
3902
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
3903
|
+
const probeProtocolV2RuntimeState = jest.fn().mockResolvedValue(features);
|
|
3904
|
+
(method as any).device = stubDevice({ probeProtocolV2RuntimeState });
|
|
3905
|
+
|
|
3906
|
+
await expect((method as any).probeProtocolV2NormalMode(deviceInfo)).resolves.toBe(expected);
|
|
3907
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
|
|
3908
|
+
});
|
|
3909
|
+
|
|
3910
|
+
test('keeps polling when the firmware status handler is missing in loader mode', async () => {
|
|
3911
|
+
const method = new FirmwareUpdateV4({
|
|
3912
|
+
id: 1,
|
|
3913
|
+
payload: {
|
|
3914
|
+
method: 'firmwareUpdateV4',
|
|
3915
|
+
},
|
|
3916
|
+
});
|
|
3917
|
+
const typedCall = jest
|
|
3918
|
+
.fn()
|
|
3919
|
+
.mockRejectedValueOnce(new Error('Failure: Handler not registered for this message'))
|
|
3920
|
+
.mockResolvedValueOnce({
|
|
3921
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
3922
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
3923
|
+
});
|
|
3924
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
3925
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
3926
|
+
callback: () => void
|
|
3927
|
+
) => {
|
|
3928
|
+
callback();
|
|
3929
|
+
return 0 as any;
|
|
3930
|
+
}) as typeof setTimeout);
|
|
3931
|
+
|
|
3932
|
+
(method as any).device = stubDevice({
|
|
3933
|
+
getCommands: () => ({ typedCall }),
|
|
3934
|
+
});
|
|
3935
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
3936
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
3937
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(false);
|
|
3938
|
+
method.postProgressMessage = jest.fn();
|
|
3939
|
+
|
|
3940
|
+
try {
|
|
3941
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
3942
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
3943
|
+
]);
|
|
3944
|
+
} finally {
|
|
3945
|
+
setTimeoutSpy.mockRestore();
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
3949
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
3950
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
3951
|
+
]);
|
|
3952
|
+
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
3424
3953
|
});
|
|
3425
3954
|
|
|
3426
3955
|
test('polls target status after update ACK and finishes when all targets complete', async () => {
|
|
@@ -3926,6 +4455,62 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3926
4455
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
3927
4456
|
});
|
|
3928
4457
|
|
|
4458
|
+
test('ignores an unsupported remote ROMLOADER component when installing selected targets', async () => {
|
|
4459
|
+
const method = new FirmwareUpdateV4({
|
|
4460
|
+
id: 1,
|
|
4461
|
+
payload: {
|
|
4462
|
+
method: 'firmwareUpdateV4',
|
|
4463
|
+
platform: 'web',
|
|
4464
|
+
targetsToUpdate: ['app_v1'],
|
|
4465
|
+
},
|
|
4466
|
+
});
|
|
4467
|
+
method.init();
|
|
4468
|
+
const applicationBinary = new Uint8Array([1]).buffer;
|
|
4469
|
+
const downloadRemoteComponent = jest.fn().mockResolvedValue({
|
|
4470
|
+
fileName: 'application_p1.bin',
|
|
4471
|
+
binary: applicationBinary,
|
|
4472
|
+
targetId: 4,
|
|
4473
|
+
kind: 'firmware',
|
|
4474
|
+
});
|
|
4475
|
+
(method as any).downloadRemoteProtocolV2Component = downloadRemoteComponent;
|
|
4476
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
4477
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
4478
|
+
.mockReturnValue({
|
|
4479
|
+
required: false,
|
|
4480
|
+
version: [1, 0, 0],
|
|
4481
|
+
url: '',
|
|
4482
|
+
installOrder: ['romloader', 'applicationP1'],
|
|
4483
|
+
components: {
|
|
4484
|
+
romloader: {
|
|
4485
|
+
target: 'ROMLOADER',
|
|
4486
|
+
url: 'https://example.com/romloader.bin',
|
|
4487
|
+
},
|
|
4488
|
+
applicationP1: {
|
|
4489
|
+
target: 'APPLICATION_P1',
|
|
4490
|
+
url: 'https://example.com/application-p1.bin',
|
|
4491
|
+
},
|
|
4492
|
+
},
|
|
4493
|
+
fingerprint: '',
|
|
4494
|
+
changelog: { 'zh-CN': '', 'en-US': '' },
|
|
4495
|
+
});
|
|
4496
|
+
|
|
4497
|
+
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
|
|
4498
|
+
deviceType: 'pro2',
|
|
4499
|
+
firmwareVersion: '0.0.0',
|
|
4500
|
+
});
|
|
4501
|
+
|
|
4502
|
+
expect(downloadRemoteComponent).toHaveBeenCalledTimes(1);
|
|
4503
|
+
expect(downloadRemoteComponent).toHaveBeenCalledWith(
|
|
4504
|
+
'applicationP1',
|
|
4505
|
+
expect.objectContaining({ target: 'APPLICATION_P1' })
|
|
4506
|
+
);
|
|
4507
|
+
expect(remoteBinaries.fwBinaryMap).toEqual([
|
|
4508
|
+
{ fileName: 'application_p1.bin', binary: applicationBinary, targetId: 4 },
|
|
4509
|
+
]);
|
|
4510
|
+
|
|
4511
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
4512
|
+
});
|
|
4513
|
+
|
|
3929
4514
|
test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
|
|
3930
4515
|
const method = new FirmwareUpdateV4({
|
|
3931
4516
|
id: 1,
|
|
@@ -4496,8 +5081,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4496
5081
|
return 0 as any;
|
|
4497
5082
|
}) as typeof setTimeout);
|
|
4498
5083
|
|
|
5084
|
+
const initialize = jest.fn().mockResolvedValue(undefined);
|
|
4499
5085
|
(method as any).device = stubDevice({
|
|
4500
5086
|
getCommands: () => ({ typedCall }),
|
|
5087
|
+
initialize,
|
|
5088
|
+
});
|
|
5089
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5090
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue({
|
|
5091
|
+
hw: { serial_no: 'PRO2-PHYSICAL-1' },
|
|
4501
5092
|
});
|
|
4502
5093
|
method.postProgressMessage = jest.fn();
|
|
4503
5094
|
|
|
@@ -4513,6 +5104,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4513
5104
|
}
|
|
4514
5105
|
|
|
4515
5106
|
expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
|
|
5107
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
5108
|
+
expect((method as any).verifyProtocolV2ReconnectIdentity).toHaveBeenCalledTimes(1);
|
|
5109
|
+
expect(initialize).toHaveBeenCalledTimes(1);
|
|
4516
5110
|
});
|
|
4517
5111
|
|
|
4518
5112
|
test('rejects a chunk-relative processed_byte during firmware staging', async () => {
|
|
@@ -5213,6 +5807,45 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
5213
5807
|
]);
|
|
5214
5808
|
});
|
|
5215
5809
|
|
|
5810
|
+
test('does not replay a business method when the hidden-wallet session cannot be restored after unlock', async () => {
|
|
5811
|
+
const initialError = deviceLockedError();
|
|
5812
|
+
const restoreError = new Error('Invalid wallet session');
|
|
5813
|
+
const method = {
|
|
5814
|
+
name: 'evmSignMessage',
|
|
5815
|
+
payload: { passphraseState: 'hidden-state' },
|
|
5816
|
+
useDevicePassphraseState: true,
|
|
5817
|
+
unlockPolicy: 'retry-on-locked',
|
|
5818
|
+
run: jest.fn().mockRejectedValueOnce(initialError),
|
|
5819
|
+
};
|
|
5820
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
5821
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
5822
|
+
return Promise.resolve({ message: { version: 2 } });
|
|
5823
|
+
}
|
|
5824
|
+
if (requestType === 'DeviceSessionGet') {
|
|
5825
|
+
return Promise.reject(restoreError);
|
|
5826
|
+
}
|
|
5827
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
5828
|
+
});
|
|
5829
|
+
const device = {
|
|
5830
|
+
features: { unlocked: true, passphraseProtection: true },
|
|
5831
|
+
passphraseState: 'hidden-state',
|
|
5832
|
+
commands: { typedCall },
|
|
5833
|
+
isProtocolV2: () => true,
|
|
5834
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
5835
|
+
getCurrentPassphraseProtection: () => true,
|
|
5836
|
+
getInternalState: () => 'hidden-session',
|
|
5837
|
+
clearInternalState: jest.fn(),
|
|
5838
|
+
getCurrentDeviceId: () => 'wallet-device-id',
|
|
5839
|
+
updateInternalState: jest.fn(),
|
|
5840
|
+
};
|
|
5841
|
+
|
|
5842
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(restoreError);
|
|
5843
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
5844
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
5845
|
+
expect(device.clearInternalState).toHaveBeenCalledTimes(1);
|
|
5846
|
+
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
5847
|
+
});
|
|
5848
|
+
|
|
5216
5849
|
test('restores the expected hidden-wallet session after pre-unlock without selecting Attach PIN', async () => {
|
|
5217
5850
|
const calls: string[] = [];
|
|
5218
5851
|
const method = {
|