@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.
Files changed (75) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +31 -0
  2. package/__tests__/core-dispose.test.ts +3 -3
  3. package/__tests__/device-lifecycle-events.test.ts +52 -0
  4. package/__tests__/device-pool-state.test.ts +14 -2
  5. package/__tests__/device-settings.test.ts +22 -0
  6. package/__tests__/device-state-mapper.test.ts +13 -2
  7. package/__tests__/device-state-projector.test.ts +2 -2
  8. package/__tests__/device-utils.test.ts +1 -0
  9. package/__tests__/device-wallet-session-store.test.ts +64 -17
  10. package/__tests__/kaspaSignTransaction.test.ts +9 -0
  11. package/__tests__/known-device-public-types.type-test.ts +3 -0
  12. package/__tests__/logBlockEvent.test.ts +89 -5
  13. package/__tests__/open-wallet-session.test.ts +334 -30
  14. package/__tests__/pro2Wallpaper.test.ts +1 -1
  15. package/__tests__/protocol-v2.test.ts +708 -75
  16. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  17. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  18. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  19. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  20. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  21. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  22. package/dist/core/index.d.ts.map +1 -1
  23. package/dist/device/Device.d.ts +7 -2
  24. package/dist/device/Device.d.ts.map +1 -1
  25. package/dist/device/DeviceConnector.d.ts +1 -1
  26. package/dist/device/DeviceConnector.d.ts.map +1 -1
  27. package/dist/device/DevicePool.d.ts +1 -0
  28. package/dist/device/DevicePool.d.ts.map +1 -1
  29. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  30. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  31. package/dist/device/DeviceWalletSessionStore.d.ts +8 -0
  32. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
  33. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  34. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
  35. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
  36. package/dist/events/logBlockEvent.d.ts +2 -0
  37. package/dist/events/logBlockEvent.d.ts.map +1 -1
  38. package/dist/index.d.ts +19 -7
  39. package/dist/index.js +456 -222
  40. package/dist/protocols/protocol-v2/walletSession.d.ts +0 -10
  41. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  42. package/dist/types/api/openWalletSession.d.ts +1 -1
  43. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  44. package/dist/types/device.d.ts +4 -2
  45. package/dist/types/device.d.ts.map +1 -1
  46. package/dist/types/params.d.ts.map +1 -1
  47. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  48. package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
  49. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  50. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/api/CheckAllFirmwareRelease.ts +11 -81
  53. package/src/api/FirmwareUpdateV4.ts +33 -17
  54. package/src/api/GetPassphraseState.ts +1 -3
  55. package/src/api/OpenWalletSession.ts +3 -3
  56. package/src/api/device/DeviceSettings.ts +8 -1
  57. package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
  58. package/src/core/index.ts +7 -5
  59. package/src/device/Device.ts +37 -9
  60. package/src/device/DeviceConnector.ts +10 -7
  61. package/src/device/DevicePool.ts +5 -3
  62. package/src/device/DeviceStateMapper.ts +5 -3
  63. package/src/device/DeviceStateProjector.ts +0 -7
  64. package/src/device/DeviceWalletSessionStore.ts +57 -4
  65. package/src/deviceProfile/buildDeviceFeatures.ts +10 -3
  66. package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
  67. package/src/events/logBlockEvent.ts +84 -1
  68. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  69. package/src/protocols/protocol-v2/walletSession.ts +116 -34
  70. package/src/types/api/openWalletSession.ts +1 -1
  71. package/src/types/device.ts +6 -2
  72. package/src/types/params.ts +2 -1
  73. package/src/utils/deviceFeaturesCompat.ts +8 -1
  74. package/src/utils/pro2Wallpaper.ts +3 -9
  75. package/tsconfig.type-tests.json +5 -1
@@ -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);
@@ -226,6 +226,28 @@ describe('DeviceSettings protocol routing', () => {
226
226
  });
227
227
  });
228
228
 
