@onekeyfe/hd-core 1.2.0-alpha.100 → 1.2.0-alpha.102

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 (56) hide show
  1. package/__tests__/bridgeBinaryPayload.test.ts +173 -0
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +2 -0
  3. package/__tests__/device-pool-state.test.ts +29 -0
  4. package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
  5. package/__tests__/get-device-state.test.ts +102 -13
  6. package/__tests__/protocol-v2-legacy-recovery.test.ts +29 -0
  7. package/__tests__/protocol-v2.test.ts +234 -1
  8. package/__tests__/refresh-device-state.test.ts +4 -1
  9. package/__tests__/search-devices.test.ts +2 -0
  10. package/__tests__/ton-sign-message.test.ts +84 -0
  11. package/dist/api/FirmwareUpdateV4.d.ts +3 -0
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/GetDeviceState.d.ts.map +1 -1
  14. package/dist/api/SearchDevices.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  16. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  17. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  18. package/dist/api/utils.d.ts.map +1 -1
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/protocolV2LegacyRecovery.d.ts +5 -0
  21. package/dist/core/protocolV2LegacyRecovery.d.ts.map +1 -0
  22. package/dist/device/Device.d.ts +9 -2
  23. package/dist/device/Device.d.ts.map +1 -1
  24. package/dist/device/DevicePool.d.ts +1 -1
  25. package/dist/device/DevicePool.d.ts.map +1 -1
  26. package/dist/index.d.ts +14 -4
  27. package/dist/index.js +263 -71
  28. package/dist/protocols/protocol-v2/features.d.ts +8 -0
  29. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  31. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  32. package/dist/topLevelInject.d.ts.map +1 -1
  33. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  34. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  35. package/dist/types/api/getDeviceState.d.ts +1 -0
  36. package/dist/types/api/getDeviceState.d.ts.map +1 -1
  37. package/dist/utils/bridgeBinaryPayload.d.ts +3 -0
  38. package/dist/utils/bridgeBinaryPayload.d.ts.map +1 -0
  39. package/package.json +4 -4
  40. package/src/api/FirmwareUpdateV4.ts +70 -23
  41. package/src/api/GetDeviceState.ts +1 -0
  42. package/src/api/SearchDevices.ts +2 -0
  43. package/src/api/firmware/FirmwareUpdatePlan.ts +37 -36
  44. package/src/api/firmware/protocolV2Release.ts +1 -0
  45. package/src/api/ton/TonSignMessage.ts +5 -3
  46. package/src/api/utils.ts +5 -2
  47. package/src/core/index.ts +4 -0
  48. package/src/core/protocolV2LegacyRecovery.ts +12 -0
  49. package/src/device/Device.ts +78 -20
  50. package/src/device/DevicePool.ts +17 -5
  51. package/src/protocols/protocol-v2/features.ts +10 -0
  52. package/src/protocols/protocol-v2/index.ts +2 -0
  53. package/src/topLevelInject.ts +4 -2
  54. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  55. package/src/types/api/getDeviceState.ts +2 -0
  56. package/src/utils/bridgeBinaryPayload.ts +137 -0
