@onekeyfe/hd-core 1.2.0-alpha.32 → 1.2.0-alpha.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +31 -0
- package/__tests__/core-dispose.test.ts +3 -3
- package/__tests__/device-lifecycle-events.test.ts +52 -0
- package/__tests__/device-pool-state.test.ts +14 -2
- package/__tests__/device-settings.test.ts +22 -0
- package/__tests__/device-state-mapper.test.ts +13 -2
- package/__tests__/device-state-projector.test.ts +2 -2
- package/__tests__/device-utils.test.ts +1 -0
- package/__tests__/device-wallet-session-store.test.ts +64 -17
- package/__tests__/kaspaSignTransaction.test.ts +9 -0
- package/__tests__/known-device-public-types.type-test.ts +3 -0
- package/__tests__/logBlockEvent.test.ts +89 -5
- package/__tests__/open-wallet-session.test.ts +334 -30
- package/__tests__/pro2Wallpaper.test.ts +1 -1
- package/__tests__/protocol-v2.test.ts +708 -75
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +7 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -0
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/device/DeviceStateProjector.d.ts.map +1 -1
- package/dist/device/DeviceWalletSessionStore.d.ts +8 -0
- package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
- package/dist/events/logBlockEvent.d.ts +2 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +456 -222
- package/dist/protocols/protocol-v2/walletSession.d.ts +0 -10
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -1
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/device.d.ts +4 -2
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +11 -81
- package/src/api/FirmwareUpdateV4.ts +33 -17
- package/src/api/GetPassphraseState.ts +1 -3
- package/src/api/OpenWalletSession.ts +3 -3
- package/src/api/device/DeviceSettings.ts +8 -1
- package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
- package/src/core/index.ts +7 -5
- package/src/device/Device.ts +37 -9
- package/src/device/DeviceConnector.ts +10 -7
- package/src/device/DevicePool.ts +5 -3
- package/src/device/DeviceStateMapper.ts +5 -3
- package/src/device/DeviceStateProjector.ts +0 -7
- package/src/device/DeviceWalletSessionStore.ts +57 -4
- package/src/deviceProfile/buildDeviceFeatures.ts +10 -3
- package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
- package/src/events/logBlockEvent.ts +84 -1
- package/src/protocols/protocol-v2/deviceSession.ts +1 -1
- package/src/protocols/protocol-v2/walletSession.ts +116 -34
- package/src/types/api/openWalletSession.ts +1 -1
- package/src/types/device.ts +6 -2
- package/src/types/params.ts +2 -1
- package/src/utils/deviceFeaturesCompat.ts +8 -1
- package/src/utils/pro2Wallpaper.ts +3 -9
- package/tsconfig.type-tests.json +5 -1
|
@@ -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
|
|
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(
|
|
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(
|
|
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.
|
|
12
|
+
DevicePool.dispose();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
afterEach(() => {
|
|
16
|
-
DevicePool.
|
|
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
|
|
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
|
|
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
|
|
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', () => {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
-
'
|
|
108
|
+
'keeps metadata and replaces binary payloads with their size for %s',
|
|
58
109
|
method => {
|
|
59
|
-
|
|
60
|
-
expect(getLogBlockLabel(
|
|
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
|
|
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
|
});
|