@onekeyfe/hd-core 1.2.0-alpha.35 → 1.2.0-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +74 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/get-device-state.test.ts +104 -12
  4. package/__tests__/open-wallet-session.test.ts +131 -7
  5. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  6. package/__tests__/protocol-v2.test.ts +290 -39
  7. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  8. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  9. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  10. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  11. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  12. package/dist/core/deviceEventRegistration.d.ts +2 -0
  13. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  14. package/dist/core/index.d.ts.map +1 -1
  15. package/dist/device/Device.d.ts +22 -4
  16. package/dist/device/Device.d.ts.map +1 -1
  17. package/dist/events/device.d.ts +4 -0
  18. package/dist/events/device.d.ts.map +1 -1
  19. package/dist/events/ui-request.d.ts +27 -2
  20. package/dist/events/ui-request.d.ts.map +1 -1
  21. package/dist/index.d.ts +56 -7
  22. package/dist/index.js +692 -397
  23. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  24. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  25. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  26. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  27. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  28. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  29. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  30. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  31. package/dist/utils/patch.d.ts +1 -1
  32. package/package.json +4 -4
  33. package/src/api/BaseMethod.ts +1 -1
  34. package/src/api/FirmwareUpdateV4.ts +13 -6
  35. package/src/api/GetPassphraseState.ts +7 -1
  36. package/src/api/OpenWalletSession.ts +18 -3
  37. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +24 -2
  38. package/src/api/device/DeviceUnlock.ts +1 -1
  39. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  40. package/src/core/deviceEventRegistration.ts +4 -0
  41. package/src/core/index.ts +46 -4
  42. package/src/data/messages/messages-protocol-v2.json +15 -0
  43. package/src/device/Device.ts +221 -24
  44. package/src/events/device.ts +4 -0
  45. package/src/events/ui-request.ts +32 -2
  46. package/src/protocols/protocol-v2/features.ts +9 -2
  47. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  48. package/src/protocols/protocol-v2/unlockRetry.ts +33 -11
  49. package/src/protocols/protocol-v2/walletSession.ts +140 -32
  50. package/src/utils/deviceFeaturesUtils.ts +4 -0
@@ -18,6 +18,79 @@ class TestAllNetworkMethod extends AllNetworkGetAddressBase {
18
18
  }
19
19
 