@@ -0,0 +1,173 @@
1
+ import { HardwareTopLevelSdk } from '../src';
2
+ import { findMethod } from '../src/api/utils';
3
+ import {
4
+ decodeBridgeBinaryPayload,
5
+ encodeBridgeBinaryPayload,
6
+ } from '../src/utils/bridgeBinaryPayload';
7
+
8
+ import type { LowLevelCoreApi } from '../src';
9
+
10
+ jest.mock('../src/data/config', () => ({
11
+ DEFAULT_DOMAIN: 'https://example.com/',
12
+ getSDKVersion: () => '0.0.0-test',
13
+ }));
14
+
15
+ const crossJsonOnlyBridge = (value: unknown): unknown => JSON.parse(JSON.stringify(value));
16
+
17
+ describe('bridge binary payload', () => {
18
+ test('preserves nested ArrayBuffer and sliced Uint8Array values', async () => {
19
+ const arrayBuffer = new Uint8Array([1, 2, 3]).buffer;
20
+ const backing = new Uint8Array([90, 4, 5, 6, 91]);
21
+ const encoded = await encodeBridgeBinaryPayload({
22
+ arrayBuffer,
23
+ bytes: backing.subarray(1, 4),
24
+ });
25
+ const decoded = decodeBridgeBinaryPayload(crossJsonOnlyBridge(encoded)) as {
26
+ arrayBuffer: ArrayBuffer;
27
+ bytes: Uint8Array;
28
+ };
29
+
30
+ expect(decoded.arrayBuffer).toBeInstanceOf(ArrayBuffer);
31
+ expect(Array.from(new Uint8Array(decoded.arrayBuffer))).toEqual([1, 2, 3]);
32
+ expect(decoded.bytes).toBeInstanceOf(Uint8Array);
33
+ expect(Array.from(decoded.bytes)).toEqual([4, 5, 6]);
34
+ });
35
+
36
+ test('encodes Blob input as byte data', async () => {
37
+ const encoded = await encodeBridgeBinaryPayload(new Blob([new Uint8Array([7, 8])]));
38
+ const decoded = decodeBridgeBinaryPayload(crossJsonOnlyBridge(encoded));
39
+
40
+ expect(decoded).toBeInstanceOf(Uint8Array);
41
+ expect(Array.from(decoded as Uint8Array)).toEqual([7, 8]);
42
+ });
43
+
44
+ test('routes portfolio, wallpaper and NFT bytes through the top-level boundary', async () => {
45
+ const restoredPayloads: Array<Record<string, any>> = [];
46
+ const lowLevelApi = {
47
+ call: jest.fn(params => {
48
+ const wirePayload = crossJsonOnlyBridge(params);
49
+ const method = findMethod({ id: 1, payload: wirePayload } as any);
50
+ restoredPayloads.push(method.payload);
51
+ return Promise.resolve({ success: true, payload: {} });
52
+ }),
53
+ init: jest.fn(() => Promise.resolve(true)),
54
+ } as unknown as LowLevelCoreApi;
55
+ const sdk = HardwareTopLevelSdk();
56
+ await sdk.init({}, lowLevelApi);
57
+
58
+ const portfolioBytes = new Uint8Array([1, 2, 3]).buffer;
59
+ const wallpaperBacking = new Uint8Array([90, 4, 5, 6, 91]);
60
+ const nftBacking = new Uint8Array([80, 7, 8, 9, 10, 81]);
61
+ await sdk.uploadPortfolio('connect-id', { packageBytes: portfolioBytes });
62
+ await sdk.deviceUploadWallpaper('connect-id', {
63
+ width: 604,
64
+ height: 1024,
65
+ rgba: wallpaperBacking.subarray(1, 4),
66
+ });
67
+ await sdk.deviceUploadNft('connect-id', {
68
+ image: { width: 540, height: 540, rgba: nftBacking.subarray(1, 3) },
69
+ thumbnail: { width: 263, height: 263, rgba: nftBacking.subarray(3, 5) },
70
+ title: 'NFT',
71
+ subtitle: '',
72
+ });
73
+
74
+ expect(Array.from(new Uint8Array(restoredPayloads[0].packageBytes))).toEqual([1, 2, 3]);
75
+ expect(Array.from(restoredPayloads[1].rgba)).toEqual([4, 5, 6]);
76
+ expect(Array.from(restoredPayloads[2].image.rgba)).toEqual([7, 8]);
77
+ expect(Array.from(restoredPayloads[2].thumbnail.rgba)).toEqual([9, 10]);
78
+ });
79
+
80
+ test('routes firmware update binaries through the top-level boundary', async () => {
81
+ const restoredPayloads: Array<Record<string, any>> = [];
82
+ const lowLevelApi = {
83
+ call: jest.fn(params => {
84
+ const wirePayload = crossJsonOnlyBridge(params);
85
+ const method = findMethod({ id: 1, payload: wirePayload } as any);
86
+ restoredPayloads.push(method.payload);
87
+ return Promise.resolve({ success: true, payload: {} });
88
+ }),
89
+ init: jest.fn(() => Promise.resolve(true)),
90
+ } as unknown as LowLevelCoreApi;
91
+ const sdk = HardwareTopLevelSdk();
92
+ await sdk.init({}, lowLevelApi);
93
+ const binary = (...bytes: number[]) => Uint8Array.from(bytes).buffer;
94
+
95
+ await sdk.firmwareUpdate('connect-id', {
96
+ binary: binary(1),
97
+ updateType: 'firmware',
98
+ });
99
+ await sdk.firmwareUpdateV2('connect-id', {
100
+ binary: binary(2),
101
+ updateType: 'firmware',
102
+ platform: 'ext',
103
+ });
104
+ await sdk.firmwareUpdateV3('connect-id', {
105
+ platform: 'ext',
106
+ bleBinary: binary(3),
107
+ firmwareBinary: binary(4),
108
+ bootloaderBinary: binary(5),
109
+ resourceBinary: binary(6),
110
+ });
111
+ await sdk.firmwareUpdateV4('connect-id', {
112
+ platform: 'ext',
113
+ targetsToUpdate: [
114
+ 'boot',
115
+ 'app_v1',
116
+ 'app_v2',
117
+ 'coprocessor',
118
+ 'se01',
119
+ 'se02',
120
+ 'se03',
121
+ 'se04',
122
+ 'resource',
123
+ ],
124
+ romloaderBinary: binary(7),
125
+ bootloaderBinary: binary(8),
126
+ applicationP1Binary: binary(9),
127
+ applicationP2Binary: binary(10),
128
+ coprocessorBinary: binary(11),
129
+ se01Binary: binary(12),
130
+ se02Binary: binary(13),
131
+ se03Binary: binary(14),
132
+ se04Binary: binary(15),
133
+ resourceArchiveBinary: binary(16),
134
+ });
135
+ await sdk.deviceUpdateBootloader('connect-id', { binary: binary(17) });
136
+ await sdk.deviceFullyUploadResource('connect-id', { binary: binary(18) });
137
+
138
+ expect(restoredPayloads).toHaveLength(6);
139
+ const restoredBinaries = [
140
+ restoredPayloads[0].binary,
141
+ restoredPayloads[1].binary,
142
+ restoredPayloads[2].bleBinary,
143
+ restoredPayloads[2].firmwareBinary,
144
+ restoredPayloads[2].bootloaderBinary,
145
+ restoredPayloads[2].resourceBinary,
146
+ restoredPayloads[3].romloaderBinary,
147
+ restoredPayloads[3].bootloaderBinary,
148
+ restoredPayloads[3].applicationP1Binary,
149
+ restoredPayloads[3].applicationP2Binary,
150
+ restoredPayloads[3].coprocessorBinary,
151
+ restoredPayloads[3].se01Binary,
152
+ restoredPayloads[3].se02Binary,
153
+ restoredPayloads[3].se03Binary,
154
+ restoredPayloads[3].se04Binary,
155
+ restoredPayloads[3].resourceArchiveBinary,
156
+ restoredPayloads[4].binary,
157
+ restoredPayloads[5].binary,
158
+ ];
159
+ expect(restoredBinaries.map(value => Array.from(new Uint8Array(value)))).toEqual(
160
+ Array.from({ length: 18 }, (_, index) => [index + 1])
161
+ );
162
+ });
163
+
164
+ test('rejects malformed tagged binary data', () => {
165
+ expect(() =>
166
+ decodeBridgeBinaryPayload({
167
+ __onekey_hd_bridge_binary_payload__: 1,
168
+ data: 'not-base64',
169
+ type: 'uint8-array',
170
+ })
171
+ ).toThrow('Invalid bridge binary payload data');
172
+ });
173
+ });
@@ -346,6 +346,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
346
346
  expect(typedCall).not.toHaveBeenCalled();
