@onekeyfe/hd-core 1.2.0-alpha.147 → 1.2.0-alpha.149

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 (39) hide show
  1. package/__tests__/device-lifecycle-events.test.ts +177 -2
  2. package/__tests__/device-utils.test.ts +8 -0
  3. package/__tests__/deviceUploadNft.test.ts +3 -0
  4. package/__tests__/protocol-v2-resources.test.ts +35 -89
  5. package/__tests__/protocol-v2-unlock-policy.test.ts +72 -0
  6. package/__tests__/protocol-v2.test.ts +51 -188
  7. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  8. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  9. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  10. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  11. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  12. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  13. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  14. package/dist/core/index.d.ts +2 -0
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/device/Device.d.ts +6 -0
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/index.d.ts +17 -20
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +212 -314
  21. package/dist/protocols/protocol-v2/resources.d.ts +11 -7
  22. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  23. package/dist/types/settings.d.ts +0 -13
  24. package/dist/types/settings.d.ts.map +1 -1
  25. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/api/FirmwareUpdateV4.ts +61 -238
  28. package/src/api/PromptWebDeviceAccess.ts +2 -2
  29. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  30. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  31. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  32. package/src/api/protocol-v2/DeviceUploadNft.ts +5 -1
  33. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +5 -1
  34. package/src/core/index.ts +31 -106
  35. package/src/device/Device.ts +71 -8
  36. package/src/index.ts +0 -4
  37. package/src/protocols/protocol-v2/resources.ts +92 -102
  38. package/src/types/settings.ts +0 -15
  39. package/src/utils/deviceInfoUtils.ts +7 -2