20
20
  describe('AllNetworkGetAddressBase tracing', () => {
21
+ test('resumes a Protocol V2 hidden wallet before running a nested chain method', async () => {
22
+ const calls: string[] = [];
23
+ const checkPassphraseStateSafety = jest.fn().mockImplementation(() => {
24
+ calls.push('resume-hidden-session');
25
+ return Promise.resolve(true);
26
+ });
27
+ const innerMethod = {
28
+ checkSafetyLevelOnTestNet: jest.fn().mockResolvedValue(false),
29
+ connectId: 'connect-id',
30
+ deviceId: 'device-id',
31
+ getVersionRange: jest.fn().mockReturnValue({}),
32
+ assertProtocolSupported: jest.fn(),
33
+ init: jest.fn(),
34
+ name: 'evmGetAddress',
35
+ responseID: 43,
36
+ run: jest.fn().mockImplementation(() => {
37
+ calls.push('run-chain-method');
38
+ return Promise.resolve([{ address: '0xhidden' }]);
39
+ }),
40
+ setDevice: jest.fn(),
41
+ strictCheckDeviceSupport: false,
42
+ };
43
+ (findMethod as jest.Mock).mockReturnValue(innerMethod);
44
+ const method = new TestAllNetworkMethod({
45
+ id: 1,
46
+ payload: {
47
+ method: 'allNetworkGetAddress',
48
+ connectId: 'connect-id',
49
+ deviceId: 'device-id',
50
+ passphraseState: 'hidden-state',
51
+ bundle: [],
52
+ },
53
+ });
54
+ method.device = {
55
+ checkPassphraseStateSafety,
56
+ commands: {
57
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
58
+ },
59
+ getCurrentFirmwareType: jest.fn(),
60
+ getProtocol: jest.fn().mockReturnValue('V2'),
61
+ getCurrentFirmwareVersionString: jest.fn().mockReturnValue('1.0.0'),
62
+ getCurrentMethodVersionRange: jest
63
+ .fn()
64
+ .mockImplementation((getRange: (type: string) => unknown) => getRange('pro2')),
65
+ instanceId: 'device-instance',
66
+ isProtocolV2: jest.fn().mockReturnValue(true),
67
+ isBootloader: jest.fn().mockReturnValue(false),
68
+ isRomloader: jest.fn().mockReturnValue(false),
69
+ off: jest.fn(),
70
+ on: jest.fn(),
71
+ state: { status: { unlocked: true } },
72
+ updateProtocolV2Status: jest.fn(),
73
+ } as any;
74
+
75
+ await method.callMethod(
76
+ 'evmGetAddress',
77
+ {
78
+ bundle: [
79
+ {
80
+ _originRequestParams: {
81
+ network: 'evm',
82
+ path: "m/44'/60'/0'/0/0",
83
+ },
84
+ },
85
+ ],
86
+ },
87
+ 0
88
+ );
89
+
90
+ expect(checkPassphraseStateSafety).toHaveBeenCalledWith('hidden-state', false, undefined);
91
+ expect(calls).toEqual(['resume-hidden-session', 'run-chain-method']);
92
+ });
93
+
21
94
  test('releases the nested request context when an unhandled error escapes', async () => {
22
95
  const deviceInstanceId = 'device-instance';
23
96
  const innerMethod = {
@@ -51,6 +124,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
51
124
  getCurrentMethodVersionRange: jest
52
125
  .fn()
53
126
  .mockImplementation((getRange: (type: string) => unknown) => getRange('classic')),
127
+ isProtocolV2: jest.fn().mockReturnValue(false),
54
128
  off: jest.fn(),
55
129
  on: jest.fn(),
56
130
  } as any;
@@ -3,6 +3,8 @@ import { registerHardwareUiEventListeners } from '../src/core/deviceEventRegistr
3
3
 
4
4
  const handlers = {
5
5
  pin: jest.fn(),
6
+ pinOnDevice: jest.fn(),
7
+ pinOnDeviceComplete: jest.fn(),
6
8
  button: jest.fn(),
7
9
  passphrase: jest.fn(),
8
10
  passphraseOnDevice: jest.fn(),
@@ -21,6 +23,8 @@ describe('hardware UI event registration', () => {
21
23
  expect(registerHardwareUiEventListeners(device as any, handlers)).toBe(true);
22
24
  expect(device.on.mock.calls.map(([type]) => type)).toEqual([
23
25
  DEVICE.PIN,
26
+ DEVICE.PIN_ON_DEVICE,
27
+ DEVICE.PIN_ON_DEVICE_COMPLETE,
24
28
  DEVICE.BUTTON,
25
29
  DEVICE.PASSPHRASE,
26
30
  DEVICE.PASSPHRASE_ON_DEVICE,
@@ -34,6 +38,8 @@ describe('hardware UI event registration', () => {
34
38
  expect(registerHardwareUiEventListeners(device as any, handlers)).toBe(true);
35
39
  expect(device.on.mock.calls.map(([type]) => type)).toEqual([
36
40
  DEVICE.PIN,
41
+ DEVICE.PIN_ON_DEVICE,
42
+ DEVICE.PIN_ON_DEVICE_COMPLETE,
37
43
  DEVICE.BUTTON,
38
44
  DEVICE.PASSPHRASE,
39
45
  DEVICE.PASSPHRASE_ON_DEVICE,
@@ -1,4 +1,4 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import { DeviceType } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import { Device } from '../src/device/Device';
@@ -43,6 +43,70 @@ const getProtocolV2LoaderInfo = (mode: 'bootloader' | 'romloader') => ({
43
43
  });
44
44
 
45
45
  describe('getDeviceState', () => {
46
+ test('coalesces concurrent Protocol V2 runtime-context negotiation', async () => {
47
+ let resolveProtocolInfo:
48
+ | ((value: { message: typeof protocolV2ApplicationInfo }) => void)
49
+ | undefined;
50
+ const typedCall = jest.fn(
51
+ () =>
52
+ new Promise<{ message: typeof protocolV2ApplicationInfo }>(resolve => {
53
+ resolveProtocolInfo = resolve;
54
+ })
55
+ );
56
+ const device = createV2Device(typedCall);
57
+
58
+ const first = device.ensureProtocolV2RuntimeContext();
59
+ const second = device.ensureProtocolV2RuntimeContext();
60
+ resolveProtocolInfo?.({ message: protocolV2ApplicationInfo });
61
+
62
+ await expect(Promise.all([first, second])).resolves.toEqual([
63
+ protocolV2ApplicationInfo,
64
+ protocolV2ApplicationInfo,
65
+ ]);
66
+ expect(typedCall).toHaveBeenCalledTimes(1);
67
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
68
+ eventless_wallet_session: true,
69
+ });
70
+ });
71
+
72
+ test('renegotiates Protocol V2 runtime context after transport disconnect', async () => {
73
+ const typedCall = jest.fn().mockResolvedValue({ message: protocolV2ApplicationInfo });
74
+ const device = createV2Device(typedCall);
75
+
76
+ await device.ensureProtocolV2RuntimeContext();
77
+ await device.ensureProtocolV2RuntimeContext();
78
+ device.markTransportDisconnected();
79
+ await device.ensureProtocolV2RuntimeContext();
80
+
81
+ expect(typedCall).toHaveBeenCalledTimes(2);
82
+ });
83
+
84
+ test('rejects every waiter when runtime-context negotiation is invalidated', async () => {
85
+ let resolveProtocolInfo:
86
+ | ((value: { message: typeof protocolV2ApplicationInfo }) => void)
87
+ | undefined;
88
+ const typedCall = jest.fn(
89
+ () =>
90
+ new Promise<{ message: typeof protocolV2ApplicationInfo }>(resolve => {
91
+ resolveProtocolInfo = resolve;
92
+ })
93
+ );
94
+ const device = createV2Device(typedCall);
95
+
96
+ const first = device.ensureProtocolV2RuntimeContext();
97
+ const second = device.ensureProtocolV2RuntimeContext();
98
+ const firstExpectation = expect(first).rejects.toMatchObject({
99
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
100
+ });
101
+ const secondExpectation = expect(second).rejects.toMatchObject({
102
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
103
+ });
104
+ device.markTransportDisconnected();
105
+ resolveProtocolInfo?.({ message: protocolV2ApplicationInfo });
106
+
107
+ await Promise.all([firstExpectation, secondExpectation]);
108
+ });
109
+
46
110
  test('does not expose the internal wallet session', async () => {
47
111
  const device = createV2Device(jest.fn());
48
112
  device.updateState({ protocol: 'V2' }, 'initialize');
@@ -159,15 +223,20 @@ describe('getDeviceState', () => {
159
223
  throw new Error(`Unexpected request: ${requestType}`);
160
224
  });
161
225
  const device = createV2Device(typedCall);
162
- device.updateState({ protocol: 'V2', status: { mode } }, 'initialize');
226
+ device.updateState(
227
+ {
228
+ protocol: 'V2',
229
+ identity: { deviceType: EDeviceType.Pro2 },
230
+ status: { mode },
231
+ raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo(mode) },
232
+ },
233
+ 'initialize'
234
+ );
163
235
 
164
236
  const state = await device.getDeviceState({ refreshSections: ['status'] });
165
237
 
166
238
  expect(state.status.mode).toBe(mode);
167
- expect(typedCall.mock.calls.map(call => call[0])).toEqual([
168
- 'DeviceInfoGet',
169
- 'ProtocolInfoRequest',
170
- ]);
239
+ expect(typedCall).not.toHaveBeenCalled();
171
240
  }
172
241
  );
173
242
 
@@ -187,7 +256,14 @@ describe('getDeviceState', () => {
187
256
  return { message: { init_states: true, unlocked: true, device_id: 'device-1' } };
188
257
  });
189
258
  const device = createV2Device(typedCall);
190
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
259
+ device.updateState(
260
+ {
261
+ protocol: 'V2',
262
+ status: { mode: 'normal' },
263
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
264
+ },
265
+ 'initialize'
266
+ );
191
267
 
192
268
  const state = await device.getDeviceState({ refreshSections: ['status'] });
193
269
 
@@ -205,7 +281,14 @@ describe('getDeviceState', () => {
205
281
  },
206
282
  });
207
283
  const device = createV2Device(typedCall);
208
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
284
+ device.updateState(
285
+ {
286
+ protocol: 'V2',
287
+ status: { mode: 'normal' },
288
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
289
+ },
290
+ 'initialize'
291
+ );
209
292
  const onState = jest.fn();
210
293
  device.on(DEVICE.STATE, onState);
211
294
 
@@ -250,13 +333,18 @@ describe('getDeviceState', () => {
250
333
  throw new Error(`Unexpected request: ${requestType}`);
251
334
  });
252
335
  const device = createV2Device(typedCall);
253
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
336
+ device.updateState(
337
+ {
338
+ protocol: 'V2',
339
+ status: { mode: 'normal' },
340
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
341
+ },
342
+ 'initialize'
343
+ );
254
344
 
255
345
  const state = await device.getDeviceState({ refreshSections: ['status', 'settings'] });
256
346
 
257
347
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
258
- 'DeviceInfoGet',
259
- 'ProtocolInfoRequest',
260
348
  'DeviceStatusGet',
261
349
  'DeviceSettingsGet',
262
350
  ]);
@@ -288,7 +376,11 @@ describe('getDeviceState', () => {
288
376
  });
289
377
  const device = createV2Device(typedCall);
290
378
  device.updateState(
291
- { protocol: 'V2', status: { mode: 'normal', unlocked: true } },
379
+ {
380
+ protocol: 'V2',
381
+ status: { mode: 'normal', unlocked: true },
382
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
383
+ },
292
384
  'initialize'
293
385
  );
294
386
 
@@ -1,10 +1,11 @@
1
1
  import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
+ import { DeviceSessionPinType, DeviceSessionSeedDomain } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import GetPassphraseState from '../src/api/GetPassphraseState';
5
5
  import OpenWalletSession from '../src/api/OpenWalletSession';
6
6
  import { Device } from '../src/device/Device';
7
7
  import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
8
+ import { ensureProtocolV2WalletSessionUnlocked } from '../src/protocols/protocol-v2/walletSession';
8
9
 
9
10
  jest.mock('../src/data/config', () => ({
10
11
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -43,6 +44,11 @@ const createDevice = ({
43
44
  }),
44
45
  promptPassphrase,
45
46
  },
47
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
48
+ device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
49
+ eventless_wallet_session: true,
50
+ })
51
+ ),
46
52
  getDeviceState: jest.fn().mockResolvedValue({
47
53
  identity: { deviceId: 'device-1' },
48
54
  status: { passphraseProtection },
@@ -82,6 +88,50 @@ describe('openWalletSession', () => {
82
88
  deviceWalletSessionStore.clear();
83
89
  });
84
90
 
91
+ test('unlocks a locked Protocol V2 device before restoring a wallet session', async () => {
92
+ const device = {
93
+ commands: {
94
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
95
+ },
96
+ isBootloader: jest.fn().mockReturnValue(false),
97
+ isProtocolV2: jest.fn().mockReturnValue(true),
98
+ isRomloader: jest.fn().mockReturnValue(false),
99
+ state: { status: { unlocked: false } },
100
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
101
+ updateProtocolV2Status: jest.fn(function updateProtocolV2Status(
102
+ this: { state: { status: { unlocked: boolean } } },
103
+ status: { unlocked?: boolean }
104
+ ) {
105
+ this.state.status.unlocked = status.unlocked ?? this.state.status.unlocked;
106
+ }),
107
+ };
108
+
109
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(true);
110
+ expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
111
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
112
+ source: 'unlock-coordinator',
113
+ reason: 'device-locked',
114
+ deviceOnly: true,
115
+ });
116
+ });
117
+
118
+ test('does not request PIN when wallet-session recovery finds the device unlocked', async () => {
119
+ const device = {
120
+ commands: {
121
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
122
+ },
123
+ isBootloader: jest.fn().mockReturnValue(false),
124
+ isProtocolV2: jest.fn().mockReturnValue(true),
125
+ isRomloader: jest.fn().mockReturnValue(false),
126
+ state: { status: { unlocked: true } },
127
+ unlockDevice: jest.fn(),
128
+ updateProtocolV2Status: jest.fn(),
129
+ };
130
+
131
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(false);
132
+ expect(device.unlockDevice).not.toHaveBeenCalled();
133
+ });
134
+
85
135
  test.each([{ useEmptyPassphrase: true }, { initSession: true }, {}])(
86
136
  'requires the explicit mode in the new public API: %p',
87
137
  legacyParams => {
@@ -479,6 +529,7 @@ describe('openWalletSession', () => {
479
529
  });
480
530
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
481
531
  session_id: 'known-session',
532
+ btc_test_address: 'hidden-state',
482
533
  });
483
534
  expect(promptPassphrase).not.toHaveBeenCalled();
484
535
  });
@@ -656,7 +707,11 @@ describe('openWalletSession', () => {
656
707
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
657
708
  eventless_wallet_session: true,
658
709
  });
659
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
710
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
711
+ source: 'wallet-session-coordinator',
712
+ reason: 'open-wallet',
713
+ deviceOnly: true,
714
+ });
660
715
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
661
716
  expect(promptPassphrase).not.toHaveBeenCalled();
662
717
  expect(device.passphraseState).toBeUndefined();
@@ -701,7 +756,11 @@ describe('openWalletSession', () => {
701
756
  passphraseState: null,
702
757
  resumed: false,
703
758
  });
704
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
759
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
760
+ source: 'wallet-session-coordinator',
761
+ reason: 'open-wallet',
762
+ deviceOnly: true,
763
+ });
705
764
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
706
765
  });