347
347
  expect(getDeviceState).toHaveBeenCalledWith({
348
348
  refreshSections: ['identity', 'versions'],
349
+ allowLegacyProtocolV2ProtocolInfo: true,
349
350
  });
350
351
  expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
351
352
  });
@@ -725,6 +726,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
725
726
  });
726
727
  expect(getDeviceState).toHaveBeenCalledWith({
727
728
  refreshSections: ['identity', 'versions', 'verification'],
729
+ allowLegacyProtocolV2ProtocolInfo: true,
728
730
  });
729
731
  });
730
732
 
@@ -38,6 +38,35 @@ describe('DevicePool state lifecycle', () => {
38
38
  expect(getDeviceState).toHaveBeenNthCalledWith(2, { refreshSections: ['settings'] });
39
39
  });
40
40
 
41
+ test('keeps legacy ProtocolInfo compatibility scoped to a discovery refresh', async () => {
42
+ const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
43
+ const run = jest.fn(async (callback: () => Promise<void>) => callback());
44
+ const device = {
45
+ isProtocolV2: () => true,
46
+ getDeviceState,
47
+ run,
48
+ } as any;
49
+
50
+ await (DevicePool as any)._refreshRuntimeState(device, {
51
+ refreshRuntimeState: true,
52
+ allowLegacyProtocolV2ProtocolInfo: true,
53
+ });
54
+
55
+ expect(run).toHaveBeenCalledWith(expect.any(Function), {
56
+ connectProtocol: undefined,
57
+ forceProtocolDetection: undefined,
58
+ allowLegacyProtocolV2ProtocolInfo: true,
59
+ });
60
+ expect(getDeviceState).toHaveBeenNthCalledWith(1, {
61
+ refreshSections: ['status'],
62
+ allowLegacyProtocolV2ProtocolInfo: true,
63
+ });
64
+ expect(getDeviceState).toHaveBeenNthCalledWith(2, {
65
+ refreshSections: ['settings'],
66
+ allowLegacyProtocolV2ProtocolInfo: true,
67
+ });
68
+ });
69
+
41
70
  test('actively re-detects a cached device when protocol detection is forced', async () => {
42
71
  const descriptor = { path: 'cached-path', protocolType: 'V1' } as any;
43
72
  const device = Device.fromDescriptor(descriptor);
@@ -376,62 +376,66 @@ describe('buildFirmwareUpdatePlan', () => {
376
376
  }
377
377
  );
378
378
 
379
- test('rejects a legacy resource archive without complete integrity metadata', () => {
380
- expectFirmwarePlanInvalid(
381
- () =>
382
- buildFirmwareUpdatePlan(
383
- createLegacyForceInput(['resource'], {
384
- url: 'https://firmware.onekey.so/pro/firmware.bin',
385
- resource: 'https://firmware.onekey.so/pro/resource.zip',
386
- resourceExpectedSize: 4096,
387
- })
388
- ),
389
- 'integrity metadata is invalid'
390
- );
391
- });
379
+ test.each([
380
+ {
381
+ executor: 'v2',
382
+ deviceType: EDeviceType.Classic1s,
383
+ bootloaderVersion: '2.1.2',
384
+ },
385
+ {
386
+ executor: 'v3',
387
+ deviceType: EDeviceType.Pro,
388
+ bootloaderVersion: '2.8.4',
389
+ },
390
+ ])(
391
+ 'allows $executor config releases without optional integrity metadata',
392
+ ({ executor, deviceType, bootloaderVersion }) => {
393
+ const plan = buildFirmwareUpdatePlan({
394
+ features: createFeatures({
395
+ deviceType,
396
+ firmwareVersion: '4.21.0',
397
+ bootloaderVersion,
398
+ }),
399
+ firmwareType: EFirmwareType.Universal,
400
+ platform: 'desktop',
401
+ firmware: {
402
+ status: 'valid',
403
+ release: {
404
+ url: 'https://firmware.onekey.so/legacy/firmware.bin',
405
+ version: [4, 21, 0],
406
+ fingerprint: '',
407
+ resource: 'https://firmware.onekey.so/legacy/resource.zip',
408
+ resourceFingerprint: '',
409
+ },
410
+ },
411
+ ble: {
412
+ status: 'valid',
413
+ release: {
414
+ webUpdate: 'https://firmware.onekey.so/legacy/ble.bin',
415
+ version: [2, 3, 7],
416
+ fingerprintWeb: '',
417
+ },
418
+ },
419
+ bootloader: {
420
+ status: 'valid',
421
+ release: {
422
+ bootloaderResource: 'https://firmware.onekey.so/legacy/bootloader.bin',
423
+ bootloaderVersion: [2, 8, 4],
424
+ bootloaderFingerprint: '',
425
+ },
426
+ },
427
+ forceUpdateTargets: ['firmware', 'resource', 'ble', 'bootloader'],
428
+ });
392
429
 
393
- test.each(['firmware', 'ble', 'bootloader'] as const)(
394
- 'rejects a remote legacy %s artifact without complete integrity metadata',
395
- target => {
396
- expectFirmwarePlanInvalid(
397
- () =>
398
- buildFirmwareUpdatePlan({
399
- features: createFeatures({ deviceType: EDeviceType.Classic1s }),
400
- firmwareType: EFirmwareType.Universal,
401
- platform: 'desktop',
402
- firmware:
403
- target === 'firmware'
404
- ? {
405
- status: 'outdated',
406
- release: {
407
- url: 'https://firmware.onekey.so/classic/firmware.bin',
408
- expectedSize: 1024,
409
- },
410
- }
411
- : noUpdate,
412
- ble:
413
- target === 'ble'
414
- ? {
415
- status: 'outdated',
416
- release: {
417
- webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
418
- expectedSize: 512,
419
- },
420
- }
421
- : noUpdate,
422
- bootloader:
423
- target === 'bootloader'
424
- ? {
425
- status: 'outdated',
426
- release: {
427
- bootloaderResource: 'https://firmware.onekey.so/classic/bootloader.bin',
428
- bootloaderExpectedSize: 768,
429
- },
430
- }
431
- : noUpdate,
432
- }),
433
- 'integrity metadata is invalid'
434
- );
430
+ expect(plan.executor).toBe(executor);
431
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual([
432
+ 'bootloader',
433
+ 'firmware',
434
+ 'resource',
435
+ 'ble',
436
+ ]);
437
+ expect(plan.artifacts.every(artifact => artifact.expectedSize === undefined)).toBe(true);
438
+ expect(plan.artifacts.every(artifact => artifact.expectedSha256 === undefined)).toBe(true);
435
439
  }
436
440
  );
437
441
 
@@ -202,25 +202,111 @@ describe('getDeviceState', () => {
202
202
  expect(state.versions.se01).toBe('1.0.0');
203
203
  });
204
204
 
205
- test.each(['bootloader', 'romloader'] as const)(
206
- 'uses ProtocolInfo to preserve %s mode without DeviceStatusGet',
207
- async mode => {
205
+ test.each([
206
+ ['bootloader', EDeviceType.Pro2, DeviceType.PRO2],
207
+ ['romloader', EDeviceType.Pro2, DeviceType.PRO2],
208
+ ['bootloader', EDeviceType.Neo, DeviceType.NEO],
209
+ ['romloader', EDeviceType.Neo, DeviceType.NEO],
210
+ ] as const)(
211
+ 'refreshes cached %s state after %s reboots into the application',
212
+ async (mode, deviceType, protocolV2DeviceType) => {
208
213
  const typedCall = jest.fn().mockImplementation((requestType: string) => {
209
- if (requestType === 'DeviceInfoGet') {
214
+ if (requestType === 'ProtocolInfoRequest') {
215
+ return { message: protocolV2ApplicationInfo };
216
+ }
217
+ if (requestType === 'DeviceStatusGet') {
210
218
  return {
211
- message: {
212
- hw: { Device_type: DeviceType.PRO2, serial_no: 'SERIAL-1' },
219
+ message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
220
+ };
221
+ }
222
+ throw new Error(`Unexpected request: ${requestType}`);
223
+ });
224
+ const device = createV2Device(typedCall);
225
+ device.updateState(
226
+ {
227
+ protocol: 'V2',
228
+ identity: { deviceType },
229
+ status: { mode },
230
+ raw: {
231
+ protocolV2DeviceInfo: {
232
+ hw: { Device_type: protocolV2DeviceType, serial_no: 'SERIAL-1' },
213
233
  fw:
214
234
  mode === 'romloader'
215
235
  ? { romloader: { version: '1.0.0' } }
216
236
  : { bootloader: { version: '1.0.0' } },
217
237
  },
218
- };
219
- }
220
- if (requestType === 'ProtocolInfoRequest') {
221
- return { message: getProtocolV2LoaderInfo(mode) };
222
- }
223
- throw new Error(`Unexpected request: ${requestType}`);
238
+ protocolV2ProtocolInfo: getProtocolV2LoaderInfo(mode),
239
+ },
240
+ },
241
+ 'initialize'
242
+ );
243
+
244
+ const state = await device.getDeviceState({ refreshSections: ['status'] });
245
+
246
+ expect(state.status.mode).toBe('normal');
247
+ expect(state.identity.deviceId).toBe('wallet-1');
248
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
249
+ 'ProtocolInfoRequest',
250
+ 'DeviceStatusGet',
251
+ ]);
252
+ }
253
+ );
254
+
255
+ test.each([
256
+ [EDeviceType.Pro2, DeviceType.PRO2],
257
+ [EDeviceType.Neo, DeviceType.NEO],
258
+ ] as const)('invalidates cached loader state when %s is cancelled', async (deviceType, type) => {
259
+ const typedCall = jest.fn().mockImplementation((requestType: string) => {
260
+ if (requestType === 'DeviceInfoGet') {
261
+ return {
262
+ message: {
263
+ protocol_version: 2,
264
+ hw: { Device_type: type, serial_no: 'SERIAL-1' },
265
+ fw: { application: { version: '5.0.0' } },
266
+ },
267
+ };
268
+ }
269
+ if (requestType === 'ProtocolInfoRequest') {
270
+ return { message: protocolV2ApplicationInfo };
271
+ }
272
+ if (requestType === 'DeviceStatusGet') {
273
+ return {
274
+ message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
275
+ };
276
+ }
277
+ throw new Error(`Unexpected request: ${requestType}`);
278
+ });
279
+ const cancel = jest.fn().mockResolvedValue(undefined);
280
+ const device = createV2Device(typedCall);
281
+ (device as any).commands = { cancel, typedCall };
282
+ device.updateState(
283
+ {
284
+ protocol: 'V2',
285
+ identity: { deviceType },
286
+ status: { mode: 'bootloader' },
287
+ raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo('bootloader') },
288
+ },
289
+ 'initialize'
290
+ );
291
+
292
+ await device.interruptionFromUser();
293
+ await device.initialize();
294
+
295
+ expect(cancel).toHaveBeenCalledTimes(1);
296
+ expect(device.state?.status.mode).toBe('normal');
297
+ expect(device.state?.identity.deviceId).toBe('wallet-1');
298
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
299
+ 'DeviceInfoGet',
300
+ 'ProtocolInfoRequest',
301
+ 'DeviceStatusGet',
302
+ ]);
303
+ });
304
+
305
+ test.each(['bootloader', 'romloader'] as const)(
306
+ 'keeps live %s mode after refreshing cached loader state',
307
+ async mode => {
308
+ const typedCall = jest.fn().mockResolvedValue({
309
+ message: getProtocolV2LoaderInfo(mode),
224
310
  });
225
311
  const device = createV2Device(typedCall);
226
312
  device.updateState(
@@ -236,7 +322,10 @@ describe('getDeviceState', () => {
236
322
  const state = await device.getDeviceState({ refreshSections: ['status'] });
237
323
 
238
324
  expect(state.status.mode).toBe(mode);
239
- expect(typedCall).not.toHaveBeenCalled();
325
+ expect(typedCall).toHaveBeenCalledTimes(1);
326
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
327
+ eventless_wallet_session: true,
328
+ });
240
329
  }
241
330
  );
242
331
 
@@ -0,0 +1,29 @@
1
+ import { isLegacyProtocolV2FirmwareRecoveryMethod } from '../src/core/protocolV2LegacyRecovery';
2
+
3
+ import type { BaseMethod } from '../src/api/BaseMethod';
4
+
5
+ const createMethod = (
6
+ name: string,
7
+ payload: Record<string, unknown>
8
+ ): Pick<BaseMethod, 'name' | 'payload'> => ({
9
+ name,
10
+ payload: { method: name, ...payload },
11
+ });
12
+
13
+ describe('Protocol V2 legacy firmware recovery policy', () => {
14
+ test.each([
15
+ ['firmware state read', 'getDeviceState', { scope: 'firmware' }],
16
+ ['firmware release check', 'checkAllFirmwareRelease', {}],
17
+ ['firmware update', 'firmwareUpdateV4', {}],
18
+ ])('allows legacy ProtocolInfo for %s', (_label, name, payload) => {
19
+ expect(isLegacyProtocolV2FirmwareRecoveryMethod(createMethod(name, payload))).toBe(true);
20
+ });
21
+
22
+ test.each([
23
+ ['runtime state read', 'getDeviceState', { scope: 'runtime' }],
24
+ ['settings state read', 'getDeviceState', { scope: 'settings' }],
25
+ ['ordinary API', 'getFeatures', {}],
26
+ ])('rejects legacy ProtocolInfo for %s', (_label, name, payload) => {
27
+ expect(isLegacyProtocolV2FirmwareRecoveryMethod(createMethod(name, payload))).toBe(false);
28
+ });
29
+ });