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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/__tests__/DeviceCommands.test.ts +84 -0
  2. package/__tests__/device-settings.test.ts +3 -0
  3. package/__tests__/device-wallet-session-store.test.ts +80 -7
  4. package/__tests__/evmSignTransaction.test.ts +69 -0
  5. package/__tests__/get-device-state.test.ts +78 -30
  6. package/__tests__/open-wallet-session.test.ts +186 -51
  7. package/__tests__/protocol-v2.test.ts +490 -112
  8. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  9. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  10. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  11. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  12. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  13. package/dist/api/evm/protocol.d.ts +3 -0
  14. package/dist/api/evm/protocol.d.ts.map +1 -0
  15. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
  16. package/dist/device/Device.d.ts +3 -2
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/device/DeviceCommands.d.ts.map +1 -1
  19. package/dist/device/DeviceWalletSessionStore.d.ts +0 -1
  20. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
  21. package/dist/index.d.ts +8 -6
  22. package/dist/index.js +606 -641
  23. package/dist/protocols/protocol-v2/features.d.ts +17 -6
  24. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  25. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  26. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  27. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts +1 -0
  28. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  29. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -0
  31. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  32. package/dist/types/api/openWalletSession.d.ts +1 -1
  33. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  34. package/dist/types/api/protocolV2.d.ts +2 -2
  35. package/dist/types/api/protocolV2.d.ts.map +1 -1
  36. package/dist/types/device.d.ts +2 -1
  37. package/dist/types/device.d.ts.map +1 -1
  38. package/dist/utils/patch.d.ts +1 -1
  39. package/dist/utils/patch.d.ts.map +1 -1
  40. package/package.json +4 -4
  41. package/src/api/FirmwareUpdateV4.ts +26 -8
  42. package/src/api/GetPassphraseState.ts +11 -0
  43. package/src/api/OpenWalletSession.ts +15 -7
  44. package/src/api/evm/EVMGetAddress.ts +2 -2
  45. package/src/api/evm/EVMGetPublicKey.ts +2 -2
  46. package/src/api/evm/EVMSignMessage.ts +2 -2
  47. package/src/api/evm/EVMSignTransaction.ts +2 -2
  48. package/src/api/evm/EVMSignTypedData.ts +35 -45
  49. package/src/api/evm/EVMVerifyMessage.ts +2 -2
  50. package/src/api/evm/protocol.ts +6 -0
  51. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
  52. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  53. package/src/data/messages/messages-protocol-v2.json +393 -485
  54. package/src/device/Device.ts +63 -37
  55. package/src/device/DeviceCommands.ts +12 -2
  56. package/src/device/DeviceWalletSessionStore.ts +0 -14
  57. package/src/deviceProfile/buildDeviceFeatures.ts +1 -1
  58. package/src/protocols/protocol-v2/features.ts +58 -16
  59. package/src/protocols/protocol-v2/index.ts +5 -0
  60. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
  61. package/src/protocols/protocol-v2/unlockRetry.ts +18 -7
  62. package/src/protocols/protocol-v2/walletSession.ts +35 -35
  63. package/src/types/api/openWalletSession.ts +1 -1
  64. package/src/types/api/protocolV2.ts +2 -2
  65. package/src/types/device.ts +2 -0
@@ -19,6 +19,90 @@ const createCommands = () => {
19
19
  };
20
20
 