707
766
 
@@ -721,6 +780,14 @@ describe('openWalletSession', () => {
721
780
  method.init();
722
781
  const device = createDevice({ typedCall });
723
782
  device.features.unlockedAttachPin = true;
783
+ device.getDeviceState = jest.fn().mockResolvedValue({
784
+ identity: { deviceId: 'device-1' },
785
+ status: {
786
+ unlocked: true,
787
+ unlockedAttachPin: true,
788
+ passphraseProtection: true,
789
+ },
790
+ });
724
791
  device.unlockDevice = jest.fn().mockImplementation(() => {
725
792
  device.features.unlockedAttachPin = false;
726
793
  return Promise.resolve(device.features);
@@ -731,7 +798,11 @@ describe('openWalletSession', () => {
731
798
  walletType: 'standard',
732
799
  passphraseState: null,
733
800
  });
734
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
801
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
802
+ source: 'wallet-session-coordinator',
803
+ reason: 'open-wallet',
804
+ deviceOnly: true,
805
+ });
735
806
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
736
807
  });
737
808
 
@@ -939,7 +1010,9 @@ describe('openWalletSession', () => {
939
1010
  },
940
1011
  { cancelDeviceOnReject: false }
941
1012
  );
942
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
1013
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
1014
+ emitUiEvent: false,
1015
+ });
943
1016
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
944
1017
  });