@@ -1,7 +1,13 @@
1
1
  import { EDeviceType, ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
2
  import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
3
3
 
4
- import { initConnector, initCore, isMissingDetectedProtocolV2Error } from '../src/core';
4
+ import {
5
+ initConnector,
6
+ initCore,
7
+ isMissingDetectedProtocolV2Error,
8
+ isRetryableBleConnectionError,
9
+ isRetryableBleProtocolV2ProbeError,
10
+ } from '../src/core';
5
11
  import { DataManager } from '../src/data-manager';
6
12
  import TransportManager from '../src/data-manager/TransportManager';
7
13
  import { Device } from '../src/device/Device';
@@ -269,6 +275,38 @@ describe('public device lifecycle events', () => {
269
275
  }
270
276
  );
271
277
 
278
+ test.each([
279
+ [HardwareErrorCode.RuntimeError, true],
280
+ [HardwareErrorCode.BleDeviceBondError, false],
281
+ ] as const)(
282
+ 'retries a Protocol V2 probe mismatch with error code %s: %s',
283
+ (errorCode, expected) => {
284
+ const method = { payload: { connectProtocol: 'V2' } } as never;
285
+ const error = {
286
+ errorCode,
287
+ message:
288
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol',
289
+ };
290
+
291
+ expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(expected);
292
+ }
293
+ );
294
+
295
+ test.each([
296
+ [HardwareErrorCode.BleConnectedError, true],
297
+ [HardwareErrorCode.BleTimeoutError, true],
298
+ [HardwareErrorCode.PollingTimeout, false],
299
+ [HardwareErrorCode.BleDeviceBondError, false],
300
+ ] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
301
+ const method = { payload: { connectProtocol: 'V2' } } as never;
302
+ const error = {
303
+ errorCode,
304
+ message: 'BLE setup wedged repeatedly',
305
+ };
306
+
307
+ expect(isRetryableBleConnectionError(method, error)).toBe(expected);
308
+ });
309
+
272
310
  test('converts an internal transport disconnect into a public KnownDevice snapshot', () => {
273
311
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
274
312
  core = initCore();
@@ -346,7 +384,7 @@ describe('public device lifecycle events', () => {
346
384
  );
347
385
 
348
386
  test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
349
- 'sends a fallback Cancel for an acquired Protocol V2 %s call without a prompt callback',
387
+ 'sends a fallback Cancel for an acquired Protocol V2 %s call with an open UI',
350
388
  async env => {
351
389
  jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
352
390
  const device = createInitializedDevice('V2');
@@ -360,6 +398,7 @@ describe('public device lifecycle events', () => {
360
398
  cancelDevice,
361
399
  cancel,
362
400
  } as never;
401
+ device.createProtocolV2UiPhaseMetadata('button', 'start');
363
402
 
364
403
  await device.interruptionFromUser();
365
404
 
@@ -369,6 +408,142 @@ describe('public device lifecycle events', () => {
369
408
  }
370
409
  );
371
410
 
411
+ test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
412
+ 'does not send Cancel for an acquired Protocol V2 %s connect without an open UI',
413
+ async env => {
414
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
415
+ const device = createInitializedDevice('V2');
416
+ const post = jest.fn().mockResolvedValue(undefined);
417
+ const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
418
+ const cancel = jest.fn().mockResolvedValue(undefined);
419
+ const disconnect = jest.fn().mockResolvedValue(undefined);
420
+ device.originalDescriptor.session = device.mainId;
421
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
422
+ device.deviceConnector = { disconnect } as never;
423
+ device.commands = {
424
+ transport: { post },
425
+ cancelDevice,
426
+ cancel,
427
+ } as never;
428
+
429
+ await device.interruptionFromUser();
430
+
431
+ expect(cancelDevice).not.toHaveBeenCalled();
432
+ expect(post).not.toHaveBeenCalled();
433
+ expect(disconnect).not.toHaveBeenCalled();
434
+ expect(cancel).toHaveBeenCalledTimes(1);
435
+ }
436
+ );
437
+
438
+ test('disconnects a BLE link without sending Cancel when the session is not acquired', async () => {
439
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
440
+ const device = createInitializedDevice('V2');
441
+ const post = jest.fn().mockResolvedValue(undefined);
442
+ const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
443
+ const cancel = jest.fn().mockResolvedValue(undefined);
444
+ const disconnect = jest.fn().mockResolvedValue(undefined);
445
+ device.deviceConnector = { disconnect } as never;
446
+ device.commands = {
447
+ transport: { post },
448
+ cancelDevice,
449
+ cancel,
450
+ } as never;
451
+
452
+ await device.interruptionFromUser();
453
+
454
+ expect(cancelDevice).not.toHaveBeenCalled();
455
+ expect(post).not.toHaveBeenCalled();
456
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
457
+ expect(device.hasDeviceAcquire()).toBe(false);
458
+ expect(device.wasInterruptedByUser()).toBe(true);
459
+ expect(cancel).toHaveBeenCalledTimes(1);
460
+ });
461
+
462
+ test('does not finish acquire after the user already cancelled', async () => {
463
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
464
+ const device = createInitializedDevice('V2');
465
+ const disconnect = jest.fn().mockResolvedValue(undefined);
466
+ let resolveAcquire: ((value: { uuid: string; protocolType: 'V2' }) => void) | undefined;
467
+ const acquire = jest.fn(
468
+ () =>
469
+ new Promise<{ uuid: string; protocolType: 'V2' }>(resolve => {
470
+ resolveAcquire = resolve;
471
+ })
472
+ );
473
+ device.deviceConnector = { acquire, disconnect } as never;
474
+
475
+ const acquirePromise = device.acquire('V2');
476
+ await device.interruptionFromUser();
477
+ resolveAcquire?.({ uuid: 'late-session', protocolType: 'V2' });
478
+
479
+ await expect(acquirePromise).rejects.toMatchObject({
480
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
481
+ });
482
+ expect(disconnect).toHaveBeenCalledWith('late-session');
483
+ expect(device.hasDeviceAcquire()).toBe(false);
484
+ });
485
+
486
+ test('does not retry a BLE connection error after the user cancelled', () => {
487
+ const device = createInitializedDevice('V2');
488
+ device.beginConnectionAttempt();
489
+ (device as unknown as { interruptedAttempt: number }).interruptedAttempt = (
490
+ device as unknown as { connectionAttempt: number }
491
+ ).connectionAttempt;
492
+ const method = { payload: { connectProtocol: 'V2' }, device } as never;
493
+ const error = {
494
+ errorCode: HardwareErrorCode.BleConnectedError,
495
+ message: 'BLE setup wedged repeatedly',
496
+ };
497
+
498
+ expect(isRetryableBleConnectionError(method, error)).toBe(false);
499
+ });
500
+
501
+ test('allows a new BLE connect after the previous attempt was cancelled', async () => {
502
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
503
+ const device = createInitializedDevice('V2');
504
+ const disconnect = jest.fn().mockResolvedValue(undefined);
505
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'next-session', protocolType: 'V2' });
506
+ device.deviceConnector = { acquire, disconnect } as never;
507
+
508
+ device.beginConnectionAttempt();
509
+ await device.interruptionFromUser();
510
+ expect(device.wasInterruptedByUser()).toBe(true);
511
+
512
+ device.beginConnectionAttempt();
513
+ await expect(device.acquire('V2')).resolves.toBeUndefined();
514
+
515
+ expect(device.wasInterruptedByUser()).toBe(false);
516
+ expect(device.hasDeviceAcquire()).toBe(true);
517
+ expect(acquire).toHaveBeenCalledTimes(1);
518
+ });
519
+
520
+ test('discards a late cancelled acquire after a newer connect attempt starts', async () => {
521
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
522
+ const device = createInitializedDevice('V2');
523
+ const disconnect = jest.fn().mockResolvedValue(undefined);
524
+ let resolveAcquire: ((value: { uuid: string; protocolType: 'V2' }) => void) | undefined;
525
+ const acquire = jest.fn(
526
+ () =>
527
+ new Promise<{ uuid: string; protocolType: 'V2' }>(resolve => {
528
+ resolveAcquire = resolve;
529
+ })
530
+ );
531
+ device.deviceConnector = { acquire, disconnect } as never;
532
+
533
+ device.beginConnectionAttempt();
534
+ const cancelledAcquire = device.acquire('V2');
535
+ await device.interruptionFromUser();
536
+ device.beginConnectionAttempt();
537
+ resolveAcquire?.({ uuid: 'stale-session', protocolType: 'V2' });
538
+
539
+ await expect(cancelledAcquire).rejects.toMatchObject({
540
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
541
+ });
542
+ expect(disconnect).toHaveBeenCalledWith('stale-session');
543
+ expect(device.hasDeviceAcquire()).toBe(false);
544
+ expect(device.wasInterruptedByUser()).toBe(false);
545
+ });
546
+
372
547
  test('waits for the canceled run to finish releasing before cancellation completes', async () => {
373
548
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
374
549
  const device = createInitializedDevice('V2');
@@ -123,6 +123,14 @@ describe('device feature selectors', () => {
123
123
  test('recognizes Neo discovery names before initialization', () => {
124
124
  expect(getDeviceTypeByBleName('OneKey Neo 1234')).toBe(EDeviceType.Neo);
125
125
  expect(getDeviceTypeByBleName('Neo 1234')).toBe(EDeviceType.Neo);
126
+ expect(getDeviceTypeByBleName('Neo22D8')).toBe(EDeviceType.Neo);
127
+ expect(getDeviceTypeByBleName('OneKeyNeo22D8')).toBe(EDeviceType.Neo);
128
+ });
129
+
130
+ test('recognizes compact Pro2 discovery names before initialization', () => {
131
+ expect(getDeviceTypeByBleName('Pro2 A1B2')).toBe(EDeviceType.Pro2);
132
+ expect(getDeviceTypeByBleName('Pro2A1B2')).toBe(EDeviceType.Pro2);
133
+ expect(getDeviceTypeByBleName('OneKeyPro2A1B2')).toBe(EDeviceType.Pro2);
126
134
  });
127
135
 
128
136
  test.each([EDeviceType.Pro2, EDeviceType.Neo])(
@@ -1,4 +1,5 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
3
  import { encode as encodeJpeg } from 'jpeg-js';
3
4
 
4
5
  import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
@@ -110,6 +111,8 @@ describe('DeviceUploadNft', () => {
110
111
 
111
112
  method.init();
112
113
 
114
+ expect(method.unlockPolicy).toBe('unlock-before-run');
115
+ expect(method.protocolV2PreUnlockPinType).toBe(DeviceSessionPinType.Any);
113
116
  expect((method as any).params).toMatchObject({
114
117
  chunkSize: 2048,
115
118
  paceMs: 0,
@@ -1,10 +1,9 @@
1
1
  import axios from 'axios';
2
- import { sha256 } from '@noble/hashes/sha256';
3
2
  import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
4
3
 
5
4
  import { DataManager } from '../src/data-manager';
6
5
  import {
7
- parseProtocolV2ResourceManifest,
6
+ parseProtocolV2ResourcePackage,
8
7
  parseProtocolV2Resources,
9
8
  } from '../src/protocols/protocol-v2/resources';
10
9
 
@@ -16,9 +15,6 @@ jest.mock('../src/data/config', () => ({
16
15
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0-test/',
17
16
  }));
18
17
 
19
- const bytesToHex = (bytes: Uint8Array) =>
20
- Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
21
-
22
18
  const resourceSource = {
23
19
  archiveUrl: 'https://example.com/resource/pro2-resource.zip',
24
20
  archiveSha256: 'a'.repeat(64),
@@ -31,65 +27,21 @@ const neoResourceSource = {
31
27
  archiveSize: 12_345_678,
32
28
  };
33
29
 
34
- const manifestFiles = [
35
- ['bundles/firmware_logo-build.okpkg', 'firmware_logo.okpkg', 'vol0:/bundles/firmware_logo.okpkg'],
36
- ['bundles/font/noto-build.okpkg', 'noto.okpkg', 'vol0:/bundles/font/noto.okpkg'],
37
- ['bundles/font/roobert-build.okpkg', 'roobert.okpkg', 'vol0:/bundles/font/roobert.okpkg'],
38
- [
39
- 'bundles/images/animation-build.okpkg',
40
- 'animation.okpkg',
41
- 'vol0:/bundles/images/animation.okpkg',
42
- ],
43
- ['bundles/images/images-build.okpkg', 'images.okpkg', 'vol0:/bundles/images/images.okpkg'],
44
- [
45
- 'bundles/images/wallpaper-build.okpkg',
46
- 'wallpaper.okpkg',
47
- 'vol0:/bundles/images/wallpaper.okpkg',
48
- ],
49
- [
50
- 'bundles/translations/translations-build.okpkg',
51
- 'translations.okpkg',
52
- 'vol0:/bundles/translations/translations.okpkg',
53
- ],
54
- [
55
- 'loaders/bootloader/boot_resource-build.okpkg',
56
- 'boot_resource.okpkg',
57
- 'vol0:/loaders/bootloader/boot_resource.okpkg',
58
- ],
59
- ['loaders/rom/params-build.okpkg', 'params.okpkg', 'vol0:/loaders/rom/params.okpkg'],
60
- ].map(([archive_path, original_name, device_path], index) => {
61
- const binary = new Uint8Array([index + 1]).buffer;
62
- return {
63
- archive_path,
64
- original_name,
65
- device_path,
66
- size: 1,
67
- sha256: bytesToHex(sha256(new Uint8Array(binary))),
68
- signed: true,
69
- sig_algo: device_path.startsWith('vol0:/bundles/') ? 'ed25519' : 'mldsa65',
70
- payload_version: '1.0.0',
71
- binary,
72
- };
73
- });
74
-
75
- const resourceManifest = {
76
- schema: 1,
77
- artifact_name: 'pro2-resource-build',
78
- release_name: 'resource-build',
79
- variant: 'resource',
80
- commit: 'a'.repeat(40),
81
- short_sha: 'aaaaaaa',
82
- timestamp_utc: '20260807_091424',
83
- core_version: '1.0.0',
84
- key_set: 'dev',
85
- device_root: 'vol0:',
86
- restore_mode: 'bootloader_update',
87
- trees: [
88
- { path: 'bundles', device: 'vol0:/bundles' },
89
- { path: 'loaders/bootloader', device: 'vol0:/loaders/bootloader' },
90
- { path: 'loaders/rom', device: 'vol0:/loaders/rom' },
91
- ],
92
- files: manifestFiles.map(({ binary: _binary, ...file }) => file),
30
+ const createResourcePackage = (devicePath: string) => {
31
+ const headerSize = 0x5f90;
32
+ const bytes = new Uint8Array(headerSize + 1);
33
+ bytes.set(new TextEncoder().encode('OKPP'), 0);
34
+ bytes.set(new TextEncoder().encode('RESC'), 0x08);
35
+ bytes.set(new TextEncoder().encode(devicePath), 0x6c);
36
+ const view = new DataView(bytes.buffer);
37
+ view.setUint32(0x04, 1, true);
38
+ view.setUint32(0x0c, headerSize, true);
39
+ view.setUint32(0x10, 0x010203, true);
40
+ view.setUint32(0x14, 1, true);
41
+ bytes.fill(0x11, 0x200, 0x240);
42
+ bytes.fill(0x22, 0x240, 0x280);
43
+ bytes[headerSize] = 0xaa;
44
+ return bytes;
93
45
  };
94
46
 
95
47
  const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
@@ -146,34 +98,36 @@ describe('Pro2 resource configuration', () => {
146
98
  DataManager.lastCheckTimestamp = 0;
147
99
  });
148
100
 
149
- test('accepts the manifest source and the CI schema 1 file set', () => {
101
+ test('accepts the release archive source', () => {
150
102
  expect(parseProtocolV2Resources({ source: resourceSource })).toEqual({
151
103
  source: resourceSource,
152
104
  });
153
- expect(parseProtocolV2ResourceManifest(resourceManifest).files).toHaveLength(9);
154
105
  });
155
106
 
156
- test('uses only the resource file list from a manifest', () => {
107
+ test('reads the version, hashes, and direct device path from a RESC package', () => {
157
108
  expect(
158
- parseProtocolV2ResourceManifest({
159
- files: resourceManifest.files,
160
- })
161
- ).toEqual({ files: resourceManifest.files });
109
+ parseProtocolV2ResourcePackage(createResourcePackage('vol0:/bundles/images.okpkg'))
110
+ ).toEqual({
111
+ version: [1, 2, 3],
112
+ payloadLength: 1,
113
+ devicePath: 'vol0:/bundles/images.okpkg',
114
+ payloadHash: '11'.repeat(64),
115
+ headerHash: '22'.repeat(64),
116
+ });
162
117
  });
163
118
 
164
- test('ignores release metadata and accepts a partial resource file set', () => {
119
+ test('accepts the boot resource staging path and rejects paths outside resource roots', () => {
165
120
  expect(
166
- parseProtocolV2ResourceManifest({
167
- schema: 1,
168
- variant: 'dev_resource',
169
- restore_mode: 'bootloader_update',
170
- trees: [],
171
- files: [resourceManifest.files[0]],
172
- })
173
- ).toEqual({ files: [resourceManifest.files[0]] });
121
+ parseProtocolV2ResourcePackage(
122
+ createResourcePackage('vol0:/loaders/bootloader/boot_resource.okpkg.staging')
123
+ ).devicePath
124
+ ).toBe('vol0:/loaders/bootloader/boot_resource.okpkg.staging');
125
+ expect(() =>
126
+ parseProtocolV2ResourcePackage(createResourcePackage('vol0:/unexpected/images.okpkg'))
127
+ ).toThrow('device path');
174
128
  });
175
129
 
176
- test('rejects missing source and malformed manifest paths', () => {
130
+ test('rejects missing or malformed release sources', () => {
177
131
  expect(() => parseProtocolV2Resources({})).toThrow('source is required');
178
132
  expect(() =>
179
133
  parseProtocolV2Resources({
@@ -193,14 +147,6 @@ describe('Pro2 resource configuration', () => {
193
147
  source: { ...resourceSource, archiveSize: 0 },
194
148
  })
195
149
  ).toThrow('positive integer');
196
- expect(() =>
197
- parseProtocolV2ResourceManifest({
198
- ...resourceManifest,
199
- files: resourceManifest.files.map((file, index) =>
200
- index === 0 ? { ...file, archive_path: '../outside.okpkg' } : file
201
- ),
202
- })
203
- ).toThrow('archive_path');
204
150
  });
205
151
 
206
152
  test('applies a validated pre-release config and exposes its archive source', async () => {
@@ -1,11 +1,17 @@
1
1
  import TransportUtils, { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
+ import { encode as encodeJpeg } from 'jpeg-js';
2
3
 
3
4
  import ConfluxSignMessageCIP23 from '../src/api/conflux/ConfluxSignMessageCIP23';
4
5
  import DeviceChangePin from '../src/api/device/DeviceChangePin';
5
6
  import DeviceLock from '../src/api/device/DeviceLock';
7
+ import DeviceRebootToBoardloader from '../src/api/device/DeviceRebootToBoardloader';
8
+ import DeviceRebootToBootloader from '../src/api/device/DeviceRebootToBootloader';
6
9
  import DeviceSettings from '../src/api/device/DeviceSettings';
7
10
  import DeviceVerify from '../src/api/device/DeviceVerify';
8
11
  import DeviceWipe from '../src/api/device/DeviceWipe';
12
+ import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
13
+ import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
14
+ import DeviceUploadWallpaper from '../src/api/protocol-v2/DeviceUploadWallpaper';
9
15
  import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4';
10
16
  import OpenWalletSession from '../src/api/OpenWalletSession';
11
17
  import DataManager from '../src/data-manager/DataManager';
@@ -16,6 +22,12 @@ jest.mock('../src/data/config', () => ({
16
22
  getSDKVersion: () => '0.0.0-test',
17
23
  }));
18
24
 
25
+ const createJpegBase64 = (width: number, height: number) => {
26
+ const data = new Uint8Array(width * height * 4);
27
+ for (let index = 3; index < data.length; index += 4) data[index] = 0xff;
28
+ return encodeJpeg({ width, height, data }, 80).data.toString('base64');
29
+ };
30
+
19
31
  describe('Protocol V2 unlock semantics', () => {
20
32
  test('serializes genuine-device verification with Protocol V2 certificate messages', async () => {
21
33
  const method = new DeviceVerify({
@@ -64,6 +76,40 @@ describe('Protocol V2 unlock semantics', () => {
64
76
  expect(method.getSupportedProtocols()).toContain('V2');
65
77
  });
66
78
 
79
+ test.each([
80
+ [
81
+ 'deviceReboot',
82
+ () =>
83
+ new DeviceReboot({
84
+ id: 1,
85
+ payload: { method: 'deviceReboot', rebootType: 2 },
86
+ }),
87
+ ],
88
+ [
89
+ 'deviceRebootToBootloader',
90
+ () =>
91
+ new DeviceRebootToBootloader({
92
+ id: 1,
93
+ payload: { method: 'deviceRebootToBootloader' },
94
+ }),
95
+ ],
96
+ [
97
+ 'deviceRebootToBoardloader',
98
+ () =>
99
+ new DeviceRebootToBoardloader({
100
+ id: 1,
101
+ payload: { method: 'deviceRebootToBoardloader' },
102
+ }),
103
+ ],
104
+ ])('pre-unlocks %s before sending DeviceReboot', (_name, createMethod) => {
105
+ const method = createMethod();
106
+ method.init();
107
+
108
+ expect(method.unlockPolicy).toBe('unlock-before-run');
109
+ expect(method.protocolV2PreUnlockPinType).toBe(DeviceSessionPinType.Any);
110
+ expect(method.useDevicePassphraseState).toBe(false);
111
+ });
112
+
67
113
  test('lock-free Protocol V2 controls explicitly opt out of wallet-session handling', () => {
68
114
  const method = new DeviceLock({
69
115
  id: 1,
@@ -258,6 +304,32 @@ describe('Protocol V2 unlock semantics', () => {
258
304
  payload: { method: 'deviceVerify', dataHex: '00' },
259
305
  }),
260
306
  ],
307
+ [
308
+ 'wallpaper uploads',
309
+ () =>
310
+ new DeviceUploadWallpaper({
311
+ id: 1,
312
+ payload: {
313
+ method: 'deviceUploadWallpaper',
314
+ jpegBase64: createJpegBase64(604, 1024),
315
+ },
316
+ }),
317
+ ],
318
+ [
319
+ 'NFT uploads',
320
+ () =>
321
+ new DeviceUploadNft({
322
+ id: 1,
323
+ payload: {
324
+ method: 'deviceUploadNft',
325
+ imageJpegBase64: createJpegBase64(540, 540),
326
+ thumbnailJpegBase64: createJpegBase64(263, 263),
327
+ title: 'NFT',
328
+ subtitle: '',
329
+ timestampMs: 1,
330
+ },
331
+ }),
332
+ ],
261
333
  ])('allows either PIN type when pre-unlocking %s', async (_name, createMethod) => {
262
334
  const method = createMethod();
263
335
  method.init();