@onekeyfe/hd-core 1.2.0-alpha.33 → 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 (108) hide show
  1. package/__tests__/DeviceCommands.test.ts +84 -0
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +31 -0
  3. package/__tests__/core-dispose.test.ts +3 -3
  4. package/__tests__/device-lifecycle-events.test.ts +52 -0
  5. package/__tests__/device-pool-state.test.ts +14 -2
  6. package/__tests__/device-settings.test.ts +25 -0
  7. package/__tests__/device-state-mapper.test.ts +13 -2
  8. package/__tests__/device-state-projector.test.ts +2 -2
  9. package/__tests__/device-utils.test.ts +1 -0
  10. package/__tests__/device-wallet-session-store.test.ts +137 -17
  11. package/__tests__/evmSignTransaction.test.ts +69 -0
  12. package/__tests__/get-device-state.test.ts +78 -30
  13. package/__tests__/kaspaSignTransaction.test.ts +9 -0
  14. package/__tests__/known-device-public-types.type-test.ts +3 -0
  15. package/__tests__/logBlockEvent.test.ts +89 -5
  16. package/__tests__/open-wallet-session.test.ts +499 -60
  17. package/__tests__/pro2Wallpaper.test.ts +1 -1
  18. package/__tests__/protocol-v2.test.ts +1180 -169
  19. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  20. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  23. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  24. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  25. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  26. package/dist/api/evm/protocol.d.ts +3 -0
  27. package/dist/api/evm/protocol.d.ts.map +1 -0
  28. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/device/Device.d.ts +10 -4
  32. package/dist/device/Device.d.ts.map +1 -1
  33. package/dist/device/DeviceCommands.d.ts.map +1 -1
  34. package/dist/device/DeviceConnector.d.ts +1 -1
  35. package/dist/device/DeviceConnector.d.ts.map +1 -1
  36. package/dist/device/DevicePool.d.ts +1 -0
  37. package/dist/device/DevicePool.d.ts.map +1 -1
  38. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  39. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  40. package/dist/device/DeviceWalletSessionStore.d.ts +7 -0
  41. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
  42. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  43. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
  44. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
  45. package/dist/events/logBlockEvent.d.ts +2 -0
  46. package/dist/events/logBlockEvent.d.ts.map +1 -1
  47. package/dist/index.d.ts +25 -11
  48. package/dist/index.js +1037 -838
  49. package/dist/protocols/protocol-v2/features.d.ts +17 -6
  50. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  51. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  52. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  53. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts +1 -0
  54. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  55. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  56. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -10
  57. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  58. package/dist/types/api/protocolV2.d.ts +2 -2
  59. package/dist/types/api/protocolV2.d.ts.map +1 -1
  60. package/dist/types/device.d.ts +6 -3
  61. package/dist/types/device.d.ts.map +1 -1
  62. package/dist/types/params.d.ts.map +1 -1
  63. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  64. package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
  65. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  66. package/dist/utils/patch.d.ts +1 -1
  67. package/dist/utils/patch.d.ts.map +1 -1
  68. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +11 -81
  71. package/src/api/FirmwareUpdateV4.ts +59 -25
  72. package/src/api/GetPassphraseState.ts +11 -2
  73. package/src/api/OpenWalletSession.ts +16 -8
  74. package/src/api/device/DeviceSettings.ts +8 -1
  75. package/src/api/evm/EVMGetAddress.ts +2 -2
  76. package/src/api/evm/EVMGetPublicKey.ts +2 -2
  77. package/src/api/evm/EVMSignMessage.ts +2 -2
  78. package/src/api/evm/EVMSignTransaction.ts +2 -2
  79. package/src/api/evm/EVMSignTypedData.ts +35 -45
  80. package/src/api/evm/EVMVerifyMessage.ts +2 -2
  81. package/src/api/evm/protocol.ts +6 -0
  82. package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
  83. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
  84. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  85. package/src/core/index.ts +7 -5
  86. package/src/data/messages/messages-protocol-v2.json +393 -485
  87. package/src/device/Device.ts +98 -44
  88. package/src/device/DeviceCommands.ts +12 -2
  89. package/src/device/DeviceConnector.ts +10 -7
  90. package/src/device/DevicePool.ts +5 -3
  91. package/src/device/DeviceStateMapper.ts +5 -3
  92. package/src/device/DeviceStateProjector.ts +0 -7
  93. package/src/device/DeviceWalletSessionStore.ts +43 -4
  94. package/src/deviceProfile/buildDeviceFeatures.ts +11 -4
  95. package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
  96. package/src/events/logBlockEvent.ts +84 -1
  97. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  98. package/src/protocols/protocol-v2/features.ts +58 -16
  99. package/src/protocols/protocol-v2/index.ts +5 -0
  100. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
  101. package/src/protocols/protocol-v2/unlockRetry.ts +18 -7
  102. package/src/protocols/protocol-v2/walletSession.ts +144 -62
  103. package/src/types/api/protocolV2.ts +2 -2
  104. package/src/types/device.ts +8 -2
  105. package/src/types/params.ts +2 -1
  106. package/src/utils/deviceFeaturesCompat.ts +8 -1
  107. package/src/utils/pro2Wallpaper.ts +3 -9
  108. package/tsconfig.type-tests.json +5 -1