945
1018
 
@@ -1180,6 +1253,7 @@ describe('openWalletSession', () => {
1180
1253
  });
1181
1254
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1182
1255
  session_id: 'known-session',
1256
+ btc_test_address: 'hidden-state',
1183
1257
  });
1184
1258
  expect(promptPassphrase).not.toHaveBeenCalled();
1185
1259
  });
@@ -1234,6 +1308,7 @@ describe('openWalletSession', () => {
1234
1308
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
1235
1309
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1236
1310
  session_id: 'known-session',
1311
+ btc_test_address: 'hidden-state',
1237
1312
  });
1238
1313
  const sessionGetCall = typedCall.mock.calls.findIndex(
1239
1314
  ([requestName]) => requestName === 'DeviceSessionGet'
@@ -1326,6 +1401,7 @@ describe('openWalletSession', () => {
1326
1401
  });
1327
1402
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1328
1403
  session_id: 'expired-session',
1404
+ btc_test_address: 'hidden-state',
1329
1405
  });
1330
1406
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
1331
1407
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
@@ -1335,7 +1411,6 @@ describe('openWalletSession', () => {
1335
1411
  const typedCall = jest
1336
1412
  .fn()
1337
1413
  .mockResolvedValueOnce({ message: { version: 2 } })
1338
- .mockResolvedValueOnce({ message: {} })
1339
1414
  .mockResolvedValueOnce({
1340
1415
  message: {
1341
1416
  btc_test_address: 'hidden-state',
@@ -1362,7 +1437,56 @@ describe('openWalletSession', () => {
1362
1437
  passphraseState: 'hidden-state',
1363
1438
  resumed: false,
1364
1439
  });
1365
- expect(promptPassphrase).toHaveBeenCalledTimes(1);
1440
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1441
+ btc_test_address: 'hidden-state',
1442
+ });
1443
+ expect(promptPassphrase).not.toHaveBeenCalled();
1366
1444
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
1367
1445
  });
1446
+
1447
+ test.each([
1448
+ {
1449
+ deriveCardano: false,
1450
+ seedDomains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1451
+ },
1452
+ {
1453
+ deriveCardano: true,
1454
+ seedDomains: [
1455
+ DeviceSessionSeedDomain.SeedDomain_Standard,
1456
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
1457
+ ],
1458
+ },
1459
+ ])('requests the selected seed domains: $seedDomains', async ({ deriveCardano, seedDomains }) => {
1460
+ const typedCall = jest
1461
+ .fn()
1462
+ .mockResolvedValueOnce({ message: { version: 2 } })
1463
+ .mockResolvedValueOnce({ message: {} })
1464
+ .mockResolvedValueOnce({
1465
+ message: {
1466
+ btc_test_address: 'hidden-state',
1467
+ session_id: 'new-session',
1468
+ },
1469
+ });
1470
+ const method = new OpenWalletSession({
1471
+ payload: {
1472
+ method: 'openWalletSession',
1473
+ connectId: 'connect-id',
1474
+ mode: 'select-hidden',
1475
+ deriveCardano,
1476
+ },
1477
+ });
1478
+ method.init();
1479
+ method.device = createDevice({
1480
+ typedCall,
1481
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
1482
+ }) as any;
1483
+
1484
+ await expect(method.run()).resolves.toMatchObject({
1485
+ walletType: 'hidden',
1486
+ passphraseState: 'hidden-state',
1487
+ });
1488
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1489
+ seed_domains: seedDomains,
1490
+ });
1491
+ });
1368
1492
  });