229
+ it('accepts a locked Pro2 as confirmation after disabling passphrase', async () => {
230
+ const { device, getDeviceState } = createDevice({ protocol: 'V2' });
231
+ getDeviceState
232
+ .mockResolvedValueOnce({
233
+ status: { unlocked: true, passphraseProtection: true },
234
+ })
235
+ .mockResolvedValueOnce({
236
+ status: { unlocked: false, passphraseProtection: true },
237
+ });
238
+ const method = new DeviceSettings({
239
+ id: 6,
240
+ payload: {
241
+ method: 'deviceSettings',
242
+ usePassphrase: false,
243
+ },
244
+ });
245
+ method.init();
246
+ (method as any).device = device;
247
+
248
+ await expect(method.run()).resolves.toEqual({ message: 'Success' });
249
+ });
250
+
229
251
  it('keeps Protocol V1 passphrase settings on ApplySettings', async () => {
230
252
  const { device, typedCall, getDeviceState } = createDevice({ protocol: 'V1' });
231
253
  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
 
@@ -35,6 +35,35 @@ describe('DeviceWalletSessionStore', () => {
35
35
  expect(store.get('device-2', 'hidden-a')).toBe('session-c');
36
36
  });
37
37
 
38
+ test('evicts the oldest wallet session when a device exceeds the SE capacity of three', () => {
39
+ const store = new DeviceWalletSessionStore();
40
+ store.setStandard('device-1', 'wallet-a', 'session-a');
41
+ store.set('device-1', 'wallet-b', 'session-b');
42
+ store.set('device-1', 'wallet-c', 'session-c');
43
+ store.set('device-1', 'wallet-d', 'session-d');
44
+
45
+ expect(store.get('device-1', 'wallet-a')).toBeUndefined();
46
+ expect(store.getStandard('device-1')).toBeUndefined();
47
+ expect(store.get('device-1', 'wallet-b')).toBe('session-b');
48
+ expect(store.get('device-1', 'wallet-c')).toBe('session-c');
49
+ expect(store.get('device-1', 'wallet-d')).toBe('session-d');
50
+ });
51
+
52
+ test('indexes the standard wallet without replacing hidden-wallet sessions', () => {
53
+ const store = new DeviceWalletSessionStore();
54
+ store.set('device-1', 'hidden-a', 'hidden-session-a');
55
+
56
+ store.setStandard('device-1', 'standard-state', 'standard-session-1');
57
+ store.setStandard('device-1', 'standard-state', 'standard-session-2');
58
+
59
+ expect(store.getStandard('device-1')).toEqual({
60
+ passphraseState: 'standard-state',
61
+ sessionId: 'standard-session-2',
62
+ });
63
+ expect(store.get('device-1', 'standard-state')).toBe('standard-session-2');
64
+ expect(store.get('device-1', 'hidden-a')).toBe('hidden-session-a');
65
+ });
66
+
38
67
  test('keeps pending sessions unreadable until wallet binding', () => {
39
68
  const store = new DeviceWalletSessionStore();
40
69
  store.setPending('device-1', 'pending-session');
@@ -46,6 +75,7 @@ describe('DeviceWalletSessionStore', () => {
46
75
  test('migrates descriptor keys to stable device ids', () => {
47
76
  const store = new DeviceWalletSessionStore();
48
77
  store.set('ble-path', 'hidden-a', 'session-a');
78
+ store.setStandard('ble-path', 'standard-state', 'standard-session');
49
79
  store.setPending('ble-path', 'pending-session');
50
80
 
51
81
  store.reconcileDeviceIdentity({
@@ -55,10 +85,34 @@ describe('DeviceWalletSessionStore', () => {
55
85
 
56
86
  expect(store.get('ble-path', 'hidden-a')).toBeUndefined();
57
87
  expect(store.get('stable-device-id', 'hidden-a')).toBe('session-a');
88
+ expect(store.getStandard('ble-path')).toBeUndefined();
89
+ expect(store.getStandard('stable-device-id')).toEqual({
90
+ passphraseState: 'standard-state',
91
+ sessionId: 'standard-session',
92
+ });
58
93
  expect(store.getPending('ble-path')).toBeUndefined();
59
94
  expect(store.getPending('stable-device-id')).toBe('pending-session');
60
95
  });
61
96
 
97
+ test('keeps the three-session limit when descriptor sessions merge into a stable device id', () => {
98
+ const store = new DeviceWalletSessionStore();
99
+ store.setStandard('stable-device-id', 'wallet-a', 'session-a');
100
+ store.set('stable-device-id', 'wallet-b', 'session-b');
101
+ store.set('ble-path', 'wallet-c', 'session-c');
102
+ store.set('ble-path', 'wallet-d', 'session-d');
103
+
104
+ store.reconcileDeviceIdentity({
105
+ temporaryKey: 'ble-path',
106
+ nextDeviceId: 'stable-device-id',
107
+ });
108
+
109
+ expect(store.get('stable-device-id', 'wallet-a')).toBeUndefined();
110
+ expect(store.getStandard('stable-device-id')).toBeUndefined();
111
+ expect(store.get('stable-device-id', 'wallet-b')).toBe('session-b');
112
+ expect(store.get('stable-device-id', 'wallet-c')).toBe('session-c');
113
+ expect(store.get('stable-device-id', 'wallet-d')).toBe('session-d');
114
+ });
115
+
62
116
  test('drops sessions from an old stable identity without overwriting the new device', () => {
63
117
  const store = new DeviceWalletSessionStore();
64
118
  store.set('device-a', 'hidden-a', 'session-a');
@@ -82,11 +136,16 @@ describe('DeviceWalletSessionStore', () => {
82
136
  const store = new DeviceWalletSessionStore();
83
137
  store.set('device-1', 'hidden-a', 'session-a');
84
138
  store.set('device-1', 'hidden-b', 'session-b');
139
+ store.setStandard('device-1', 'standard-state', 'standard-session');
85
140
  store.set('device-2', 'hidden-a', 'session-c');
86
141
 
87
142
  store.delete('device-1', 'hidden-a');
88
143
  expect(store.get('device-1', 'hidden-a')).toBeUndefined();
89
144
  expect(store.get('device-1', 'hidden-b')).toBe('session-b');
145
+ expect(store.getStandard('device-1')?.sessionId).toBe('standard-session');
146
+
147
+ store.delete('device-1', 'standard-state');
148
+ expect(store.getStandard('device-1')).toBeUndefined();
90
149
 
91
150
  store.deleteDevice('device-1');
92
151
  expect(store.get('device-1', 'hidden-b')).toBeUndefined();
@@ -195,8 +254,7 @@ describe('ClearSessionCache', () => {
195
254
  session_id: null,
196
255
  });
197
256
  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');
257
+ expect(device.features).not.toHaveProperty('raw');
200
258
 
201
259
  const method = new ClearSessionCache({
202
260
  payload: {
@@ -287,7 +345,7 @@ describe('Protocol V1 wallet identity initialization', () => {
287
345
  jest.restoreAllMocks();
288
346
  });
289
347
 
290
- test('verifies the live device id before sending a cached wallet session', async () => {
348
+ test('verifies the live device id without resetting the cached wallet session', async () => {
291
349
  const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
292
350
  device.features = {
293
351
  protocol: 'V1',
@@ -307,15 +365,10 @@ describe('Protocol V1 wallet identity initialization', () => {
307
365
  device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
308
366
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
309
367
  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
- );
368
+ expect(typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
316
369
  });
317
370
 
318
- test('resumes a cached V1 wallet only after the live device id matches', async () => {
371
+ test('resumes a cached V1 wallet after a non-destructive live identity read', async () => {
319
372
  const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
320
373
  device.features = {
321
374
  protocol: 'V1',
@@ -337,13 +390,7 @@ describe('Protocol V1 wallet identity initialization', () => {
337
390
  await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
338
391
 
339
392
  expect(typedCall).toHaveBeenCalledTimes(2);
340
- expect(typedCall).toHaveBeenNthCalledWith(
341
- 1,
342
- 'Initialize',
343
- 'Features',
344
- expect.not.objectContaining({ session_id: 'session-a' }),
345
- expect.any(Object)
346
- );
393
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
347
394
  expect(typedCall).toHaveBeenNthCalledWith(
348
395
  2,
349
396
  'Initialize',
@@ -135,6 +135,15 @@ describe('KaspaSignTransaction capability flags', () => {
135
135
  createMethod({ inputs: [buildInput()], outputs: [{ satoshis: 1 }] }).init()
136
136
  ).toThrow('outputs require either address/addressN');
137
137
  });
138
+
139
+ it('rejects outputs containing both address and addressN', () => {
140
+ expect(() =>
141
+ createMethod({
142
+ inputs: [buildInput()],
143
+ outputs: [{ satoshis: 1, address: ADDRESS, addressN: CHANGE_PATH }],
144
+ }).init()
145
+ ).toThrow('outputs must not contain both address and addressN');
146
+ });
138
147
  });
139
148
 
140
149
  describe('KaspaSignTransaction protocol negotiation', () => {
@@ -0,0 +1,3 @@
1
+ import type { Features, KnownDevice } from '../src/types';
2
+
3
+ export const requireKnownDeviceFeatures = ({ features }: KnownDevice): Features => features;
@@ -1,4 +1,10 @@
1
- import { UI_RESPONSE, getLogBlockLabel } from '../src/events';
1
+ import {
2
+ UI_REQUEST,
3
+ UI_RESPONSE,
4
+ formatLogMethodLabel,
5
+ getLogBlockLabel,
6
+ getSafeLogPayload,
7
+ } from '../src/events';
2
8
 
3
9
  describe('getLogBlockLabel', () => {
4
10
  it('blocks evmSignTypedData params before logging large typed data', () => {
@@ -35,6 +41,21 @@ describe('getLogBlockLabel', () => {
35
41
  expect(getLogBlockLabel({ type: UI_RESPONSE.RECEIVE_PIN })).toBe(UI_RESPONSE.RECEIVE_PIN);
36
42
  });
37
43
 
44
+ it.each([UI_REQUEST.REQUEST_PASSPHRASE, UI_REQUEST.REQUEST_PASSPHRASE_ON_DEVICE])(
45
+ 'blocks wallet identifiers in %s event logging',
46
+ type => {
47
+ expect(
48
+ getLogBlockLabel({
49
+ type,
50
+ payload: {
51
+ passphraseState: 'wallet-identifier',
52
+ expectedPassphraseState: 'expected-wallet-identifier',
53
+ },
54
+ })
55
+ ).toBe(type);
56
+ }
57
+ );
58
+
38
59
  it('blocks openWalletSession wallet identifiers in direct and iframe call logging', () => {
39
60
  const payload = {
40
61
  method: 'openWalletSession',
@@ -53,16 +74,51 @@ describe('getLogBlockLabel', () => {
53
74
  ).toBe('openWalletSession');
54
75
  });
55
76
 
77
+ it('keeps openWalletSession metadata while redacting wallet session identifiers', () => {
78
+ const safeResponse = getSafeLogPayload(
79
+ {
80
+ success: true,
81
+ payload: {
82
+ protocol: 'V2',
83
+ walletType: 'hidden',
84
+ deviceId: 'device-id',
85
+ passphraseState: 'wallet-identifier',
86
+ sessionId: 'wallet-session-id',
87
+ resumed: false,
88
+ },
89
+ },
90
+ 'openWalletSession'
91
+ );
92
+
93
+ expect(safeResponse).toEqual({
94
+ method: 'openWalletSession',
95
+ success: true,
96
+ payload: {
97
+ protocol: 'V2',
98
+ walletType: 'hidden',
99
+ deviceId: 'device-id',
100
+ passphraseState: '[REDACTED]',
101
+ sessionId: '[REDACTED]',
102
+ resumed: false,
103
+ },
104
+ });
105
+ });
106
+
56
107
  it.each(['deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
57
- 'blocks binary payload logging for %s',
108
+ 'keeps metadata and replaces binary payloads with their size for %s',
58
109
  method => {
59
- expect(getLogBlockLabel({ method, data: new Uint8Array(1024) })).toBe(method);
60
- expect(getLogBlockLabel({ payload: { method, data: new Uint8Array(1024) } })).toBe(method);
110
+ const request = { method, path: 'resource.bin', data: new Uint8Array(1024) };
111
+ expect(getLogBlockLabel(request)).toBe(method);
112
+ expect(getSafeLogPayload(request, method)).toEqual({
113
+ method,
114
+ path: 'resource.bin',
115
+ data: '[BINARY:1024]',
116
+ });
61
117
  }
62
118
  );
63
119
 
64
120
  it.each(['evmSignMessage', 'btcSignMessage', 'evmSignTransaction'])(
65
- 'blocks request and response payload logging for every API method including %s',
121
+ 'blocks request and response payload logging for signing method %s',
66
122
  method => {
67
123
  expect(getLogBlockLabel({ method, message: 'sensitive signing payload' })).toBe(method);
68
124
  expect(
@@ -74,6 +130,34 @@ describe('getLogBlockLabel', () => {
74
130
  }
75
131
  );
76
132
 
133
+ it('keeps ordinary API requests and responses visible', () => {
134
+ const request = { method: 'getDeviceState', connectId: 'connect-id', scope: 'runtime' };
135
+ const response = { success: true, payload: { protocol: 'V2', initialized: true } };
136
+
137
+ expect(getLogBlockLabel(request)).toBeUndefined();
138
+ expect(getSafeLogPayload(request)).toEqual(request);
139
+ expect(getSafeLogPayload(response)).toEqual(response);
140
+ });
141
+
142
+ it('redacts sensitive keys even for ordinary API payloads', () => {
143
+ expect(
144
+ getSafeLogPayload({
145
+ method: 'ordinaryMethod',
146
+ payload: { session_id: 'session-secret', nested: { pin: '1234' } },
147
+ })
148
+ ).toEqual({
149
+ method: 'ordinaryMethod',
150
+ payload: { session_id: '[REDACTED]', nested: { pin: '[REDACTED]' } },
151
+ });
152
+ });
153
+
154
+ it('puts sensitive API method names in the log label', () => {
155
+ expect(formatLogMethodLabel('response:', 'openWalletSession')).toBe(
156
+ 'response: [openWalletSession]'
157
+ );
158
+ expect(formatLogMethodLabel('response:')).toBe('response:');
159
+ });
160
+
77
161
  it('does not classify events without an API method as method calls', () => {
78
162
  expect(getLogBlockLabel({ event: 'DEVICE_EVENT', type: 'device-connect' })).toBeUndefined();
79
163
  });