@@ -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);
@@ -160,6 +160,37 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
160
160
  });
161
161
  });
162
162
 
163
+ test('keeps same-version hash-aware components unknown without metadata payloadHash', () => {
164
+ const packageSet: IFirmwareReleaseInfo = {
165
+ ...release,
166
+ required: false,
167
+ resourceBundles: undefined,
168
+ installOrder: ['applicationP1'],
169
+ components: {
170
+ applicationP1: {
171
+ target: 'APPLICATION_P1',
172
+ version: [1, 0, 0],
173
+ url: 'https://untrusted.example/application-p1.okpkg',
174
+ fingerprint: 'file-sha256-p1',
175
+ },
176
+ },
177
+ };
178
+
179
+ expect(
180
+ buildProtocolV2FirmwareRelease({
181
+ currentVersions: { ...currentVersions, applicationP1: '1.0.0' },
182
+ currentVerification: { applicationP1Hash: 'aa'.repeat(32) },
183
+ firmwareType: EFirmwareType.Universal,
184
+ release: packageSet,
185
+ })
186
+ ).toMatchObject({
187
+ status: 'unknown',
188
+ hasUpgrade: false,
189
+ targetsToUpdate: [],
190
+ components: [{ configKey: 'applicationP1', status: 'unknown' }],
191
+ });
192
+ });
193
+
163
194
  test('updates both application packages when normal mode only reports P1', () => {
164
195
  const packageSet: IFirmwareReleaseInfo = {
165
196
  ...release,
@@ -17,18 +17,18 @@ describe('Core.dispose', () => {
17
17
  const connectorStop = jest.spyOn(connector, 'stop');
18
18
  const transportStop = jest.fn().mockResolvedValue(undefined);
19
19
  jest.spyOn(TransportManager, 'getTransport').mockReturnValue({ stop: transportStop } as never);
20
- const resetDevicePool = jest.spyOn(DevicePool, 'resetState');
20
+ const disposeDevicePool = jest.spyOn(DevicePool, 'dispose');
21
21
  const core = new Core();
22
22
 
23
23
  const firstDispose = core.dispose();
24
24
  expect(connectorStop).toHaveBeenCalledTimes(1);
25
25
  expect(transportStop).toHaveBeenCalledTimes(1);
26
- expect(resetDevicePool).toHaveBeenCalledTimes(1);
26
+ expect(disposeDevicePool).toHaveBeenCalledTimes(1);
27
27
 
28
28
  await firstDispose;
29
29
  await core.dispose();
30
30
  expect(connectorStop).toHaveBeenCalledTimes(1);
31
31
  expect(transportStop).toHaveBeenCalledTimes(1);
32
- expect(resetDevicePool).toHaveBeenCalledTimes(1);
32
+ expect(disposeDevicePool).toHaveBeenCalledTimes(1);
33
33
  });
34
34
  });
@@ -79,6 +79,57 @@ describe('public device lifecycle events', () => {
79
79
  expect(DevicePool.emitter.listenerCount(DEVICE.DISCONNECT)).toBe(1);
80
80
  });
81
81
 
82
+ test('keeps shared device lifecycle listeners across a device cache reset', () => {
83
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
84
+ core = initCore();
85
+ initConnector();
86
+ const messages: CoreMessage[] = [];
87
+ core.on(CORE_EVENT, message => messages.push(message));
88
+ const device = createInitializedDevice('V2');
89
+
90
+ DevicePool.resetState();
91
+ DevicePool.emitter.emit(DEVICE.CONNECT, device);
92
+ DevicePool.emitter.emit(DEVICE.DISCONNECT, device);
93
+
94
+ expect(messages.filter(message => message.type === DEVICE.CONNECT)).toHaveLength(1);
95
+ expect(messages.filter(message => message.type === DEVICE.DISCONNECT)).toHaveLength(1);
96
+ });
97
+
98
+ test('uses a cached protocol as a fallback-capable acquire hint', async () => {
99
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
100
+ const device = Device.fromDescriptor({
101
+ id: 'ble-id',
102
+ path: 'ble-id',
103
+ commType: 'ble',
104
+ protocolType: 'V1',
105
+ } as never);
106
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
107
+ device.deviceConnector = { acquire } as never;
108
+
109
+ await device.acquire();
110
+
111
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, 'V1');
112
+ expect(device.getProtocol()).toBe('V2');
113
+ expect(device.originalDescriptor.protocolType).toBe('V2');
114
+ });
115
+
116
+ test('keeps an explicit connectProtocol as a strict expected protocol', async () => {
117
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
118
+ const device = Device.fromDescriptor({
119
+ id: 'ble-id',
120
+ path: 'ble-id',
121
+ commType: 'ble',
122
+ protocolType: 'V1',
123
+ } as never);
124
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
125
+ device.deviceConnector = { acquire } as never;
126
+
127
+ await device.acquire('V2');
128
+
129
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined);
130
+ expect(device.getProtocol()).toBe('V2');
131
+ });
132
+
82
133
  test('converts an internal transport disconnect into a public KnownDevice snapshot', () => {
83
134
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
84
135
  core = initCore();
@@ -125,6 +176,7 @@ describe('public device lifecycle events', () => {
125
176
  const device = createInitializedDevice('V2');
126
177
 
127
178
  expect(device.toMessageObject()?.status).toBe('available');
179
+ expect(device.toMessageObject()?.connectProtocol).toBe('V2');
128
180
 
129
181
  (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
130
182
  expect(device.toMessageObject()?.status).toBe('used');
@@ -9,11 +9,11 @@ jest.mock('../src/data/config', () => ({
9
9
 
10
10
  describe('DevicePool state lifecycle', () => {
11
11
  beforeEach(() => {
12
- DevicePool.resetState();
12
+ DevicePool.dispose();
13
13
  });
14
14
 
15
15
  afterEach(() => {
16
- DevicePool.resetState();
16
+ DevicePool.dispose();
17
17
  });
18
18
 
19
19
  test('refreshes runtime state for a cached Protocol V2 discovery result', async () => {
@@ -74,6 +74,18 @@ describe('DevicePool state lifecycle', () => {
74
74
  expect(DevicePool.disconnectPool).toEqual([]);
75
75
  });
76
76
 
77
+ test('preserves shared listeners when resetting cached device state', () => {
78
+ const listener = jest.fn();
79
+ DevicePool.emitter.on(DEVICE.CONNECT, listener);
80
+ DevicePool.devicesCache = { stale: {} as Device };
81
+
82
+ DevicePool.resetState();
83
+ DevicePool.emitter.emit(DEVICE.CONNECT, { path: 'next-device' });
84
+
85
+ expect(DevicePool.devicesCache).toEqual({});
86
+ expect(listener).toHaveBeenCalledWith({ path: 'next-device' });
87
+ });
88
+
77
89
  test('releases a newly created device when initialization fails', async () => {
78
90
  const descriptor = { path: 'pro2-path', protocolType: 'V2' } as any;
79
91
  const release = jest.fn().mockResolvedValue(undefined);
@@ -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,
@@ -226,6 +229,28 @@ describe('DeviceSettings protocol routing', () => {
226
229
  });
227
230
  });
228
231
 
232
+ it('accepts a locked Pro2 as confirmation after disabling passphrase', async () => {
233
+ const { device, getDeviceState } = createDevice({ protocol: 'V2' });
234
+ getDeviceState
235
+ .mockResolvedValueOnce({
236
+ status: { unlocked: true, passphraseProtection: true },
237
+ })
238
+ .mockResolvedValueOnce({
239
+ status: { unlocked: false, passphraseProtection: true },
240
+ });
241
+ const method = new DeviceSettings({
242
+ id: 6,
243
+ payload: {
244
+ method: 'deviceSettings',
245
+ usePassphrase: false,
246
+ },
247
+ });
248
+ method.init();
249
+ (method as any).device = device;
250
+
251
+ await expect(method.run()).resolves.toEqual({ message: 'Success' });
252
+ });
253
+
229
254
  it('keeps Protocol V1 passphrase settings on ApplySettings', async () => {
230
255
  const { device, typedCall, getDeviceState } = createDevice({ protocol: 'V1' });
231
256
  const method = new DeviceSettings({
@@ -1,6 +1,6 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
- import { Enum_SafetyCheckLevel } from '@onekeyfe/hd-transport';
3
+ import { DeviceType, Enum_SafetyCheckLevel } from '@onekeyfe/hd-transport';
4
4
  import {
5
5
  mapApplySettingsToState,
6
6
  mapDeviceSettingsToState,
@@ -109,7 +109,7 @@ describe('DeviceStateMapper', () => {
109
109
  const patch = mapProtocolV2DeviceInfoToState(
110
110
  {
111
111
  protocol_version: 2,
112
- hw: { serial_no: 'SERIAL-1' },
112
+ hw: { Device_type: DeviceType.PRO2, serial_no: 'SERIAL-1' },
113
113
  fw: {
114
114
  application: { version: '5.0.0', build_id: 'p1-build', hash: [0x01, 0x02] },
115
115
  application_data: { version: '5.0.1', build_id: 'p2-build', hash: [0x03, 0x04] },
@@ -145,6 +145,17 @@ describe('DeviceStateMapper', () => {
145
145
  expect(patch.status?.unlocked).toBeUndefined();
146
146
  });
147
147
 
148
+ test('maps the hardware model independently from Protocol V2', () => {
149
+ const pro = mapProtocolV2DeviceInfoToState({
150
+ protocol_version: 2,
151
+ hw: { Device_type: 'PRO' as unknown as DeviceType, serial_no: 'SERIAL-PRO' },
152
+ });
153
+ const unknown = mapProtocolV2DeviceInfoToState({ protocol_version: 2, hw: {} });
154
+
155
+ expect(pro.identity).toMatchObject({ deviceType: EDeviceType.Pro, model: 'pro' });
156
+ expect(unknown.identity).toMatchObject({ deviceType: EDeviceType.Unknown, model: null });
157
+ });
158
+
148
159
  test('keeps normal mode when Protocol V2 application and SE versions are returned together', () => {
149
160
  const patch = mapProtocolV2DeviceInfoToState(
150
161
  {
@@ -73,7 +73,7 @@ describe('DeviceStateProjector', () => {
73
73
  });
74
74
  });
75
75
 
76
- test('returns an isolated compatibility snapshot', () => {
76
+ test('returns an isolated compatibility snapshot without internal raw state', () => {
77
77
  const state = createEmptyDeviceState({ deviceType: EDeviceType.Pro });
78
78
  state.protocol = 'V1';
79
79
  state.capabilities = [1];
@@ -85,10 +85,10 @@ describe('DeviceStateProjector', () => {
85
85
  const features = projectFeatures(state);
86
86
  features.capabilities.push(2);
87
87
  if (features.verify) features.verify.firmwareBuildId = 'mutated';
88
- if (features.raw?.protocolV1Features) features.raw.protocolV1Features.label = 'Mutated';
89
88
 
90
89
  expect(state.capabilities).toEqual([1]);
91
90
  expect(state.verification?.firmwareBuildId).toBe('build-1');
92
91
  expect(state.raw.protocolV1Features?.label).toBe('Original');
92
+ expect(features).not.toHaveProperty('raw');
93
93
  });
94
94
  });
@@ -61,6 +61,7 @@ describe('device feature selectors', () => {
61
61
  } as PROTO.Features;
62
62
 
63
63
  expect(getDeviceType(features)).toBe(EDeviceType.Classic);
64
+ expect(getDeviceFirmwareVersion(features)).toEqual([0, 0, 0]);
64
65
  expect(getDeviceBootloaderVersion(features)).toEqual([1, 8, 3]);
65
66
  });
66
67
 
@@ -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,6 +36,38 @@ describe('DeviceWalletSessionStore', () => {
35
36
  expect(store.get('device-2', 'hidden-a')).toBe('session-c');
36
37
  });
37
38
 
39
+ test('keeps SDK session handles without mirroring hardware eviction policy', () => {
40
+ const store = new DeviceWalletSessionStore();
41
+ store.setStandard('device-1', 'wallet-a', 'session-a');
42
+ store.set('device-1', 'wallet-b', 'session-b');
43
+ store.set('device-1', 'wallet-c', 'session-c');
44
+ store.set('device-1', 'wallet-d', 'session-d');
45
+
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
+ });
51
+ expect(store.get('device-1', 'wallet-b')).toBe('session-b');
52
+ expect(store.get('device-1', 'wallet-c')).toBe('session-c');
53
+ expect(store.get('device-1', 'wallet-d')).toBe('session-d');
54
+ });
55
+
56
+ test('indexes the standard wallet without replacing hidden-wallet sessions', () => {
57
+ const store = new DeviceWalletSessionStore();
58
+ store.set('device-1', 'hidden-a', 'hidden-session-a');
59
+
60
+ store.setStandard('device-1', 'standard-state', 'standard-session-1');
61
+ store.setStandard('device-1', 'standard-state', 'standard-session-2');
62
+
63
+ expect(store.getStandard('device-1')).toEqual({
64
+ passphraseState: 'standard-state',
65
+ sessionId: 'standard-session-2',
66
+ });
67
+ expect(store.get('device-1', 'standard-state')).toBe('standard-session-2');
68
+ expect(store.get('device-1', 'hidden-a')).toBe('hidden-session-a');
69
+ });
70
+
38
71
  test('keeps pending sessions unreadable until wallet binding', () => {
39
72
  const store = new DeviceWalletSessionStore();
40
73
  store.setPending('device-1', 'pending-session');
@@ -46,6 +79,7 @@ describe('DeviceWalletSessionStore', () => {
46
79
  test('migrates descriptor keys to stable device ids', () => {
47
80
  const store = new DeviceWalletSessionStore();
48
81
  store.set('ble-path', 'hidden-a', 'session-a');
82
+ store.setStandard('ble-path', 'standard-state', 'standard-session');
49
83
  store.setPending('ble-path', 'pending-session');
50
84
 
51
85
  store.reconcileDeviceIdentity({
@@ -55,10 +89,37 @@ describe('DeviceWalletSessionStore', () => {
55
89
 
56
90
  expect(store.get('ble-path', 'hidden-a')).toBeUndefined();
57
91
  expect(store.get('stable-device-id', 'hidden-a')).toBe('session-a');
92
+ expect(store.getStandard('ble-path')).toBeUndefined();
93
+ expect(store.getStandard('stable-device-id')).toEqual({
94
+ passphraseState: 'standard-state',
95
+ sessionId: 'standard-session',
96
+ });
58
97
  expect(store.getPending('ble-path')).toBeUndefined();
59
98
  expect(store.getPending('stable-device-id')).toBe('pending-session');
60
99
  });
61
100
 
101
+ test('keeps every known handle when descriptor sessions merge into a stable device id', () => {
102
+ const store = new DeviceWalletSessionStore();
103
+ store.setStandard('stable-device-id', 'wallet-a', 'session-a');
104
+ store.set('stable-device-id', 'wallet-b', 'session-b');
105
+ store.set('ble-path', 'wallet-c', 'session-c');
106
+ store.set('ble-path', 'wallet-d', 'session-d');
107
+
108
+ store.reconcileDeviceIdentity({
109
+ temporaryKey: 'ble-path',
110
+ nextDeviceId: 'stable-device-id',
111
+ });
112
+
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
+ });
118
+ expect(store.get('stable-device-id', 'wallet-b')).toBe('session-b');
119
+ expect(store.get('stable-device-id', 'wallet-c')).toBe('session-c');
120
+ expect(store.get('stable-device-id', 'wallet-d')).toBe('session-d');
121
+ });
122
+
62
123
  test('drops sessions from an old stable identity without overwriting the new device', () => {
63
124
  const store = new DeviceWalletSessionStore();
64
125
  store.set('device-a', 'hidden-a', 'session-a');
@@ -82,11 +143,16 @@ describe('DeviceWalletSessionStore', () => {
82
143
  const store = new DeviceWalletSessionStore();
83
144
  store.set('device-1', 'hidden-a', 'session-a');
84
145
  store.set('device-1', 'hidden-b', 'session-b');
146
+ store.setStandard('device-1', 'standard-state', 'standard-session');
85
147
  store.set('device-2', 'hidden-a', 'session-c');
86
148
 
87
149
  store.delete('device-1', 'hidden-a');
88
150
  expect(store.get('device-1', 'hidden-a')).toBeUndefined();
89
151
  expect(store.get('device-1', 'hidden-b')).toBe('session-b');
152
+ expect(store.getStandard('device-1')?.sessionId).toBe('standard-session');
153
+
154
+ store.delete('device-1', 'standard-state');
155
+ expect(store.getStandard('device-1')).toBeUndefined();
90
156
 
91
157
  store.deleteDevice('device-1');
92
158
  expect(store.get('device-1', 'hidden-b')).toBeUndefined();
@@ -195,8 +261,7 @@ describe('ClearSessionCache', () => {
195
261
  session_id: null,
196
262
  });
197
263
  expect(device.features).not.toHaveProperty('passphraseState');
198
- expect(device.features?.raw?.protocolV1Features).not.toHaveProperty('sessionId');
199
- expect(device.features?.raw?.protocolV1Features).not.toHaveProperty('session_id');
264
+ expect(device.features).not.toHaveProperty('raw');
200
265
 
201
266
  const method = new ClearSessionCache({
202
267
  payload: {
@@ -287,7 +352,7 @@ describe('Protocol V1 wallet identity initialization', () => {
287
352
  jest.restoreAllMocks();
288
353
  });
289
354
 
290
- test('verifies the live device id before sending a cached wallet session', async () => {
355
+ test('verifies the live device id without resetting the cached wallet session', async () => {
291
356
  const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
292
357
  device.features = {
293
358
  protocol: 'V1',
@@ -307,15 +372,10 @@ describe('Protocol V1 wallet identity initialization', () => {
307
372
  device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
308
373
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
309
374
  expect(typedCall).toHaveBeenCalledTimes(1);
310
- expect(typedCall).toHaveBeenCalledWith(
311
- 'Initialize',
312
- 'Features',
313
- expect.not.objectContaining({ session_id: 'session-a' }),
314
- expect.any(Object)
315
- );
375
+ expect(typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
316
376
  });
317
377
 
318
- test('resumes a cached V1 wallet only after the live device id matches', async () => {
378
+ test('resumes a cached V1 wallet after a non-destructive live identity read', async () => {
319
379
  const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
320
380
  device.features = {
321
381
  protocol: 'V1',
@@ -337,22 +397,82 @@ describe('Protocol V1 wallet identity initialization', () => {
337
397
  await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
338
398
 
339
399
  expect(typedCall).toHaveBeenCalledTimes(2);
400
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
340
401
  expect(typedCall).toHaveBeenNthCalledWith(
341
- 1,
402
+ 2,
342
403
  'Initialize',
343
404
  'Features',
344
- expect.not.objectContaining({ session_id: 'session-a' }),
405
+ expect.objectContaining({
406
+ session_id: 'session-a',
407
+ passphrase_state: 'hidden-a',
408
+ }),
345
409
  expect.any(Object)
346
410
  );
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', {});
347
432
  expect(typedCall).toHaveBeenNthCalledWith(
348
433
  2,
349
434
  'Initialize',
350
435
  'Features',
351
- expect.objectContaining({
352
- session_id: 'session-a',
353
- passphrase_state: 'hidden-a',
354
- }),
436
+ {
437
+ passphrase_state: undefined,
438
+ is_contains_attach: true,
439
+ },
355
440
  expect.any(Object)
356
441
  );
357
442
  });
358
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
+ });
478
+ });