@@ -0,0 +1,56 @@
1
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
+
3
+ import { DEVICE } from '../src/events';
4
+ import { Device } from '../src/device/Device';
5
+
6
+ jest.mock('../src/data/config', () => ({
7
+ getSDKVersion: jest.fn(() => '1.0.0-test'),
8
+ DEFAULT_DOMAIN: 'https://example.com/',
9
+ }));
10
+
11
+ describe('Protocol V2 UI interaction lifecycle', () => {
12
+ test('emits a matching PIN phase completion after DeviceSessionAskPin succeeds', async () => {
13
+ const device = new Device({ id: 'connect-1' } as any);
14
+ device.isProtocolV2 = jest.fn(() => true);
15
+ device.updateProtocolV2Status = jest.fn(() => undefined as any);
16
+ device.commands = {
17
+ typedCall: jest.fn().mockResolvedValue({
18
+ message: { unlocked: true },
19
+ }),
20
+ } as any;
21
+ device.beginProtocolV2UiInteraction();
22
+
23
+ const starts: unknown[] = [];
24
+ const completions: unknown[] = [];
25
+ device.on(DEVICE.PIN_ON_DEVICE, (...args) => starts.push(args));
26
+ device.on(DEVICE.PIN_ON_DEVICE_COMPLETE, (...args) => completions.push(args));
27
+
28
+ await device.unlockDevice(DeviceSessionPinType.Main, {
29
+ source: 'unlock-coordinator',
30
+ reason: 'device-locked',
31
+ deviceOnly: true,
32
+ });
33
+
34
+ const startMetadata = (starts[0] as unknown[])[2] as Record<string, unknown>;
35
+ const completionMetadata = (completions[0] as unknown[])[1] as Record<string, unknown>;
36
+ expect(startMetadata.interaction).toMatchObject({
37
+ phase: 'pin',
38
+ transition: 'start',
39
+ protocol: 'V2',
40
+ });
41
+ expect(completionMetadata).toMatchObject({
42
+ interactionId: (startMetadata.interaction as { interactionId: string }).interactionId,
43
+ phaseId: (startMetadata.interaction as { phaseId: string }).phaseId,
44
+ phase: 'pin',
45
+ transition: 'complete',
46
+ outcome: 'succeeded',
47
+ protocol: 'V2',
48
+ });
49
+ expect(completionMetadata.sequence).toBeGreaterThan(
50
+ (startMetadata.interaction as { sequence: number }).sequence
51
+ );
52
+ expect(completionMetadata.phaseId).toBe(
53
+ (startMetadata.interaction as { phaseId: string }).phaseId
54
+ );
55
+ });
56
+ });