21
21
  describe('DeviceCommands failure mapping', () => {
22
+ it('logs passphrase call request and response without exposing the passphrase', async () => {
23
+ const commands = createCommands();
24
+ const requestLog = getLogger(LoggerNames.DeviceCommands);
25
+ const responseLog = getLogger(LoggerNames.Core);
26
+ requestLog.messages.length = 0;
27
+ responseLog.messages.length = 0;
28
+ commands.mainId = 'main-id';
29
+ commands.transport = {
30
+ call: jest.fn().mockResolvedValue({
31
+ type: 'Success',
32
+ message: {
33
+ message: 'Passphrase accepted',
34
+ },
35
+ }),
36
+ } as any;
37
+
38
+ await expect(
39
+ commands.call('DeviceSessionAskPassphrase', {
40
+ passphrase: 'hidden-wallet-secret',
41
+ on_device: false,
42
+ })
43
+ ).resolves.toMatchObject({ type: 'Success' });
44
+
45
+ expect(requestLog.messages.at(-1)?.message).toEqual([
46
+ '[DeviceCommands] [call] Sending',
47
+ 'DeviceSessionAskPassphrase',
48
+ {
49
+ passphrase: '[REDACTED]',
50
+ on_device: false,
51
+ },
52
+ ]);
53
+ expect(responseLog.messages.at(-1)?.message).toEqual([
54
+ '[DeviceCommands] [call] Received',
55
+ 'Success',
56
+ {
57
+ message: 'Passphrase accepted',
58
+ },
59
+ ]);
60
+ expect(JSON.stringify([...requestLog.messages, ...responseLog.messages])).not.toContain(
61
+ 'hidden-wallet-secret'
62
+ );
63
+ });
64
+
65
+ it('logs DeviceStatus response fields without exposing the device ID', async () => {
66
+ const commands = createCommands();
67
+ const log = getLogger(LoggerNames.Core);
68
+ log.messages.length = 0;
69
+ commands.mainId = 'main-id';
70
+ commands.transport = {
71
+ call: jest.fn().mockResolvedValue({
72
+ type: 'DeviceStatus',
73
+ message: {
74
+ device_id: 'sensitive-device-id',
75
+ unlocked: true,
76
+ init_states: true,
77
+ backup_required: false,
78
+ passphrase_enabled: true,
79
+ attach_to_pin_enabled: false,
80
+ unlocked_by_attach_to_pin: false,
81
+ },
82
+ }),
83
+ } as any;
84
+
85
+ await expect(commands.call('DeviceStatusGet', {})).resolves.toMatchObject({
86
+ type: 'DeviceStatus',
87
+ });
88
+
89
+ const receivedLog = log.messages.at(-1)?.message;
90
+ expect(receivedLog).toEqual([
91
+ '[DeviceCommands] [call] Received',
92
+ 'DeviceStatus',
93
+ {
94
+ device_id: '[REDACTED]',
95
+ unlocked: true,
96
+ init_states: true,
97
+ backup_required: false,
98
+ passphrase_enabled: true,
99
+ attach_to_pin_enabled: false,
100
+ unlocked_by_attach_to_pin: false,
101
+ },
102
+ ]);
103
+ expect(JSON.stringify(receivedLog)).not.toContain('sensitive-device-id');
104
+ });
105
+
22
106
  it('does not log DeviceSessionGet response secrets', async () => {
23
107
  const commands = createCommands();
24
108
  const log = getLogger(LoggerNames.DeviceCommands);
@@ -40,6 +40,9 @@ describe('DeviceSettings protocol routing', () => {
40
40
  ['language', { language: 'ja-JP' }],
41
41
  ['brightness', { brightness: 80 }],
42
42
  ['haptic feedback', { hapticFeedback: true }],
43
+ ['animation', { animationEnabled: true }],
44
+ ['tap to wake', { tapToWake: true }],
45
+ ['device name display', { deviceNameDisplayEnabled: true }],
43
46
  ])('does not unlock Protocol V2 before changing %s', (_name, settings) => {
44
47
  const method = new DeviceSettings({
45
48
  id: 1,
@@ -1,6 +1,7 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import ClearSessionCache from '../src/api/ClearSessionCache';
4
+ import GetPassphraseState from '../src/api/GetPassphraseState';
4
5
  import DeviceWipe from '../src/api/device/DeviceWipe';
5
6
  import {
6
7
  DeviceWalletSessionStore,
@@ -35,15 +36,18 @@ describe('DeviceWalletSessionStore', () => {
35
36
  expect(store.get('device-2', 'hidden-a')).toBe('session-c');
36
37
  });
37
38
 
38
- test('evicts the oldest wallet session when a device exceeds the SE capacity of three', () => {
39
+ test('keeps SDK session handles without mirroring hardware eviction policy', () => {
39
40
  const store = new DeviceWalletSessionStore();
40
41
  store.setStandard('device-1', 'wallet-a', 'session-a');
41
42
  store.set('device-1', 'wallet-b', 'session-b');
42
43
  store.set('device-1', 'wallet-c', 'session-c');
43
44
  store.set('device-1', 'wallet-d', 'session-d');
44
45
 
45
- expect(store.get('device-1', 'wallet-a')).toBeUndefined();
46
- expect(store.getStandard('device-1')).toBeUndefined();
46
+ expect(store.get('device-1', 'wallet-a')).toBe('session-a');
47
+ expect(store.getStandard('device-1')).toEqual({
48
+ passphraseState: 'wallet-a',
49
+ sessionId: 'session-a',
50
+ });
47
51
  expect(store.get('device-1', 'wallet-b')).toBe('session-b');
48
52
  expect(store.get('device-1', 'wallet-c')).toBe('session-c');
49
53
  expect(store.get('device-1', 'wallet-d')).toBe('session-d');
@@ -94,7 +98,7 @@ describe('DeviceWalletSessionStore', () => {
94
98
  expect(store.getPending('stable-device-id')).toBe('pending-session');
95
99
  });
96
100
 
97
- test('keeps the three-session limit when descriptor sessions merge into a stable device id', () => {
101
+ test('keeps every known handle when descriptor sessions merge into a stable device id', () => {
98
102
  const store = new DeviceWalletSessionStore();
99
103
  store.setStandard('stable-device-id', 'wallet-a', 'session-a');
100
104
  store.set('stable-device-id', 'wallet-b', 'session-b');
@@ -106,8 +110,11 @@ describe('DeviceWalletSessionStore', () => {
106
110
  nextDeviceId: 'stable-device-id',
107
111
  });
108
112
 
109
- expect(store.get('stable-device-id', 'wallet-a')).toBeUndefined();
110
- expect(store.getStandard('stable-device-id')).toBeUndefined();
113
+ expect(store.get('stable-device-id', 'wallet-a')).toBe('session-a');
114
+ expect(store.getStandard('stable-device-id')).toEqual({
115
+ passphraseState: 'wallet-a',
116
+ sessionId: 'session-a',
117
+ });
111
118
  expect(store.get('stable-device-id', 'wallet-b')).toBe('session-b');
112
119
  expect(store.get('stable-device-id', 'wallet-c')).toBe('session-c');
113
120
  expect(store.get('stable-device-id', 'wallet-d')).toBe('session-d');
@@ -402,4 +409,70 @@ describe('Protocol V1 wallet identity initialization', () => {
402
409
  expect.any(Object)
403
410
  );
404
411
  });
412
+
413
+ test('selects the standard V1 wallet after a non-destructive live identity read', async () => {
414
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
415
+ device.features = {
416
+ protocol: 'V1',
417
+ deviceId: 'device-a',
418
+ unlocked: true,
419
+ passphraseProtection: false,
420
+ } as never;
421
+ const typedCall = jest
422
+ .fn()
423
+ .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
424
+ .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
425
+ device.commands = { typedCall } as never;
426
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
427
+
428
+ await device.initialize({ deviceId: 'device-a' });
429
+
430
+ expect(typedCall).toHaveBeenCalledTimes(2);
431
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
432
+ expect(typedCall).toHaveBeenNthCalledWith(
433
+ 2,
434
+ 'Initialize',
435
+ 'Features',
436
+ {
437
+ passphrase_state: undefined,
438
+ is_contains_attach: true,
439
+ },
440
+ expect.any(Object)
441
+ );
442
+ });
443
+ });
444
+
445
+ describe('Protocol V1 passphrase state classification', () => {
446
+ test('does not expose a standard-wallet address as passphrase state', async () => {
447
+ const typedCall = jest.fn().mockResolvedValue({
448
+ type: 'Address',
449
+ message: { address: 'standard-wallet-address' },
450
+ });
451
+ const method = new GetPassphraseState({
452
+ payload: {
453
+ method: 'getPassphraseState',
454
+ connectId: 'connect-a',
455
+ },
456
+ });
457
+ method.device = {
458
+ features: {},
459
+ commands: { typedCall },
460
+ isProtocolV2: () => false,
461
+ getCurrentDeviceType: () => EDeviceType.Classic,
462
+ getCurrentFirmwareVersionString: () => '1.0.0',
463
+ getCurrentPassphraseProtection: () => false,
464
+ getCurrentDeviceId: () => 'device-a',
465
+ getFeatures: jest.fn(),
466
+ getInternalState: () => undefined,
467
+ updateInternalState: jest.fn(),
468
+ } as never;
469
+
470
+ await expect(method.run()).resolves.toBeUndefined();
471
+ expect(typedCall).toHaveBeenCalledWith('GetAddress', 'Address', {
472
+ address_n: expect.any(Array),
473
+ coin_name: 'Testnet',
474
+ script_type: 'SPENDADDRESS',
475
+ show_display: false,
476
+ });
477
+ });
405
478
  });
@@ -1,4 +1,5 @@
1
1
  import EVMSignTransaction from '../src/api/evm/EVMSignTransaction';
2
+ import TransportManager from '../src/data-manager/TransportManager';
2
3
 
3
4
  import type { EVMTransactionEIP7702 } from '../src/types';
4
5
 
@@ -17,13 +18,17 @@ jest.mock('../src/device/Device', () => ({
17
18
  Device: jest.fn(),
18
19
  }));
19
20
 
21
+ const mockTransportManager = jest.mocked(TransportManager);
22
+
20
23
  describe('EVMSignTransaction EIP-7702', () => {
21
24
  let mockDevice: any;
22
25
  let mockTypedCall: jest.Mock;
23
26
 
24
27
  beforeEach(() => {
28
+ mockTransportManager.getProtocolV1MessageSchema.mockReturnValue('v1CurrentSchema');
25
29
  mockTypedCall = jest.fn();
26
30
  mockDevice = {
31
+ isProtocolV2: jest.fn(() => false),
27
32
  commands: {
28
33
  typedCall: {
29
34
  bind: jest.fn(() => mockTypedCall),
@@ -32,6 +37,70 @@ describe('EVMSignTransaction EIP-7702', () => {
32
37
  };
33
38
  });
34
39
 
40
+ describe('Protocol-specific schema routing', () => {
41
+ const transaction: EVMTransactionEIP7702 = {
42
+ to: '0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9',
43
+ value: '0x0',
44
+ gasLimit: '0x5208',
45
+ nonce: '0x0',
46
+ chainId: 1,
47
+ maxFeePerGas: '0xbebc200',
48
+ maxPriorityFeePerGas: '0x9502f900',
49
+ authorizationList: [
50
+ {
51
+ chainId: 1,
52
+ address: '0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9',
53
+ nonce: '0x1',
54
+ },
55
+ ],
56
+ };
57
+
58
+ const createTransactionMethod = () => {
59
+ const method = new EVMSignTransaction({
60
+ id: 1,
61
+ payload: {
62
+ method: 'evmSignTransaction',
63
+ path: "m/44'/60'/0'/0/0",
64
+ transaction,
65
+ },
66
+ });
67
+ method.device = mockDevice;
68
+ method.init();
69
+ return method;
70
+ };
71
+
72
+ it('keeps Protocol V2 on current OneKey messages after a legacy V1 device', async () => {
73
+ mockTransportManager.getProtocolV1MessageSchema.mockReturnValue('v1LegacySchema');
74
+ mockDevice.isProtocolV2.mockReturnValue(true);
75
+ mockTypedCall.mockResolvedValue({
76
+ message: {
77
+ signature_v: 27,
78
+ signature_r: '01',
79
+ signature_s: '02',
80
+ authorization_signatures: [],
81
+ },
82
+ });
83
+
84
+ await expect(createTransactionMethod().run()).resolves.toMatchObject({
85
+ v: '0x1b',
86
+ });
87
+ expect(mockTypedCall).toHaveBeenCalledWith(
88
+ 'EthereumSignTxEIP7702OneKey',
89
+ 'EthereumTxRequestOneKey',
90
+ expect.any(Object)
91
+ );
92
+ });
93
+
94
+ it('continues to use legacy messages for a Protocol V1 legacy device', async () => {
95
+ mockTransportManager.getProtocolV1MessageSchema.mockReturnValue('v1LegacySchema');
96
+
97
+ await expect(createTransactionMethod().run()).rejects.toThrow(
98
+ 'EIP7702 not supported by Trezor'
99
+ );
100
+ expect(mockTypedCall).not.toHaveBeenCalled();
101
+ });
102
+ });
103
+
35
104
  describe('EIP-7702 Transaction Detection', () => {
36
105
  it('should detect EIP-7702 transaction with authorization list', () => {
37
106
  const transaction: EVMTransactionEIP7702 = {
@@ -1,7 +1,9 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DeviceType } from '@onekeyfe/hd-transport';
2
3
 
3
4
  import { Device } from '../src/device/Device';
4
5
  import { DEVICE } from '../src/events';
6
+ import { PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE } from '../src/protocols/protocol-v2';
5
7
 
6
8
  jest.mock('../src/data/config', () => ({
7
9
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -28,6 +30,18 @@ const createV1Device = (typedCall: jest.Mock) => {
28
30
  return device;
29
31
  };
30
32
 
33
+ const protocolV2ApplicationInfo = {
34
+ version: 1,
35
+ build_fingerprint: 'application__5.0.0__abcdef0__PROD__RELEASE',
36
+ supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
37
+ };
38
+
39
+ const getProtocolV2LoaderInfo = (mode: 'bootloader' | 'romloader') => ({
40
+ version: 1,
41
+ build_fingerprint: `${mode}__1.0.0__abcdef0__PROD__RELEASE`,
42
+ supported_messages: [],
43
+ });
44
+
31
45
  describe('getDeviceState', () => {
32
46
  test('does not expose the internal wallet session', async () => {
33
47
  const device = createV2Device(jest.fn());
@@ -39,45 +53,55 @@ describe('getDeviceState', () => {
39
53
  });
40
54
 
41
55
  test('hydrates Protocol V2 with separate DeviceInfoGet and DeviceStatusGet', async () => {
42
- const typedCall = jest.fn().mockImplementation((requestType: string) => ({
43
- message:
44
- requestType === 'DeviceInfoGet'
45
- ? {
46
- protocol_version: 2,
47
- hw: { serial_no: 'SERIAL-1' },
48
- fw: { application: { version: '5.0.0' } },
49
- }
50
- : { init_states: true, unlocked: true },
51
- }));
56
+ const typedCall = jest.fn().mockImplementation((requestType: string) => {
57
+ if (requestType === 'DeviceInfoGet') {
58
+ return {
59
+ message: {
60
+ protocol_version: 2,
61
+ hw: { serial_no: 'SERIAL-1' },
62
+ fw: { application: { version: '5.0.0' } },
63
+ },
64
+ };
65
+ }
66
+ if (requestType === 'ProtocolInfoRequest') {
67
+ return { message: protocolV2ApplicationInfo };
68
+ }
69
+ return { message: { init_states: true, unlocked: true } };
70
+ });
52
71
  const device = createV2Device(typedCall);
53
72
 
54
73
  const state = await device.getDeviceState();
55
74
 
56
75
  expect(state.identity.serialNo).toBe('SERIAL-1');
57
- expect(typedCall).toHaveBeenCalledTimes(2);
76
+ expect(typedCall).toHaveBeenCalledTimes(3);
58
77
  expect(typedCall.mock.calls[0][0]).toBe('DeviceInfoGet');
59
78
  expect(typedCall.mock.calls[0][2]?.targets?.status).not.toBe(true);
60
79
  expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
61
80
  });
62
81
 
63
82
  test('uses one full DeviceInfoGet plus DeviceStatusGet when firmware refresh initializes state', async () => {
64
- const typedCall = jest.fn().mockImplementation((requestType: string) => ({
65
- message:
66
- requestType === 'DeviceInfoGet'
67
- ? {
68
- protocol_version: 2,
69
- hw: { serial_no: 'SERIAL-1' },
70
- fw: { application: { version: '5.0.0', hash: 'HASH-1' } },
71
- }
72
- : { init_states: true, unlocked: true },
73
- }));
83
+ const typedCall = jest.fn().mockImplementation((requestType: string) => {
84
+ if (requestType === 'DeviceInfoGet') {
85
+ return {
86
+ message: {
87
+ protocol_version: 2,
88
+ hw: { serial_no: 'SERIAL-1' },
89
+ fw: { application: { version: '5.0.0', hash: 'HASH-1' } },
90
+ },
91
+ };
92
+ }
93
+ if (requestType === 'ProtocolInfoRequest') {
94
+ return { message: protocolV2ApplicationInfo };
95
+ }
96
+ return { message: { init_states: true, unlocked: true } };
97
+ });
74
98
  const device = createV2Device(typedCall);
75
99
 
76
100
  await device.getDeviceState({
77
101
  refreshSections: ['identity', 'versions', 'verification'],
78
102
  });
79
103
 
80
- expect(typedCall).toHaveBeenCalledTimes(2);
104
+ expect(typedCall).toHaveBeenCalledTimes(3);
81
105
  expect(typedCall).toHaveBeenNthCalledWith(
82
106
  1,
83
107
  'DeviceInfoGet',
@@ -115,13 +139,13 @@ describe('getDeviceState', () => {
115
139
  });
116
140
 
117
141
  test.each(['bootloader', 'romloader'] as const)(
118
- 'temporarily treats failed DeviceStatusGet with %s metadata as bootloader',
142
+ 'uses ProtocolInfo to preserve %s mode without DeviceStatusGet',
119
143
  async mode => {
120
144
  const typedCall = jest.fn().mockImplementation((requestType: string) => {
121
145
  if (requestType === 'DeviceInfoGet') {
122
146
  return {
123
147
  message: {
124
- hw: { serial_no: 'SERIAL-1' },
148
+ hw: { Device_type: DeviceType.PRO2, serial_no: 'SERIAL-1' },
125
149
  fw:
126
150
  mode === 'romloader'
127
151
  ? { romloader: { version: '1.0.0' } }
@@ -129,24 +153,38 @@ describe('getDeviceState', () => {
129
153
  },
130
154
  };
131
155
  }
132
- throw new Error('DeviceStatusGet unavailable');
156
+ if (requestType === 'ProtocolInfoRequest') {
157
+ return { message: getProtocolV2LoaderInfo(mode) };
158
+ }
159
+ throw new Error(`Unexpected request: ${requestType}`);
133
160
  });
134
161
  const device = createV2Device(typedCall);
135
162
  device.updateState({ protocol: 'V2', status: { mode } }, 'initialize');
136
163
 
137
164
  const state = await device.getDeviceState({ refreshSections: ['status'] });
138
165
 
139
- expect(state.status.mode).toBe('bootloader');
166
+ expect(state.status.mode).toBe(mode);
140
167
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
141
168
  'DeviceInfoGet',
142
- 'DeviceStatusGet',
169
+ 'ProtocolInfoRequest',
143
170
  ]);
144
171
  }
145
172
  );
146
173
 
147
174
  test('refreshes Protocol V2 status only when explicitly requested in normal mode', async () => {
148
- const typedCall = jest.fn().mockResolvedValue({
149
- message: { init_states: true, unlocked: true, device_id: 'device-1' },
175
+ const typedCall = jest.fn().mockImplementation((requestType: string) => {
176
+ if (requestType === 'DeviceInfoGet') {
177
+ return {
178
+ message: {
179
+ hw: { serial_no: 'SERIAL-1' },
180
+ fw: { application: { version: '5.0.0' } },
181
+ },
182
+ };
183
+ }
184
+ if (requestType === 'ProtocolInfoRequest') {
185
+ return { message: protocolV2ApplicationInfo };
186
+ }
187
+ return { message: { init_states: true, unlocked: true, device_id: 'device-1' } };
150
188
  });
151
189
  const device = createV2Device(typedCall);
152
190
  device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
@@ -203,6 +241,9 @@ describe('getDeviceState', () => {
203
241
  message: { init_states: true, unlocked: false, device_id: 'device-1' },
204
242
  };
205
243
  }
244
+ if (requestType === 'ProtocolInfoRequest') {
245
+ return { message: protocolV2ApplicationInfo };
246
+ }
206
247
  if (requestType === 'DeviceSettingsGet') {
207
248
  return { message: { label: 'Renamed Pro 2' } };
208
249
  }
@@ -215,6 +256,7 @@ describe('getDeviceState', () => {
215
256
 
216
257
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
217
258
  'DeviceInfoGet',
259
+ 'ProtocolInfoRequest',
218
260
  'DeviceStatusGet',
219
261
  'DeviceSettingsGet',
220
262
  ]);
@@ -237,6 +279,9 @@ describe('getDeviceState', () => {
237
279
  message: { init_states: true, unlocked: false, device_id: 'device-1' },
238
280
  };
239
281
  }
282
+ if (requestType === 'ProtocolInfoRequest') {
283
+ return { message: protocolV2ApplicationInfo };
284
+ }
240
285
  throw Object.assign(new Error('Device locked'), {
241
286
  errorCode: HardwareErrorCode.DeviceLocked,
242
287
  });
@@ -383,7 +428,10 @@ describe('getDeviceState', () => {
383
428
  },
384
429
  };
385
430
  }
386
- throw new Error('DeviceStatusGet unavailable in bootloader');
431
+ if (requestType === 'ProtocolInfoRequest') {
432
+ return { message: getProtocolV2LoaderInfo('bootloader') };
433
+ }
434
+ throw new Error(`Unexpected request: ${requestType}`);
387
435
  });
388
436
  const device = createV2Device(typedCall);
389
437
  device.updateState(